Skip to content

Commit 8e2f86b

Browse files
authored
feat: cacheless hamt iteration (#2216)
* feat: cacheless hamt iteration * update bench * refactor * parity test * fix iteration order * docs and changelog * resolve AI comments * remove unnecessary Clone
1 parent b5883f4 commit 8e2f86b

10 files changed

Lines changed: 375 additions & 60 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fvm/src/state_tree.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ where
364364
self.hamt.into_store()
365365
}
366366

367+
/// Iterates over each KV in the Hamt and runs a function on the values with cache.
367368
pub fn for_each<F>(&self, mut f: F) -> anyhow::Result<()>
368369
where
369370
F: FnMut(Address, &ActorState) -> anyhow::Result<()>,
@@ -374,4 +375,16 @@ where
374375
})?;
375376
Ok(())
376377
}
378+
379+
/// Iterates over each KV in the Hamt and runs a function on the values without cache.
380+
pub fn for_each_cacheless<F>(&self, mut f: F) -> anyhow::Result<()>
381+
where
382+
F: FnMut(Address, &ActorState) -> anyhow::Result<()>,
383+
{
384+
self.hamt.for_each_cacheless(|k, v| {
385+
let addr = Address::from_bytes(&k.0)?;
386+
f(addr, v)
387+
})?;
388+
Ok(())
389+
}
377390
}

ipld/hamt/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Changes to the reference FVM's HAMT implementation.
44

55
## [Unreleased]
66

7+
- Added `for_each_cacheless` method to iterate over the HAMT without caching the values. This is lowers memory requirements usage and is useful for single-pass, read-only operations over large HAMTs.
8+
79
## 0.10.4 [2025-04-09]
810

911
- Updates multiple dependencies (semver breaking internally but not exported).

ipld/hamt/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ unsigned-varint = { workspace = true }
3232
quickcheck = { workspace = true }
3333
quickcheck_macros = { workspace = true }
3434
rand = { workspace = true }
35+
itertools = { workspace = true }
3536

3637
[[bench]]
3738
name = "hamt_beckmark"

ipld/hamt/benches/hamt_benchmark.rs

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
use std::hint::black_box;
66

77
use criterion::{Criterion, criterion_group, criterion_main};
8+
use fvm_ipld_blockstore::{Blockstore, MemoryBlockstore};
89
use fvm_ipld_encoding::tuple::*;
910
use fvm_ipld_hamt::Hamt;
1011

12+
const BIT_WIDTH: u32 = 5;
1113
const ITEM_COUNT: u8 = 40;
1214

