32-bit MCU to 64-bit server Pure Rust • no_std MIT OR Apache-2.0

Judy arrays in pure Rust, rebuilt for modern hardware

Sparse, ordered maps and sets with adaptive density — memory follows the key ranges you populate, never pre-sized tables or fixed buckets.

Sorted iteration, range scans and rank over integers, strings and byte slices — with cache-line-aligned nodes, SIMD/SWAR search and optimistic reader concurrency. One engine from a 32-bit MCU to a server, reachable from nine languages over a stable C ABI.

0.67 B/key
clustered timestamps, 32-bit
9 languages
over one C ABI
no_std
32-bit and 64-bit targets
optimistic
OCC reader concurrency

Partitioning by key expanse, rather than population

Comparison trees (B-trees, red-black trees) divide nodes by key population count. Judy digital trees divide uniformly by key digit ranges — an architectural invariant where memory scales strictly with populated density rather than table sizing.

“Expanse, population, and density are not commonly used terms in tree search literature, so let’s define them here: Expanse is a range of possible keys. Population is the number of keys actually stored in that expanse. Density is the population divided by the expanse.”
— Doug Baskins, A 10-Minute Description of How Judy Arrays Work (2002) • Alan Silverstein, Judy IV Shop Manual
Expanse: The numerical key span covered by a node (from 264 at the tree root down to 256 for a single 1-byte level).
Population: The count of keys present in that span. Subtrees with population ≤ 15 pack immediately with 0 heap bytes.
Density (pop / expanse): Governs adaptive compression — nodes reshape automatically between Linear, Bitmap, and Uncompressed forms.
Clustered sensor timestamps (10k keys)
0.67 B/key
Dense subexpanse: Level 1 bitset with 0 B pointer overhead (ExpanseSet32)
IPv4 /24 subnet routing (2k keys)
9.38 B/key
Clustered subnet: LeafBitmapL with packed 4B value slots (ExpanseMap32)
Sparse CAN-bus 29-bit IDs (500 keys)
12.61 B/key
Sparse expanse: 8-byte Edge32 with zero-span bypass (ExpanseSet32)
Uniform-random 64-bit (1M keys)
16.70 B/key
Worst-case dispersion: deep uncompressed branches (ExpanseMap)

Is this the right structure for you?

Two lists answer it in ten seconds, naming the alternatives by name.

Reach for Expanse when

  • Your keys are clustered or sequential — timestamps, IDs, addresses, offsets.
  • Memory is the binding constraint, not raw lookup throughput.
  • You need ordered iteration, range scans or rank — not just point lookup.
  • You are on a 32-bit MCU and a hash table's load factor costs too much SRAM.
  • You want one engine across nine languages rather than a per-language reimplementation.

Use something else when

  • Uniform-random point lookup is your hot path — hashbrown wins that, and random keys are a trie’s worst case (16.70 B/key against 0.67 for clustered).
  • You never iterate in order — HashMap is simpler and faster, and ordering is most of what you are paying for here.
  • You need a concurrent writer workload; on a 50/50 mix every single-writer arm loses throughput as threads are added (0.12×–0.55×). DashMap wins that regime.
  • Your keys are long, high-entropy strings with no shared prefixes — the trie's structure buys you nothing.

Architectural Highlights

Engineered for cache-line density, hardware SIMD lanes, and optimistic multi-core read throughput.

Zero-Alloc Immediates

Up to 7 keys in sets and up to 3 key-value pairs in maps are packed directly inside tagged 64-bit edge words, bypassing heap allocation entirely for small collections.

Adaptive Compression Ladder

Trie branches dynamically morph between Linear leaves (sorted key arrays), Bitmap leaves (64-bit subexpanse bitboards), and full uncompressed 256-way digital branches.

🚀

Lock-Free OCC Concurrency

SyncExpanseMap and SyncExpanseSet employ epoch-based optimistic concurrency control (OCC). Readers perform optimistic validated traversals with zero reader-lock cache-line bouncing.

🎯

SIMD & SWAR Acceleration

Search kernels utilize vector instructions (AVX2, AVX-512, ARM NEON) with bitwise SWAR fallbacks, scanning linear leaves in single clock cycles.

📦

glibc-hwcaps Multi-Arch

Debian and RPM packages provide optimized runtime libraries automatically selected by the dynamic loader for x86-64-v2, v3 (AVX2), and v4 (AVX-512).

🔗

Drop-in Judy C ABI Parity

Provides 100% C ABI compatibility with stock libjudy (Judy1, JudyL, JudySL, JudyHS) alongside modern, type-safe expanse_* C interfaces.

🌱

