Skip to content

Commit 1e6a772

Browse files
committed
replace bitvec with faster warm address bits lookup
nits use direct path to std format
1 parent cfcf038 commit 1e6a772

2 files changed

Lines changed: 251 additions & 22 deletions

File tree

crates/context/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ bytecode.workspace = true
2828
# misc
2929
derive-where.workspace = true
3030
cfg-if.workspace = true
31-
bitvec.workspace = true
3231

3332
# Optional
3433
serde = { workspace = true, features = ["derive", "rc"], optional = true }
3534

3635
[dev-dependencies]
3736
database.workspace = true
37+
bitvec.workspace = true
3838

3939
[features]
4040
default = ["std"]
@@ -46,7 +46,6 @@ std = [
4646
"database-interface/std",
4747
"primitives/std",
4848
"state/std",
49-
"bitvec/std",
5049
]
5150
serde = [
5251
"dep:serde",
@@ -57,7 +56,6 @@ serde = [
5756
"database/serde",
5857
"database-interface/serde",
5958
"derive-where/serde",
60-
"bitvec/serde",
6159
]
6260
dev = [
6361
"memory_limit",

crates/context/src/journal/warm_addresses.rs

Lines changed: 250 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,42 @@
22
//!
33
//! It is used to optimize access to precompile addresses.
44
5-
use bitvec::{bitvec, vec::BitVec};
65
use context_interface::journaled_state::JournalLoadError;
76
use primitives::{
87
short_address, Address, AddressMap, AddressSet, HashSet, StorageKey, SHORT_ADDRESS_CAP,
98
};
109

10+
/// Number of bytes needed to hold SHORT_ADDRESS_CAP bits (300 bits == 38 bytes).
11+
const PRECOMPILE_SHORT_ADDRESS_BYTES: usize = (SHORT_ADDRESS_CAP + 7) / 8;
12+
13+
#[cfg(feature = "serde")]
14+
mod fixed_array {
15+
use serde::{Deserialize, Deserializer, Serializer};
16+
17+
pub(super) fn serialize<S: Serializer, const N: usize>(
18+
arr: &[u8; N],
19+
serializer: S,
20+
) -> Result<S::Ok, S::Error> {
21+
serializer.serialize_bytes(arr)
22+
}
23+
24+
pub(super) fn deserialize<'de, D: Deserializer<'de>, const N: usize>(
25+
deserializer: D,
26+
) -> Result<[u8; N], D::Error> {
27+
let bytes: &[u8] = Deserialize::deserialize(deserializer)?;
28+
if bytes.len() != N {
29+
return Err(serde::de::Error::custom(std::format!(
30+
"expected {} bytes, got {}",
31+
N,
32+
bytes.len()
33+
)));
34+
}
35+
let mut arr = [0u8; N];
36+
arr.copy_from_slice(bytes);
37+
Ok(arr)
38+
}
39+
}
40+
1141
/// Stores addresses that are warm loaded. Contains precompiles and coinbase address.
1242
///
1343
/// It contains precompiles addresses that are not changed frequently and AccessList that
@@ -22,9 +52,11 @@ use primitives::{
2252
pub struct WarmAddresses {
2353
/// Set of warm loaded precompile addresses.
2454
precompile_set: AddressSet,
25-
/// Bit vector of precompile short addresses. If address is shorter than [`SHORT_ADDRESS_CAP`] it
26-
/// will be stored in this bit vector for faster access.
27-
precompile_short_addresses: BitVec,
55+
/// Fixed bitset of precompile short addresses (one bit per possible short address).
56+
/// Uses a plain byte array + manual bit ops for fast access in the hot path
57+
/// (avoids BitVec overhead, similar to JumpTable optimization).
58+
#[cfg_attr(feature = "serde", serde(with = "fixed_array"))]
59+
precompile_short_addresses: [u8; PRECOMPILE_SHORT_ADDRESS_BYTES],
2860
/// `true` if all precompiles are short addresses.
2961
precompile_all_short_addresses: bool,
3062
/// Coinbase address.
@@ -45,7 +77,7 @@ impl WarmAddresses {
4577
pub fn new() -> Self {
4678
Self {
4779
precompile_set: AddressSet::default(),
48-
precompile_short_addresses: bitvec![0; SHORT_ADDRESS_CAP],
80+
precompile_short_addresses: [0u8; PRECOMPILE_SHORT_ADDRESS_BYTES],
4981
precompile_all_short_addresses: true,
5082
coinbase: None,
5183
access_list: AddressMap::default(),
@@ -66,12 +98,12 @@ impl WarmAddresses {
6698

6799
/// Set the precompile addresses and short addresses.
68100
pub fn set_precompile_addresses(&mut self, addresses: &AddressSet) {
69-
self.precompile_short_addresses.fill(false);
101+
self.precompile_short_addresses.fill(0);
70102

71103
let mut all_short_addresses = true;
72104
for address in addresses.iter() {
73105
if let Some(short_address) = short_address(address) {
74-
self.precompile_short_addresses.set(short_address, true);
106+
self.set_precompile_short(short_address);
75107
} else {
76108
all_short_addresses = false;
77109
}
@@ -81,6 +113,31 @@ impl WarmAddresses {
81113
self.precompile_set.clone_from(addresses);
82114
}
83115

116+
/// Set the bit for a short precompile address.
117+
///
118+
/// Uses manual bit manipulation on a fixed byte array (instead of `BitVec`)
119+
/// to avoid per-access overhead
120+
#[inline(always)]
121+
fn set_precompile_short(&mut self, idx: usize) {
122+
// get the index to the byte in the arr
123+
let byte = idx >> 3;
124+
// bit position within the byte (0..7)
125+
let bit = 1 << (idx & 7);
126+
self.precompile_short_addresses[byte] |= bit;
127+
}
128+
129+
/// Returns whether the bit for the given short address index is set.
130+
///
131+
/// This is the hot path inside `is_warm`/`is_cold` for short precompile
132+
/// addresses. We perform direct byte + bit operations on the fixed array
133+
/// instead of going through `BitVec` (same technique I used in `JumpTable::is_valid`).
134+
#[inline(always)]
135+
pub(crate) fn is_precompile_short(&self, idx: usize) -> bool {
136+
let byte = idx >> 3;
137+
let bit = 1 << (idx & 7);
138+
self.precompile_short_addresses[byte] & bit != 0
139+
}
140+
84141
/// Set the coinbase address.
85142
#[inline]
86143
pub const fn set_coinbase(&mut self, address: Address) {
@@ -131,7 +188,7 @@ impl WarmAddresses {
131188

132189
// check if it is short precompile address
133190
if let Some(short_address) = short_address(address) {
134-
return self.precompile_short_addresses[short_address];
191+
return self.is_precompile_short(short_address);
135192
}
136193

137194
if !self.precompile_all_short_addresses {
@@ -185,9 +242,12 @@ mod tests {
185242
assert!(warm_addresses.precompile_set.is_empty());
186243
assert_eq!(
187244
warm_addresses.precompile_short_addresses.len(),
188-
SHORT_ADDRESS_CAP
245+
PRECOMPILE_SHORT_ADDRESS_BYTES
189246
);
190-
assert!(!warm_addresses.precompile_short_addresses.any());
247+
assert!(warm_addresses
248+
.precompile_short_addresses
249+
.iter()
250+
.all(|&b| b == 0));
191251
assert!(warm_addresses.coinbase.is_none());
192252

193253
// Test Default trait
@@ -234,13 +294,13 @@ mod tests {
234294
assert_eq!(warm_addresses.precompile_set, precompiles);
235295
assert_eq!(
236296
warm_addresses.precompile_short_addresses.len(),
237-
SHORT_ADDRESS_CAP
297+
PRECOMPILE_SHORT_ADDRESS_BYTES
238298
);
239299

240-
// Verify bitvec optimization
241-
assert!(warm_addresses.precompile_short_addresses[1]);
242-
assert!(warm_addresses.precompile_short_addresses[5]);
243-
assert!(!warm_addresses.precompile_short_addresses[0]);
300+
// Verify optimization (replaces old BitVec)
301+
assert!(warm_addresses.is_precompile_short(1));
302+
assert!(warm_addresses.is_precompile_short(5));
303+
assert!(!warm_addresses.is_precompile_short(0));
244304

245305
// Verify warmth detection
246306
assert!(warm_addresses.is_warm(&short_addr1));
@@ -272,7 +332,10 @@ mod tests {
272332

273333
// Verify storage
274334
assert_eq!(warm_addresses.precompile_set, precompiles);
275-
assert!(!warm_addresses.precompile_short_addresses.any());
335+
assert!(warm_addresses
336+
.precompile_short_addresses
337+
.iter()
338+
.all(|&b| b == 0));
276339

277340
// Verify warmth detection
278341
assert!(warm_addresses.is_warm(&regular_addr));
@@ -303,8 +366,8 @@ mod tests {
303366
assert!(warm_addresses.is_warm(&regular_addr));
304367

305368
// Verify short address optimization is used
306-
assert!(warm_addresses.precompile_short_addresses[7]);
307-
assert!(!warm_addresses.precompile_short_addresses[8]);
369+
assert!(warm_addresses.is_precompile_short(7));
370+
assert!(!warm_addresses.is_precompile_short(8));
308371
}
309372

310373
#[test]
@@ -324,6 +387,174 @@ mod tests {
324387
warm_addresses.set_precompile_addresses(&precompiles);
325388

326389
assert!(warm_addresses.is_warm(&boundary_addr));
327-
assert!(warm_addresses.precompile_short_addresses[SHORT_ADDRESS_CAP - 1]);
390+
assert!(warm_addresses.is_precompile_short(SHORT_ADDRESS_CAP - 1));
391+
}
392+
}
393+
394+
#[cfg(test)]
395+
mod bench_is_short_precompile {
396+
use super::*;
397+
use std::time::Instant;
398+
399+
use bitvec::{bitvec, vec::BitVec};
400+
401+
const ITERATIONS: usize = 1_000_000;
402+
const TEST_SIZE: usize = SHORT_ADDRESS_CAP;
403+
404+
/// Legacy BitVec version (for comparison only).
405+
#[derive(Clone)]
406+
struct ShortAddressesWithBitVec {
407+
bits: BitVec,
408+
}
409+
410+
impl ShortAddressesWithBitVec {
411+
fn new() -> Self {
412+
Self {
413+
bits: bitvec![0; SHORT_ADDRESS_CAP],
414+
}
415+
}
416+
417+
fn set_many(&mut self, indices: impl IntoIterator<Item = usize>) {
418+
for i in indices {
419+
if i < SHORT_ADDRESS_CAP {
420+
self.bits.set(i, true);
421+
}
422+
}
423+
}
424+
425+
/// Old BitVec [] style (for comparison).
426+
#[inline]
427+
fn is_set(&self, idx: usize) -> bool {
428+
self.bits[idx]
429+
}
430+
}
431+
432+
fn create_test_data() -> (WarmAddresses, ShortAddressesWithBitVec) {
433+
let mut real = WarmAddresses::new();
434+
let mut precompile_set = AddressSet::default();
435+
436+
for i in (0..TEST_SIZE).step_by(3) {
437+
let mut bytes = [0u8; 20];
438+
bytes[18] = (i >> 8) as u8;
439+
bytes[19] = i as u8;
440+
precompile_set.insert(Address::from(bytes));
441+
}
442+
real.set_precompile_addresses(&precompile_set);
443+
444+
let mut legacy = ShortAddressesWithBitVec::new();
445+
legacy.set_many((0..TEST_SIZE).step_by(3));
446+
447+
(real, legacy)
448+
}
449+
450+
fn benchmark_implementation<F>(name: &str, table: &F, test_fn: impl Fn(&F, usize) -> bool)
451+
where
452+
F: Clone,
453+
{
454+
for i in 0..10_000 {
455+
std::hint::black_box(test_fn(table, i % TEST_SIZE));
456+
}
457+
458+
let start = Instant::now();
459+
let mut count = 0;
460+
461+
for i in 0..ITERATIONS {
462+
if test_fn(table, i % TEST_SIZE) {
463+
count += 1;
464+
}
465+
}
466+
467+
let duration = start.elapsed();
468+
let ns_per_op = duration.as_nanos() as f64 / ITERATIONS as f64;
469+
let ops_per_sec = ITERATIONS as f64 / duration.as_secs_f64();
470+
471+
println!("{name} Performance:");
472+
println!(" Time per op: {ns_per_op:.2} ns");
473+
println!(" Ops per sec: {ops_per_sec:.0}");
474+
println!(" True count: {count}");
475+
println!();
476+
477+
std::hint::black_box(count);
478+
}
479+
480+
#[test]
481+
fn bench_is_short_precompile() {
482+
println!("\nWarmAddresses short precompile bit test Benchmark Comparison");
483+
println!("============================================================");
484+
485+
let (real_warm, legacy_bitvec) = create_test_data();
486+
487+
benchmark_implementation(
488+
"WarmAddresses (Fixed Array + Manual Bits)",
489+
&real_warm,
490+
|wa, idx| wa.is_precompile_short(idx),
491+
);
492+
493+
benchmark_implementation("Legacy (BitVec indexing)", &legacy_bitvec, |lb, idx| {
494+
lb.is_set(idx)
495+
});
496+
497+
println!("Benchmark completed successfully!\n");
498+
}
499+
500+
#[test]
501+
fn bench_different_access_patterns() {
502+
let (real_warm, legacy_bitvec) = create_test_data();
503+
504+
println!("Short Precompile Access Pattern Comparison");
505+
println!("==========================================");
506+
507+
let start = Instant::now();
508+
for i in 0..ITERATIONS {
509+
std::hint::black_box(real_warm.is_precompile_short(i % TEST_SIZE));
510+
}
511+
let fixed_sequential = start.elapsed();
512+
513+
let start = Instant::now();
514+
for i in 0..ITERATIONS {
515+
std::hint::black_box(legacy_bitvec.is_set(i % TEST_SIZE));
516+
}
517+
let bitvec_sequential = start.elapsed();
518+
519+
let start = Instant::now();
520+
for i in 0..ITERATIONS {
521+
std::hint::black_box(real_warm.is_precompile_short((i * 17) % TEST_SIZE));
522+
}
523+
let fixed_random = start.elapsed();
524+
525+
let start = Instant::now();
526+
for i in 0..ITERATIONS {
527+
std::hint::black_box(legacy_bitvec.is_set((i * 17) % TEST_SIZE));
528+
}
529+
let bitvec_random = start.elapsed();
530+
531+
println!("Sequential Access:");
532+
println!(
533+
" Fixed Array: {:.2} ns/op",
534+
fixed_sequential.as_nanos() as f64 / ITERATIONS as f64
535+
);
536+
println!(
537+
" BitVec: {:.2} ns/op",
538+
bitvec_sequential.as_nanos() as f64 / ITERATIONS as f64
539+
);
540+
println!(
541+
" Speedup: {:.1}x",
542+
bitvec_sequential.as_nanos() as f64 / fixed_sequential.as_nanos() as f64
543+
);
544+
545+
println!();
546+
println!("Random Access:");
547+
println!(
548+
" Fixed Array: {:.2} ns/op",
549+
fixed_random.as_nanos() as f64 / ITERATIONS as f64
550+
);
551+
println!(
552+
" BitVec: {:.2} ns/op",
553+
bitvec_random.as_nanos() as f64 / ITERATIONS as f64
554+
);
555+
println!(
556+
" Speedup: {:.1}x",
557+
bitvec_random.as_nanos() as f64 / fixed_random.as_nanos() as f64
558+
);
328559
}
329560
}

0 commit comments

Comments
 (0)