Demonstrates that a single-threaded custom Rust future executor with explicit
software prefetch (prfm on AArch64, prefetcht0 on x86-64) + cooperative
yield can dramatically improve throughput for pointer-chasing lookups that miss
LLC, compared with a synchronous loop.
Based on the interleaved coroutine execution idea from Interleaving with Coroutines (VLDB '17).
| Variant | Description |
|---|---|
scalar |
Plain synchronous pointer chase (baseline) |
scalar-prefetch |
Scalar + prefetch for next node (no delay between prefetch and use) |
async-no-prefetch |
Custom executor with yield interleaving, no prefetch (isolates scheduling overhead) |
async-prefetch |
Main experiment: prefetch before yield, dereference after resume |
| Apple M1 Max | AMD EPYC 4585PX | |
|---|---|---|
| Architecture | AArch64 | x86-64 |
| CPU | Apple M1 Max (10-core) | AMD EPYC 4585PX (16-core / 32-thread) |
| LLC | ~48 MB shared L2 (no L3) | 128 MB L3 (2×64 MB) |
| Memory | 64 GB unified LPDDR5 | 128 GB DDR5 |
| OS | macOS 15.6.1 | Ubuntu 24.04 (kernel 6.8.0) |
| Rust | 1.94.0-nightly (2f1bd3f37) | 1.93.0 (254b59607) |
| Build | RUSTFLAGS="-C target-cpu=native" cargo build --release |
same |
| CPU pinning | None (macOS has no taskset) |
None (unpinned, to match M1 methodology) |
All numbers are median ns/lookup over 5 measured runs, 500k queries, depth=12.
| Working Set | Scalar (ns/lookup) | Async+Prefetch (ns/lookup) | Best Group Size | Speedup |
|---|---|---|---|---|
| 8 MB | 69 | 51 | 16 | 1.4x |
| 32 MB | 208 | 82 | 16 | 2.5x |
| 128 MB | 505 | 76 | 64 | 6.6x |
| 512 MB | 620 | 78 | 48 | 7.9x |
| 2 GB | 763 | 101 | 64 | 7.5x |
| 4 GB | 822 | 104 | 64 | 7.9x |
| Working Set | Scalar (ns/lookup) | Async+Prefetch (ns/lookup) | Best Group Size | Speedup |
|---|---|---|---|---|
| 8 MB | 35 | 27 | 32 | 1.3x |
| 32 MB | 78 | 40 | 128 | 2.0x |
| 128 MB | 251 | 50 | 192 | 5.0x |
| 512 MB | 377 | 56 | 192 | 6.8x |
| 2 GB | 429 | 57 | 192 | 7.5x |
| 4 GB | 445 | 62 | 128 | 7.2x |
Demonstrates that the gain comes from the combination of interleaving + prefetch, not from either alone.
8 MB (cache-resident):
| Variant | ns/lookup | vs Scalar |
|---|---|---|
| scalar | 69 | 1.00x |
| scalar+prefetch | 70 | 1.00x |
| async-no-prefetch (g=8) | 53 | 1.31x |
| async+prefetch (g=16) | 51 | 1.36x |
512 MB (memory-bound):
| Variant | ns/lookup | vs Scalar |
|---|---|---|
| scalar | 620 | 1.00x |
| scalar+prefetch | 695 | 0.89x |
| async-no-prefetch (g=12) | 202 | 3.07x |
| async+prefetch (g=48) | 78 | 7.93x |
At 512 MB, scalar+prefetch hurts (the prefetched line is evicted before use). Interleaving alone gives 3x. Adding prefetch to interleaving pushes it to 8x.
8 MB (cache-resident):
| Variant | ns/lookup | vs Scalar |
|---|---|---|
| scalar | 35 | 1.00x |
| scalar+prefetch | 48 | 0.73x |
| async-no-prefetch (g=256) | 26 | 1.33x |
| async+prefetch (g=32) | 27 | 1.31x |
512 MB (memory-bound):
| Variant | ns/lookup | vs Scalar |
|---|---|---|
| scalar | 377 | 1.00x |
| scalar+prefetch | 524 | 0.72x |
| async-no-prefetch (g=128) | 154 | 2.45x |
| async+prefetch (g=192) | 56 | 6.76x |
On x86-64, scalar+prefetch is even more harmful than on AArch64 (0.72x vs 0.89x). Interleaving alone gives 2.5x; adding prefetch pushes it to 6.8x.
Speedup over scalar baseline at each group size. Median of 5 runs.
| g | 128 MB | 512 MB | 2 GB | 4 GB |
|---|---|---|---|---|
| 1 | 0.6x | 0.6x | 0.7x | 0.6x |
| 2 | 0.8x | 0.8x | 0.8x | 0.8x |
| 4 | 1.5x | 1.5x | 1.6x | 1.6x |
| 8 | 2.5x | 2.7x | 2.9x | 3.0x |
| 12 | 3.4x | 3.7x | 4.0x | 4.1x |
| 16 | 4.2x | 4.4x | 4.8x | 5.1x |
| 24 | 5.0x | 5.4x | 6.0x | 6.3x |
| 32 | 5.7x | 6.6x | 7.0x | 7.0x |
| 48 | 6.6x | 7.9x | 7.4x | 7.9x |
| 64 | 6.6x | 7.8x | 7.5x | 7.9x |
| g | 128 MB | 512 MB | 2 GB | 4 GB |
|---|---|---|---|---|
| 1 | 0.4x | 0.5x | 0.5x | 0.5x |
| 2 | 0.6x | 0.6x | 0.6x | 0.6x |
| 4 | 1.0x | 1.0x | 1.0x | 1.1x |
| 6 | 1.3x | 1.4x | 1.5x | 1.5x |
| 8 | 1.6x | 1.8x | 1.9x | 2.0x |
| 12 | 2.0x | 2.5x | 2.7x | 2.7x |
| 16 | 2.5x | 3.1x | 3.4x | 3.3x |
| 24 | 3.1x | 4.0x | 4.4x | 4.4x |
| 32 | 3.7x | 4.8x | 5.2x | 5.1x |
| 48 | 4.4x | 5.8x | 6.3x | 6.0x |
| 64 | 4.7x | 6.1x | 6.9x | 6.6x |
| 96 | 5.0x | 6.5x | 7.1x | 6.8x |
| 128 | 5.0x | 6.7x | 7.1x | 7.2x |
| 192 | 5.0x | 6.8x | 7.5x | 7.0x |
| 256 | 4.9x | 6.6x | 7.1x | 6.8x |
- g=1 is pure overhead: the executor's
Pin<Box<>>allocation and poll machinery costs ~0.5–0.6x vs scalar. This confirms the runtime isn't free. - Scalar+prefetch hurts at large sizes: with no interleaving delay between prefetch and use, the prefetched line is evicted before consumption. This validates the coroutine approach — you need the yield gap.
- Breakeven at g≈3: interleaving begins to pay for itself once 3+ lookups overlap, hiding enough memory latency to offset executor cost.
- Saturation at g≈32–48: performance plateaus, likely reflecting the core's maximum memory-level parallelism (outstanding cache-line requests the load/store unit can track). This is higher than the paper's predicted 4–12 sweet spot, possibly because Apple's unified memory has higher latency than typical DDR setups.
- Prefetch adds ~2.5x on top of interleaving alone at large sizes
(async-no-prefetch gives ~3x, async+prefetch gives ~8x), confirming the
prfminstruction is doing real work. - Peak speedup: 7.9x at 512 MB and 4 GB.
- Breakeven at g≈4: slightly later than M1, consistent with lower baseline memory latency (scalar is 2–3x faster than M1 at equivalent sizes).
- Saturation at g≈128–192: the EPYC needs significantly more in-flight tasks than M1 to saturate. Performance peaks at g=192 and regresses slightly beyond g=256, likely from executor overhead and cache pollution in the larger task ring.
- Prefetch adds ~2.8x on top of interleaving alone at 512 MB (async-no-prefetch gives ~2.5x, async+prefetch gives ~6.8x).
- Peak speedup: 7.5x at 2 GB (g=192) — comparable to M1's 7.9x. Both platforms were tested unpinned (macOS does not support CPU pinning). The remaining gap is consistent with a ratio effect: EPYC's scalar baseline is ~1.9x lower (429 ns vs 822 ns at the respective peak sizes), but its async+prefetch floor is only ~1.8x lower (57 ns vs 104 ns).
- 128 MB crosses the L3 boundary on the EPYC (128 MB L3), so the speedup at that size (5.0x) is closer to M1's 6.6x than earlier pinned results suggested.
Apple M1 Max:
- No CPU pinning available on macOS; the OS scheduler may migrate threads between performance and efficiency cores.
- Apple Silicon uses unified memory (no separate DRAM bus), so memory latency characteristics differ from typical x86 NUMA systems.
- No hugepage support on macOS. TLB pressure at 4 GB is handled by the hardware's large TLB and multi-level page walker.
AMD EPYC 4585PX:
- Run unpinned to match M1 methodology. Pinned runs (with
taskset -c 0) show ~45% lower scalar latency at 512 MB due to eliminated migration overhead, but async+prefetch times are nearly identical either way. - 128 MB L3 (2×64 MB CCDs). The 128 MB working set straddles the cache boundary, producing meaningful speedups.
- Standard 4K pages; transparent hugepages may be active but were not explicitly configured.
# Build with native CPU tuning
RUSTFLAGS="-C target-cpu=native" cargo build --release
# Run all variants across multiple working-set sizes
./target/release/amaco2 --output results.csv
# Run with custom parameters
./target/release/amaco2 \
--size-mb 8,32,128,512 \
--depth 12 \
--group-size 1,2,4,6,8,12,16 \
--num-queries 1000000 \
--runs 10 \
--warmup 2 \
--output results.csv# Pin to a specific performance core and set governor
sudo cpupower -c 0 frequency-set -g performance
taskset -c 0 ./target/release/amaco2 --output results.csvOn macOS (Apple Silicon), the OS handles core placement; no pinning is needed.
| Flag | Default | Description |
|---|---|---|
--variant |
all |
scalar, scalar-prefetch, async-no-prefetch, async-prefetch, or all |
--size-mb |
8,32,128,512 |
Working-set sizes in MB (comma-separated) |
--depth |
12 |
Chain depth (hops per lookup) |
--group-size |
1,2,4,6,8,12,16 |
Concurrent task slots for async variants |
--hint |
pldl1keep |
Prefetch hint: pldl1keep, pldl2keep, pldl1strm |
--runs |
10 |
Measured runs per configuration |
--warmup |
2 |
Warmup runs (discarded) |
--num-queries |
1000000 |
Number of independent pointer-chase lookups |
--output |
stdout | CSV output file path |
pip install matplotlib pandas
python3 scripts/plot.py results.csvProduces (in current directory):
throughput_vs_size.png— throughput vs working-set size (best group-size per variant)speedup_vs_groupsize.png— speedup of async+prefetch over scalar baseline vs group size
variant,workload_size_mb,depth,group_size,hint,run_id,ns_total,ns_per_lookup,lookups_per_sec,checksum
src/node.rs— 64-byte cache-line-aligned node, random chain constructionsrc/prefetch.rs— Prefetch inline assembly (prfmon AArch64,prefetcht0on x86-64)src/executor.rs— Zero-alloc round-robin single-threaded future executorsrc/variants.rs— Four benchmark variant implementationssrc/main.rs— CLI, measurement harness, CSV output