32-Bit Embedded (#![no_std])

Compact 8-byte Edge32 layout saving 50% structural SRAM on ARM Cortex-M and RISC-V RV32 microcontrollers, with 32-byte cache alignment and zero-alloc inlined payloads.

🗃

Database Engine Subsystems

MVCC visibility scans, string interning dictionaries, and an ExpanseBlobMap slab arena for variable-length values — plus a RocksDB MemTable plugin.

Explore the Interactive Data Visualizer

Inspect tagged pointer layouts, simulate dynamic compression transitions across the ladder, and step through branch bitboard operations in real-time.

Launch Visualizer →

Benchmarks

The same three charts already on the page — given titles, a one-line read, and provenance a reader can resolve.

VS STD & THIRD-PARTY COLLECTIONS
Insert, random lookup and clustered lookup at 100k–1M keys. Clustered lookup is the win; random lookup is a measured loss to HashSet and is published as such.
harness compare.rs
commit 695b98d
ExpanseSet vs std collections -- insert, random lookup, clustered lookup Cold-Build Insert 100,000 clustered keys, cold build (higher is better) ▲ Throughput (M keys / sec) 100M 50M 0 96.2 M/s ExpanseSet 1.74x faster 55.2 M/s HashSet SwissTable 21.8 M/s BTreeSet std ordered Point Lookup: Random 1,000,000 keys, hit (lower is better) ▼ Latency (ns / op) 120 ns 60 ns 0 36.5 ns ExpanseSet 3.07x slower 11.9 ns HashSet SwissTable 104.5 ns BTreeSet std ordered Point Lookup: Clustered 1,000,000 keys, hit (lower is better) ▼ Latency (ns / op) 120 ns 60 ns 0 8.2 ns ExpanseSet 1.47x faster 12.0 ns HashSet SwissTable 101.6 ns BTreeSet std ordered Measured: reference host -- Intel i9-12900F, 24 threads, 30 MiB L3, Ubuntu 22.04 / kernel 6.8 · commit 695b98d · benches/compare.rs + benches/comparative.rs, criterion medians Source: docs/BENCHMARKING.md, section 'vs stdlib & 3rd-party collections'. Random-key lookup is the engine's measured weak arm and is published as a loss.
CONCURRENT READ SCALING
Sync* OCC arms against DashMap, SkipMap and lock-based baselines at 100% read, with the 50/50 read/write mix below. Two runs of one commit, side by side.
harness concurrency.rs
CI runs 34881026495, 34882381735
OCC concurrency: 100% read at 16 threads MULTITHREADED OCC CONCURRENCY · 100% READ Read throughput at 16 threads · M ops/sec · higher is better · bounded 2xPOP keyspace (#375) Expanse OCC arm Baseline Bars are 16-thread throughput on a zero-based linear axis; the badge is that arm's own 1→16-thread scaling. Two runs of one commit: bars are run 1, labels and badges read run 1 / run 2. Compare arms only within the key type under each name. SyncExpanseSet 562.0 / 414.9 M ops/s 7.36x / 5.44x u64 keys, 1M draws SyncExpanseMap 361.0 / 360.5 M ops/s 9.44x / 9.44x u64 keys, 1M draws SyncExpanseBlobMap 305.8 / 306.7 M ops/s 9.86x / 9.87x u64 -> 128-byte payload, 200k SkipMap<u64, Vec<u8>> 38.3 / 38.4 M ops/s 11.05x / 11.10x u64 -> 128-byte payload, 200k RwLock<BTreeMap<u64, ...>> 15.3 / 16.0 M ops/s 1.45x / 1.52x u64 -> 128-byte payload, 200k SyncExpanseBytesMap 126.4 / 125.6 M ops/s 11.08x / 11.03x string keys, 100k SyncExpanseStrMap 73.3 / 75.6 M ops/s 10.63x / 10.99x string keys, 100k DashMap<Vec<u8>, u64> 129.9 / 129.4 M ops/s 8.46x / 8.39x string keys, 100k At 50% read / 50% write — 1→16 threads, read + write ops/s (a mixed-operation rate), then that arm's scaling in run 1 / run 2: SyncExpanseMap [u64 keys, 1M draws]: 28.0 → 40.9 → 62.5 M ops/s (run 1) · 2.23x / 2.23x SyncExpanseSet [u64 keys, 1M draws]: 42.9 → 44.7 → 82.9 M ops/s (run 1) · 1.93x / 1.98x SyncExpanseBlobMap [u64 -> 128-byte payload, 200k]: 27.2 → 9.5 → 7.3 M ops/s (run 1) · 0.27x / 0.27x SkipMap<u64, Vec<u8>> [u64 -> 128-byte payload, 200k]: 2.0 → 6.6 → 17.5 M ops/s (run 1) · 8.54x / 8.54x SyncExpanseBytesMap [string keys, 100k]: 4.8 → 3.6 → 3.1 M ops/s (run 1) · 0.65x / 0.65x SyncExpanseStrMap [string keys, 100k]: 6.2 → 5.3 → 4.1 M ops/s (run 1) · 0.65x / 0.65x DashMap<Vec<u8>, u64> [string keys, 100k]: 10.7 → 33.0 → 83.6 M ops/s (run 1) · 7.80x / 7.77x Measured: reference host -- 12th Gen Intel(R) Core(TM) i9-12900F, pin 0-15 · runs 34881026495 and 34882381735, ref 76432c5c threads 1,4,16, 18 interleaved rounds of 500 ms windows, largest foreign busy CPUs over a group 0.04 across both runs
YCSB KEY-VALUE WORKLOADS
Workloads A, B, C, D, F. Workload E (short range scans) is a measured loss to BTreeMap with symmetric predicates.
harness ycsb.rs
commit 43b46f38
YCSB workloads A-F throughput YCSB WORKLOADS A–F THROUGHPUT Mops/s · higher is better · N = 100,000, Zipfian theta = 0.99, 128B blobs, seed 0x1234_5678_9ABC, criterion median throughput ExpanseMap (u64) ExpanseBlobMap (128B) BTreeMap (128B) SkipMap (128B) Workloads A–D and F share one zero-based axis; Workload E has its own, an order of magnitude lower. Workload A 50% Read / 50% Update 20.49 20.23 4.25 1.90 Expanse 4.82x Workload B 95% Read / 5% Update 23.02 21.27 4.43 2.57 Expanse 5.20x Workload C 100% Read 23.47 21.41 4.43 1.94 Expanse 5.30x Workload D 95% Read-Latest / 5% Insert 22.80 21.49 4.24 1.89 Expanse 5.38x Workload F 50% Read / 50% Read-Modify-Write 18.66 14.69 4.36 1.76 Expanse 4.28x Workload E, separate axis (max 1.259 Mops/s) — Plotted on its own axis: every arm is an order of magnitude below A-D/F, and BTreeMap wins. Workload E 95% Short Range Scan / 5% Insert 0.857 0.693 1.259 0.276 BTreeMap 1.47x Measured: reference host -- Intel i9-12900F, 24 threads, 30 MiB L3, Ubuntu 22.04 / kernel 6.8, idle · run 33219093994 · benches/ycsb.rs, post-#470 payload-dereference repair The Workload E figure published under #375 is retracted; E is re-measured here and inverts.
MEMORY DENSITY ACROSS EXPANSE OCCUPANCY
Bytes per key on uniform random keys is a sawtooth in λ = N / 216, not a curve in population: keyspace width and population are one knob, and the LEAF_CAP overflow cascade sets the tooth. The same structure spans 7.6–21 B/key under density alone; the memory-budget gate samples both sides of the cascade.
example keyspace_density.rs
deterministic byte accounting
ExpanseSet bytes per key across expanse occupancy MEMORY DENSITY ACROSS EXPANSE OCCUPANCY · UNIFORM RANDOM KEYS ExpanseSet bytes/key against λ = N / 2^(w−48), the mean population of a 2-byte-prefix expanse · log λ axis · lower is better 0 4 8 12 16 20 24 B/key 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 λ (keys per 2-byte expanse) LEAF_CAP = 32 linear leaf cascades into a branch 256 × LEAF_CAP = 8192 the next byte level cascades memory-budget cell, N = 1M: 8.21 B/key (λ = 15.26) second gate cell, N = 2M: 13.74 B/key (λ = 30.52) Keyspace width (same seed) 64-bit keys · 17 cells 63-bit keys · 11 cells 62-bit keys · 10 cells 58–55-bit keys · 7 cells 9 λ values are hit by two or three widths; their cells agree within 0.05 B/key: one curve. Range under density alone: 7.08–21.02 B/key (2.97×) with no code change. Fixed-occupancy distributions clustered: 0.36–0.37 B/key, flat across N sequential: 0.06–0.07 B/key, flat across N sparse: 16.31–16.32 B/key, flat across N Measured: mem_used() deterministic byte accounting — host-independent; no wall clock; commit 66a355f9; workload example_keyspace_density. Clearing one top key bit halves the expanses and is exactly a doubling of N, so the three widths share one axis. Source: docs/ARCHITECTURE.md §3.5.

Install

Native packages and zero-cost bindings. Pick your target.

Languages
System packages
Integrations

Add core Expanse engine to your Cargo.toml:

cargo add expanse-trie

Usage example in Rust (Maps, Sets, Off-Heap Blobs, Lock-Free OCC):

use expanse_trie::{ExpanseMap, ExpanseSet, ExpanseBlobMap, SyncExpanseMap};

// 64-bit integer map with zero-allocation immediates
let mut map = ExpanseMap::new();
map.insert(42, 100);
assert_eq!(map.get(42), Some(100));

// Variable-length byte blob map with slab arena & hot metadata
let mut blobs = ExpanseBlobMap::new();
blobs.insert(1, b"hello expanse", 0x2A);
assert_eq!(blobs.get(1), Some(&b"hello expanse"[..]));

// Thread-safe optimistic OCC map (zero reader locks)
let sync_map = SyncExpanseMap::new();
sync_map.insert(99, 500);
let reader = sync_map.reader();
assert_eq!(reader.get(99), Some(500));