Skip to content

feat(arena): arena-backed hashconsing behind bumpalo feature (2.16x) - #33

Open
elefthei wants to merge 1 commit into
AdrienChampion:masterfrom
elefthei:feat/arena-hashconsing
Open

feat(arena): arena-backed hashconsing behind bumpalo feature (2.16x)#33
elefthei wants to merge 1 commit into
AdrienChampion:masterfrom
elefthei:feat/arena-hashconsing

Conversation

@elefthei

@elefthei elefthei commented Aug 5, 2026

Copy link
Copy Markdown

Measurements

cargo bench --features bumpalo --bench arena_vs_arc, 200k instructions, min of 5 reps, same generated term DAG replayed through both consigns:

phase Arc (HConsed) Arena (BHConsed) speedup
build 30.989 ms 18.200 ms 1.70x
re-intern 22.953 ms 11.956 ms 1.92x
traverse 15.506 ms 6.697 ms 2.32x
teardown 10.282 ms 0.002 ms 5411.73x
total 79.730 ms 36.855 ms 2.16x

The bench is a target in this PR, not a one-off: it is harness = false, required-features = ["bumpalo"], and asserts the wins (total ≥ 2.0x, traverse ≥ 1.5x, teardown ≥ 3.0x), so it exits non-zero if the arena path regresses. Numbers above are from one machine and are not the pass criterion — the exit code is.

Only traverse and teardown carry thresholds, because those wins are structural: traversal pushes/pops/inserts handles, which is several uncontended atomic RMWs per node on the Arc side versus a 16-byte copy on the arena side; teardown frees N Arc allocations plus N owned table keys versus dropping one POD HashMap and a few bump chunks. build and re-intern ratios are printed but not asserted — real, but smaller and more machine-dependent.

Summary

Adds BHConsed/BHConsign, an arena-backed counterpart to HConsed/HConsign, behind a new optional bumpalo feature. Values live in a caller-owned bumpalo::Bump; handles are Copy (&'bump T + u64 uid) instead of refcounted. The existing Arc path is untouched — this is an addition, not a change.

Two wins beyond raw speed:

  • the consign keys its table on the interned reference itself, so it needs no T: Clone bound (the Arc consign clones every element to use as its key);
  • cloning a term is a register copy, not an atomic increment.

Changes

  • src/arena.rs — new module, compiled only under feature = "bumpalo". BHConsed<'bump, T> mirrors HConsed's impls (PartialEq/Eq/Ord/Hash on uid only, Deref, Borrow, Debug, Display) with Clone/Copy hand-written to avoid spurious T: Clone/T: Copy bounds. BHConsign<'bump, T, S> provides new/with_capacity/with_hasher/with_capacity_and_hasher, mk/mk_is_new, contains, len/is_empty/capacity, iter/consed_iter, arena, reserve/shrink_to_fit. Re-exports bumpalo so callers need not pin a matching version.
  • Cargo.toml — optional bumpalo = "^3.16" + bumpalo = ["dep:bumpalo"] feature, added to the unstable_docrs aggregate so the module renders on docs.rs. rust-version untouched. New [[bench]] arena_vs_arc.
  • src/lib.rs#[cfg(feature = "bumpalo")] pub mod arena; plus re-exports, and a short crate-doc section. Mentions are plain backticks, not intra-doc links, so cargo doc stays clean with the feature off.
  • src/test/arena.rs, tests/send_sync.rs — unit tests and a rayon test.
  • benches/arena_vs_arc.rs — the bench above.

Caveats

Destructors never run. Values are bump-allocated, so any heap owned by T (String, Vec<_>, …) is leaked until the arena drops. This is forced rather than incidental: handles are Copy and carry 'bump, so no owner could drop a value earlier without leaving handles dangling. Documented prominently in the module header, with the recommendation to use arena-friendly payloads (&'bump str, &'bump [T] allocated via BHConsign::arena) or to stay on the Arc path when destructors matter.

Other differences from the Arc path, all documented:

  • no weak refs, hence no collect/collect_to_fit — memory returns when the Bump drops or resets;
  • no consign! analogue: Bump is Send but not Sync, so a lazy_static arena consign is impossible. BHConsign is !Send; individual BHConsed handles are Send + Sync when T: Sync;
  • hash_coll::HConSet/HConMap stay HConsed-only. BHConsed::hash writes exactly one u64, so HashSet<Term, hash_coll::hashers::p_hash::Builder> works directly and is already fast. Generalizing those collections over HashConsed is a crate-wide refactor and deliberately out of scope here.

Verification

command result
cargo test --workspace pass, no warnings (feature off)
cargo test --workspace --all-features 5 lib + 2 integration + 11 doc, all pass
cargo clippy --workspace --all-features 0 warnings (note: #![deny(warnings)])
cargo doc --workspace --all-features / cargo doc --workspace both clean
cargo fmt --check clean
cargo bench --features bumpalo --bench arena_vs_arc exit 0

Two module doctests double as the user-facing correctness proof: the lambda-calculus one interns Var(3) twice and asserts factory.len() == 3, v2.uid() == v3.uid() and std::ptr::eq(v2.get(), v3.get()), i.e. perfect sharing with each distinct term allocated exactly once. Note its term type deliberately does not derive Clone.

benches/traversals.rs is #![feature(test)] and still nightly-only; the new bench is harness = false and selected explicitly, so nothing here requires nightly and default target selection is unaffected.

Adds `BHConsed`/`BHConsign`, the arena counterpart of `HConsed`/`HConsign`,
in a new `arena` module gated on the optional `bumpalo` feature. The existing
`Arc` path is untouched.

Values are bump-allocated in a caller-owned `bumpalo::Bump`, so handles are
`Copy` (`&'bump T` + `u64` uid) rather than refcounted, and the consign keys
its table on the interned reference itself, dropping the `T: Clone` bound the
`Arc` consign needs. The trade-off is documented prominently: `Bump` never
runs destructors, and there are no weak refs, hence no `collect` and no
`consign!` analogue (`Bump` is `Send` but not `Sync`).

Verified by unit tests (`src/test/arena.rs`), two module doctests, a rayon
integration test proving handles are `Send + Sync`, and a new
`arena_vs_arc` bench that replays one generated term DAG through both
consigns and fails unless the arena wins. Measured, 200k instructions:

  phase        Arc (HConsed)  Arena (BHConsed)   speedup
  build            30.989 ms         18.200 ms     1.70x
  re-intern        22.953 ms         11.956 ms     1.92x
  traverse         15.506 ms          6.697 ms     2.32x
  teardown         10.282 ms          0.002 ms  5411.73x
  total            79.730 ms         36.855 ms     2.16x

Assistant-model: Claude Opus
@lenianiva

Copy link
Copy Markdown
Collaborator

Is there a reason that htis uses bumpalo rather than some other arenas in https://donsz.nl/blog/arenas/? For example, https://crates.io/crates/concurrent_arena is concurrent and can reuse memory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants