Skip to content

Commit 57fe04d

Browse files
committed
use bump allocator (bumpalo) to speed up allocations for partial paths, in find_path()
1 parent 5bbdb48 commit 57fe04d

File tree

4 files changed

+69
-38
lines changed

4 files changed

+69
-38
lines changed

Cargo.lock

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ clap = "4.5.29"
6363
rand_chacha = "0.3.1"
6464
bitvec = "1.0.1"
6565
arbitrary = { version = "1.4.1", features = ["derive"] }
66+
bumpalo = "3.17.0"
6667

6768
[dependencies]
6869
lazy_static = { workspace = true }
@@ -81,6 +82,7 @@ sha3 = "0.10.8"
8182
rand = { workspace = true }
8283
hex = { workspace = true }
8384
sha1 = { workspace = true }
85+
bumpalo = { workspace = true }
8486

8587
[dev-dependencies]
8688
rstest = { workspace = true }

src/serde/path_builder.rs

+54-29
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use bumpalo::Bump;
2+
13
#[repr(u8)]
24
#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
35
pub enum ChildPos {
@@ -9,33 +11,42 @@ pub enum ChildPos {
911
/// are laid out in big-endian order, where the left-most byte is index 0. The
1012
/// path is built from left to right, since it's parsed right to left when
1113
/// followed).
12-
#[derive(Clone, Debug, PartialEq)]
13-
pub struct PathBuilder {
14-
// TODO: It might make sense to implement small object optimization here.
15-
// The vast majority of paths are just a single byte, statically allocate 8
16-
// would seem reasonable
17-
store: Vec<u8>,
14+
#[derive(Debug, PartialEq)]
15+
pub struct PathBuilder<'a> {
16+
store: &'a mut [u8],
17+
in_use: u32,
1818
/// the bit the next write will happen to (counts down)
1919
bit_pos: u8,
2020
}
2121

22-
impl Default for PathBuilder {
22+
impl Default for PathBuilder<'_> {
2323
fn default() -> Self {
2424
Self {
25-
store: Vec::with_capacity(16),
25+
store: &mut [],
26+
in_use: 0,
2627
bit_pos: 7,
2728
}
2829
}
2930
}
3031

31-
impl PathBuilder {
32-
pub fn push(&mut self, dir: ChildPos) {
32+
impl<'a> PathBuilder<'a> {
33+
pub fn push(&mut self, a: &'a Bump, dir: ChildPos) {
3334
if self.bit_pos == 7 {
34-
self.store.push(0);
35+
if self.in_use as usize == self.store.len() {
36+
let old_size = self.store.len();
37+
let new_size = std::cmp::max(old_size * 2, 16);
38+
let new_store = a.alloc_slice_fill_default::<u8>(new_size);
39+
new_store[0..old_size].copy_from_slice(self.store);
40+
self.store = new_store;
41+
}
42+
self.in_use += 1;
3543
}
3644

45+
assert!(self.in_use > 0);
46+
assert!(self.store.len() >= self.in_use as usize);
47+
3748
if dir == ChildPos::Right {
38-
*self.store.last_mut().unwrap() |= 1 << self.bit_pos;
49+
self.store[self.in_use as usize - 1] |= 1 << self.bit_pos;
3950
}
4051
if self.bit_pos == 0 {
4152
self.bit_pos = 7;
@@ -44,20 +55,28 @@ impl PathBuilder {
4455
}
4556
}
4657

47-
pub fn done(mut self) -> Vec<u8> {
58+
pub fn clone(&self, a: &'a Bump) -> Self {
59+
Self {
60+
store: a.alloc_slice_copy(self.store),
61+
in_use: self.in_use,
62+
bit_pos: self.bit_pos,
63+
}
64+
}
65+
66+
pub fn done(self) -> Vec<u8> {
4867
if self.bit_pos < 7 {
4968
let right_shift = self.bit_pos + 1;
5069
let left_shift = 7 - self.bit_pos;
5170
// we need to shift all bits to the right, to right-align the path
5271
let mask = 0xff << left_shift;
53-
for idx in (1..self.store.len()).rev() {
72+
for idx in (1..self.in_use as usize).rev() {
5473
self.store[idx] >>= right_shift;
5574
let from_next = self.store[idx - 1] << left_shift;
5675
self.store[idx] |= from_next & mask;
5776
}
5877
self.store[0] >>= right_shift;
5978
}
60-
self.store
79+
self.store[0..self.in_use as usize].to_vec()
6180
}
6281

6382
pub fn is_empty(&self) -> bool {
@@ -66,17 +85,17 @@ impl PathBuilder {
6685

6786
pub fn len(&self) -> u32 {
6887
if self.bit_pos == 7 {
69-
(self.store.len() as u32) * u8::BITS
88+
self.in_use * u8::BITS
7089
} else {
71-
(self.store.len() as u32) * u8::BITS - self.bit_pos as u32 - 1
90+
self.in_use * u8::BITS - self.bit_pos as u32 - 1
7291
}
7392
}
7493

7594
/// returns the number of bytes this atom would need to serialize If this,
7695
/// plus 1 (for the 0xfe introduction) is larger or equal to the one we're
7796
/// deduplicating, we should leave it.
7897
pub fn serialized_length(&self) -> u32 {
79-
let len = self.store.len() as u32;
98+
let len = self.in_use;
8099
match len {
81100
0 => 1,
82101
// if we have one byte, the top bit determines whether we can
@@ -108,7 +127,7 @@ impl PathBuilder {
108127
match lhs_len.cmp(&rhs_len) {
109128
Ordering::Less => true,
110129
Ordering::Greater => false,
111-
Ordering::Equal => self.store <= rhs.store,
130+
Ordering::Equal => rhs.store.cmp(&self.store) != Ordering::Less,
112131
}
113132
}
114133
}
@@ -120,17 +139,20 @@ mod tests {
120139
use hex;
121140
use rstest::rstest;
122141

123-
fn build_path(input: &[u8]) -> PathBuilder {
142+
fn build_path<'a>(a: &'a Bump, input: &[u8]) -> PathBuilder<'a> {
124143
let mut path = PathBuilder::default();
125144
// keep in mind that paths are built in reverse order (starting from the
126145
// target).
127146
for (idx, b) in input.iter().enumerate() {
128147
assert_eq!(path.len(), idx as u32);
129-
path.push(if *b == 0 {
130-
ChildPos::Left
131-
} else {
132-
ChildPos::Right
133-
});
148+
path.push(
149+
a,
150+
if *b == 0 {
151+
ChildPos::Left
152+
} else {
153+
ChildPos::Right
154+
},
155+
);
134156
}
135157
path
136158
}
@@ -155,7 +177,8 @@ mod tests {
155177
#[case(&[1,1,1,0,0], "1c")]
156178
#[case(&[1,0,1,0,0,1,0,0,0], "0148")]
157179
fn test_build(#[case] input: &[u8], #[case] expect: &str) {
158-
let path = build_path(input);
180+
let a = Bump::new();
181+
let path = build_path(&a, input);
159182
let ret = path.done();
160183
assert_eq!(hex::encode(ret), expect);
161184
}
@@ -187,8 +210,9 @@ mod tests {
187210
#[case(&[1,1,0,0,0,0,0,0,0], &[1,0,0,0,0,0,0,0,0], false)]
188211
#[case(&[1,1,0,0,0,0,0,0,1], &[1,0,0,0,0,0,0,0,0], false)]
189212
fn test_better_than(#[case] lhs: &[u8], #[case] rhs: &[u8], #[case] expect: bool) {
190-
let lhs = build_path(lhs);
191-
let rhs = build_path(rhs);
213+
let a = Bump::new();
214+
let lhs = build_path(&a, lhs);
215+
let rhs = build_path(&a, rhs);
192216
assert_eq!(lhs.better_than(&rhs), expect);
193217
}
194218

@@ -209,9 +233,10 @@ mod tests {
209233
#[case(513)]
210234
#[case(0xfff9)]
211235
fn test_serialized_length(#[case] num_bits: u32) {
236+
let a = Bump::new();
212237
let mut path = PathBuilder::default();
213238
for _ in 0..num_bits {
214-
path.push(ChildPos::Right);
239+
path.push(&a, ChildPos::Right);
215240
}
216241
let ser_len = path.serialized_length();
217242
let vec = path.done();

src/serde/tree_cache.rs

+10-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::allocator::{Allocator, NodePtr, SExp};
33
use crate::serde::serialized_length_atom;
44
use crate::serde::RandomState;
55
use crate::serde::VisitedNodes;
6+
use bumpalo::Bump;
67
use rand::prelude::*;
78
use sha1::{Digest, Sha1};
89
use std::collections::hash_map::Entry;
@@ -39,9 +40,9 @@ struct NodeEntry {
3940
pub on_stack: u32,
4041
}
4142

42-
struct PartialPath {
43+
struct PartialPath<'alloc> {
4344
// the path we've built so far
44-
path: PathBuilder,
45+
path: PathBuilder<'alloc>,
4546
// if we're traversing the stack, this is the stack position. Note that this
4647
// is not an index into the stack array, it's a counter of how far away from
4748
// the top of the stack we are. 0 means we're at the top, and we've found
@@ -387,6 +388,8 @@ impl TreeCache {
387388

388389
let mut seen = VisitedNodes::new(self.node_entry.len() as u32);
389390

391+
let arena = Bump::new();
392+
390393
// We perform a breadth-first search from the node we're finding a path
391394
// to, up through its parents until we find the top of the stack. Note
392395
// since nodes are deduplicated, they may have multiple parents.
@@ -432,7 +435,7 @@ impl TreeCache {
432435
// we found the shortest path
433436
break partial_paths.swap_remove(cursor).path;
434437
}
435-
p.path.push(ChildPos::Right);
438+
p.path.push(&arena, ChildPos::Right);
436439
p.stack_pos -= 1;
437440
cursor += 1;
438441
if cursor >= partial_paths.len() {
@@ -451,7 +454,7 @@ impl TreeCache {
451454
}
452455
continue;
453456
}
454-
p.path.push(p.child);
457+
p.path.push(&arena, p.child);
455458

456459
let entry = &self.node_entry[p.idx as usize];
457460
let idx = p.idx;
@@ -478,14 +481,14 @@ impl TreeCache {
478481
// from now on, we can't use "p" anymore, since we're about to
479482
// mutate partial_paths and p is a reference into one of its
480483
// elements
481-
let mut current_path = p.path.clone();
484+
let mut current_path = p.path.clone(&arena);
482485
debug_assert_eq!(self.node_entry[idx as usize].tree_hash, entry.tree_hash);
483486

484487
debug_assert!(remaining_parents.is_empty() || used_p);
485488
for parent in remaining_parents {
486489
if !seen.is_visited(parent.0) {
487490
partial_paths.push(PartialPath {
488-
path: current_path.clone(),
491+
path: current_path.clone(&arena),
489492
stack_pos: -1,
490493
idx: parent.0,
491494
child: parent.1,
@@ -494,7 +497,7 @@ impl TreeCache {
494497
}
495498
if entry.on_stack > 0 {
496499
// this is to pick the stack entry (left value)
497-
current_path.push(ChildPos::Left);
500+
current_path.push(&arena, ChildPos::Left);
498501

499502
// now step down the stack until we find the element
500503
// the stack grows downwards (indices going up). Now we're starting from

0 commit comments

Comments
 (0)