1315
// Struct to simulate a reasonable amount of data per value into the amt
@@ -37,8 +39,8 @@ impl BenchData {
3739
fn insert(c: &mut Criterion) {
3840
c.bench_function("HAMT bulk insert (no flush)", |b| {
3941
b.iter(|| {
40-
let db = fvm_ipld_blockstore::MemoryBlockstore::default();
41-
let mut a = Hamt::<_, _>::new_with_bit_width(&db, 5);
42+
let db = MemoryBlockstore::default();
43+
let mut a = Hamt::<_, _>::new_with_bit_width(&db, BIT_WIDTH);
4244

4345
for i in 0..black_box(ITEM_COUNT) {
4446
a.set(black_box(vec![i; 20].into()), black_box(BenchData::new(i)))
@@ -51,12 +53,12 @@ fn insert(c: &mut Criterion) {
5153
fn insert_load_flush(c: &mut Criterion) {
5254
c.bench_function("HAMT bulk insert with flushing and loading", |b| {
5355
b.iter(|| {
54-
let db = fvm_ipld_blockstore::MemoryBlockstore::default();
55-
let mut empt = Hamt::<_, ()>::new_with_bit_width(&db, 5);
56+
let db = MemoryBlockstore::default();
57+
let mut empt = Hamt::<_, ()>::new_with_bit_width(&db, BIT_WIDTH);
5658
let mut cid = empt.flush().unwrap();
5759

5860
for i in 0..black_box(ITEM_COUNT) {
59-
let mut a = Hamt::<_, _>::load_with_bit_width(&cid, &db, 5).unwrap();
61+
let mut a = Hamt::<_, _>::load_with_bit_width(&cid, &db, BIT_WIDTH).unwrap();
6062
a.set(black_box(vec![i; 20].into()), black_box(BenchData::new(i)))
6163
.unwrap();
6264
cid = a.flush().unwrap();
@@ -66,16 +68,13 @@ fn insert_load_flush(c: &mut Criterion) {
6668
}
6769

6870
fn delete(c: &mut Criterion) {
69-
let db = fvm_ipld_blockstore::MemoryBlockstore::default();
70-
let mut a = Hamt::<_, _>::new_with_bit_width(&db, 5);
71-
for i in 0..black_box(ITEM_COUNT) {
72-
a.set(vec![i; 20].into(), BenchData::new(i)).unwrap();
73-
}
71+
let db = MemoryBlockstore::default();
72+
let mut a = setup_hamt(&db);
7473
let cid = a.flush().unwrap();
7574

7675
c.bench_function("HAMT deleting all nodes", |b| {
7776
b.iter(|| {
78-
let mut a = Hamt::<_, BenchData>::load_with_bit_width(&cid, &db, 5).unwrap();
77+
let mut a = Hamt::<_, BenchData>::load_with_bit_width(&cid, &db, BIT_WIDTH).unwrap();
7978
for i in 0..black_box(ITEM_COUNT) {
8079
a.delete(black_box([i; 20].as_ref())).unwrap();
8180
}
@@ -84,20 +83,47 @@ fn delete(c: &mut Criterion) {
8483
}
8584

8685
fn for_each(c: &mut Criterion) {
87-
let db = fvm_ipld_blockstore::MemoryBlockstore::default();
88-
let mut a = Hamt::<_, _>::new_with_bit_width(&db, 5);
89-
for i in 0..black_box(ITEM_COUNT) {
90-
a.set(vec![i; 20].into(), BenchData::new(i)).unwrap();
91-
}
86+
let db = MemoryBlockstore::default();
87+
let mut a = setup_hamt(&db);
9288
let cid = a.flush().unwrap();
9389

9490
c.bench_function("HAMT for_each function", |b| {
9591
b.iter(|| {
96-
let a = Hamt::<_, _>::load_with_bit_width(&cid, &db, 5).unwrap();
92+
let a = Hamt::<_, _>::load_with_bit_width(&cid, &db, BIT_WIDTH).unwrap();
9793
black_box(a).for_each(|_k, _v: &BenchData| Ok(())).unwrap();
9894
})
9995
});
10096
}
10197

102-
criterion_group!(benches, insert, insert_load_flush, delete, for_each);
98+
fn for_each_cacheless(c: &mut Criterion) {
99+
let db = MemoryBlockstore::default();
100+
let mut a = setup_hamt(&db);
101+
let cid = a.flush().unwrap();
102+
103+
c.bench_function("HAMT for_each_cacheless function", |b| {
104+
b.iter(|| {
105+
let a = Hamt::<_, _>::load_with_bit_width(&cid, &db, BIT_WIDTH).unwrap();
106+
black_box(a)
107+
.for_each_cacheless(|_k, _v: &BenchData| Ok(()))
108+
.unwrap();
109+
})
110+
});
111+
}
112+
113+
fn setup_hamt<BS: Blockstore>(db: &BS) -> Hamt<&BS, BenchData> {
114+
let mut a = Hamt::<_, _>::new_with_bit_width(db, BIT_WIDTH);
115+
for i in 0..ITEM_COUNT {
116+
a.set(vec![i; 20].into(), BenchData::new(i)).unwrap();
117+
}
118+
a
119+
}
120+
121+
criterion_group!(
122+
benches,
123+
insert,
124+
insert_load_flush,
125+
delete,
126+
for_each,
127+
for_each_cacheless
128+
);
103129
criterion_main!(benches);

ipld/hamt/src/hamt.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,37 @@ where
382382
Ok(())
383383
}
384384

385+
/// Iterates over each KV in the Hamt and runs a function on the values. This is a
386+
/// non-caching version of [`Self::for_each`]. It can potentially be more efficient, especially memory-wise,
387+
/// for large HAMTs or when the iteration occurs only once.
388+
///
389+
/// # Examples
390+
///
391+
/// ```
392+
/// use fvm_ipld_hamt::Hamt;
393+
///
394+
/// let store = fvm_ipld_blockstore::MemoryBlockstore::default();
395+
///
396+
/// let mut map: Hamt<_, _, usize> = Hamt::new(store);
397+
/// map.set(1, 1).unwrap();
398+
/// map.set(4, 2).unwrap();
399+
///
400+
/// let mut total = 0;
401+
/// map.for_each_cacheless(|_, v: &u64| {
402+
/// total += v;
403+
/// Ok(())
404+
/// }).unwrap();
405+
/// assert_eq!(total, 3);
406+
/// ```
407+
pub fn for_each_cacheless<F>(&self, mut f: F) -> Result<(), Error>
408+
where
409+
V: DeserializeOwned,
410+
F: FnMut(&K, &V) -> anyhow::Result<()>,
411+
{
412+
self.root
413+
.for_each_cacheless(&self.store, &self.conf, &mut f)
414+
}
415+
385416
/// Iterates over each KV in the Hamt and runs a function on the values. If starting key is
386417
/// provided, iteration will start from that key. If max is provided, iteration will stop after
387418
/// max number of items have been traversed. The number of items that were traversed is

ipld/hamt/src/hash_algorithm.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ pub enum Identity {}
7272

7373
#[cfg(feature = "identity")]
7474
impl HashAlgorithm for Identity {
75-
fn hash<X: ?Sized>(key: &X) -> HashedKey
75+
fn hash<X>(key: &X) -> HashedKey
7676
where
77-
X: Hash,
77+
X: Hash + ?Sized,
7878
{
7979
let mut ident_hasher = IdentityHasher::default();
8080
key.hash(&mut ident_hasher);

ipld/hamt/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ impl Default for Config {
7575

7676
type HashedKey = [u8; 32];
7777

78-
#[derive(Debug, Serialize, Deserialize, PartialEq)]
78+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7979
struct KeyValuePair<K, V>(K, V);
8080

8181
impl<K, V> KeyValuePair<K, V> {

ipld/hamt/src/node.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,86 @@ where
206206
self.pointers.is_empty()
207207
}
208208

209+
/// Non-caching iteration over the values in the node.
210+
pub(super) fn for_each_cacheless<S, F>(
211+
&self,
212+
bs: &S,
213+
conf: &Config,
214+
f: &mut F,
215+
) -> Result<(), Error>
216+
where
217+
F: FnMut(&K, &V) -> anyhow::Result<()>,
218+
S: Blockstore,
219+
{
220+
enum IterItem<'a, T> {
221+
Borrowed(&'a T),
222+
Owned(T),
223+
}
224+
225+
enum StackItem<'a, T> {
226+
Iter(std::slice::Iter<'a, T>),
227+
IntoIter(std::vec::IntoIter<T>),
228+
}
229+
230+
impl<'a, V> From<std::slice::Iter<'a, V>> for StackItem<'a, V> {
231+
fn from(value: std::slice::Iter<'a, V>) -> Self {
232+
Self::Iter(value)
233+
}
234+
}
235+
236+
impl<V> From<std::vec::IntoIter<V>> for StackItem<'_, V> {
237+
fn from(value: std::vec::IntoIter<V>) -> Self {
238+
Self::IntoIter(value)
239+
}
240+
}
241+
242+
impl<'a, V> Iterator for StackItem<'a, V> {
243+
type Item = IterItem<'a, V>;
244+
245+
fn next(&mut self) -> Option<Self::Item> {
246+
match self {
247+
Self::Iter(it) => it.next().map(IterItem::Borrowed),
248+
Self::IntoIter(it) => it.next().map(IterItem::Owned),
249+
}
250+
}
251+
}
252+
253+
let mut stack: Vec<StackItem<_>> = vec![self.pointers.iter().into()];
254+
loop {
255+
let Some(pointers) = stack.last_mut() else {
256+
return Ok(());
257+
};
258+
let Some(pointer) = pointers.next() else {
259+
stack.pop();
260+
continue;
261+
};
262+
match pointer {
263+
IterItem::Borrowed(Pointer::Link { cid, cache: _ }) => {
264+
let node = Node::load(conf, bs, cid, stack.len() as u32)?;
265+
stack.push(node.pointers.into_iter().into())
266+
}
267+
IterItem::Owned(Pointer::Link { cid, cache: _ }) => {
268+
let node = Node::load(conf, bs, &cid, stack.len() as u32)?;
269+
stack.push(node.pointers.into_iter().into())
270+
}
271+
IterItem::Borrowed(Pointer::Dirty(node)) => stack.push(node.pointers.iter().into()),
272+
IterItem::Owned(Pointer::Dirty(node)) => {
273+
stack.push(node.pointers.into_iter().into())
274+
}
275+
IterItem::Borrowed(Pointer::Values(kvs)) => {
276+
for kv in kvs.iter() {
277+
f(kv.key(), kv.value())?;
278+
}
279+
}
280+
IterItem::Owned(Pointer::Values(kvs)) => {
281+
for kv in kvs.iter() {
282+
f(kv.key(), kv.value())?;
283+
}
284+
}
285+
}
286+
}
287+
}
288+
209289
/// Search for a key.
210290
fn search<Q, S: Blockstore>(
211291
&self,

0 commit comments

Comments
 (0)