Skip to content

Commit b98d12b

Browse files
authored
perf: optimize bloom membership checks (#1134)
* perf: avoid temporary bitset in FixedBytes covers * perf: avoid temporary blooms in membership checks
1 parent d153d17 commit b98d12b

2 files changed

Lines changed: 52 additions & 22 deletions

File tree

crates/primitives/src/bits/bloom.rs

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ pub const BLOOM_SIZE_BYTES: usize = 256;
1111
/// Size of the bloom filter in bits
1212
pub const BLOOM_SIZE_BITS: usize = BLOOM_SIZE_BYTES * 8;
1313

14-
/// Mask, used in accrue
15-
const MASK: usize = BLOOM_SIZE_BITS - 1;
16-
/// Number of bytes per item, used in accrue
17-
const ITEM_BYTES: usize = BLOOM_SIZE_BITS.ilog2().div_ceil(8) as usize;
18-
1914
// BLOOM_SIZE_BYTES must be a power of 2
2015
#[allow(clippy::assertions_on_constants)]
2116
const _: () = assert!(BLOOM_SIZE_BYTES.is_power_of_two());
@@ -128,7 +123,8 @@ impl Bloom {
128123
/// bloom filter data structure.
129124
#[inline]
130125
pub fn contains_input(&self, input: BloomInput<'_>) -> bool {
131-
self.contains(&input.into())
126+
let hash = input.into_hash();
127+
self.contains_m3_2048_hashed(&hash)
132128
}
133129

134130
/// Compile-time version of [`contains`](Self::contains).
@@ -151,18 +147,7 @@ impl Bloom {
151147
/// Accrues the input into the bloom filter.
152148
pub fn accrue(&mut self, input: BloomInput<'_>) {
153149
let hash = input.into_hash();
154-
155-
let mut ptr = 0;
156-
157-
for _ in 0..3 {
158-
let mut index = 0_usize;
159-
for _ in 0..ITEM_BYTES {
160-
index = (index << 8) | hash[ptr] as usize;
161-
ptr += 1;
162-
}
163-
index &= MASK;
164-
self.0[BLOOM_SIZE_BYTES - 1 - index / 8] |= 1 << (index % 8);
165-
}
150+
self.m3_2048_hashed(&hash);
166151
}
167152

168153
/// Accrues the input into the bloom filter.
@@ -189,6 +174,24 @@ impl Bloom {
189174
}
190175
}
191176

177+
/// Returns true if this bloom filter contains the bits set by [`m3_2048`](Self::m3_2048).
178+
fn contains_m3_2048(&self, bytes: &[u8]) -> bool {
179+
self.contains_m3_2048_hashed(&keccak256(bytes))
180+
}
181+
182+
/// [`contains_m3_2048`](Self::contains_m3_2048) but with a pre-hashed input.
183+
fn contains_m3_2048_hashed(&self, hash: &B256) -> bool {
184+
for i in [0, 2, 4] {
185+
// Each adjacent hash byte pair forms one big-endian 11-bit index.
186+
// Bloom bytes are stored high-to-low, while bits within a byte are low-to-high.
187+
let bit = (hash[i + 1] as usize + ((hash[i] as usize) << 8)) & 0x7FF;
188+
if self[BLOOM_SIZE_BYTES - 1 - bit / 8] & (1 << (bit % 8)) == 0 {
189+
return false;
190+
}
191+
}
192+
true
193+
}
194+
192195
/// Ingests a raw log into the bloom filter.
193196
pub fn accrue_raw_log(&mut self, address: Address, topics: &[B256]) {
194197
self.m3_2048(address.as_slice());
@@ -214,9 +217,8 @@ impl Bloom {
214217
/// Note: This method may return false positives. This is inherent to the
215218
/// bloom filter data structure.
216219
pub fn contains_raw_log(&self, address: Address, topics: &[B256]) -> bool {
217-
let mut bloom = Self::default();
218-
bloom.accrue_raw_log(address, topics);
219-
self.contains(&bloom)
220+
self.contains_m3_2048(address.as_slice())
221+
&& topics.iter().all(|topic| self.contains_m3_2048(topic.as_slice()))
220222
}
221223

222224
/// True if the bloom filter contains a log with given address and topics.
@@ -255,19 +257,47 @@ mod tests {
255257
);
256258
let address = hex!("ef2d6d194084c2de36e0dabfce45d046b37d1106");
257259
let topic = hex!("02c69be41d0b7e40352fc85be1cd65eb03d40ef8427a0ca4596b1ead9a00e9fc");
260+
let address_hash = keccak256(address);
261+
let topic_hash = keccak256(topic);
258262

259263
let mut my_bloom = Bloom::default();
260264
assert!(!my_bloom.contains_input(BloomInput::Raw(&address)));
261265
assert!(!my_bloom.contains_input(BloomInput::Raw(&topic)));
266+
assert!(!my_bloom.contains_input(BloomInput::Hash(address_hash)));
267+
assert!(!my_bloom.contains_input(BloomInput::Hash(topic_hash)));
262268

263269
my_bloom.accrue(BloomInput::Raw(&address));
264270
assert!(my_bloom.contains_input(BloomInput::Raw(&address)));
265271
assert!(!my_bloom.contains_input(BloomInput::Raw(&topic)));
272+
assert!(my_bloom.contains_input(BloomInput::Hash(address_hash)));
273+
assert!(!my_bloom.contains_input(BloomInput::Hash(topic_hash)));
266274

267275
my_bloom.accrue(BloomInput::Raw(&topic));
268276
assert!(my_bloom.contains_input(BloomInput::Raw(&address)));
269277
assert!(my_bloom.contains_input(BloomInput::Raw(&topic)));
278+
assert!(my_bloom.contains_input(BloomInput::Hash(address_hash)));
279+
assert!(my_bloom.contains_input(BloomInput::Hash(topic_hash)));
270280

271281
assert_eq!(my_bloom, bloom);
272282
}
283+
284+
#[test]
285+
#[cfg(feature = "arbitrary")]
286+
#[cfg_attr(miri, ignore = "proptest is too slow under miri")]
287+
fn contains_raw_log_matches_bloom_contains() {
288+
use proptest::{arbitrary::any, collection::vec};
289+
290+
fn contains_via_bloom(bloom: &Bloom, address: Address, topics: &[B256]) -> bool {
291+
let mut log_bloom = Bloom::default();
292+
log_bloom.accrue_raw_log(address, topics);
293+
bloom.contains(&log_bloom)
294+
}
295+
296+
proptest::proptest!(|(bloom: Bloom, address: Address, topics in vec(any::<B256>(), 0..8))| {
297+
proptest::prop_assert_eq!(
298+
bloom.contains_raw_log(address, &topics),
299+
contains_via_bloom(&bloom, address, &topics)
300+
);
301+
});
302+
}
273303
}

crates/primitives/src/bits/fixed.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ impl<const N: usize> FixedBytes<N> {
604604
/// Returns `true` if all bits set in `self` are also set in `b`.
605605
#[inline]
606606
pub fn covers(&self, other: &Self) -> bool {
607-
(*self & *other) == *other
607+
iter::zip(self, other).fold(0, |acc, (a, b)| acc | (b & !a)) == 0
608608
}
609609

610610
/// Returns `true` if all bits set in `self` are also set in `b`.

0 commit comments

Comments
 (0)