Skip to content

pepyakin/amaco2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

amaco2 — Coroutine-Prefetch Pointer-Chase Benchmark

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).

Variants

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

Results

Platforms

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)

Summary: Scalar vs Best Async+Prefetch

All numbers are median ns/lookup over 5 measured runs, 500k queries, depth=12.

Apple M1 Max (AArch64)

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

AMD EPYC 4585PX (x86-64)

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

Throughput vs Working Set Size

Apple M1 Max (AArch64)

Throughput vs Working Set Size — M1

AMD EPYC 4585PX (x86-64)

Throughput vs Working Set Size — x86

Variant Isolation

Demonstrates that the gain comes from the combination of interleaving + prefetch, not from either alone.

Apple M1 Max (AArch64)

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.

AMD EPYC 4585PX (x86-64)

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.

Group-Size Sweep (Async+Prefetch)

Speedup over scalar baseline at each group size. Median of 5 runs.

Apple M1 Max (AArch64)

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

AMD EPYC 4585PX (x86-64)

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

Apple M1 Max (AArch64)

Speedup vs Group Size — M1

AMD EPYC 4585PX (x86-64)

Speedup vs Group Size — x86

Observations

Common to both platforms

  • 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.

Apple M1 Max (AArch64)

  • 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 prfm instruction is doing real work.
  • Peak speedup: 7.9x at 512 MB and 4 GB.

AMD EPYC 4585PX (x86-64)

  • 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.

Platform Notes

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.

Quick Start

# 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

CPU Pinning (recommended on Linux)

# 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.csv

On macOS (Apple Silicon), the OS handles core placement; no pinning is needed.

CLI Flags

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

Plotting

pip install matplotlib pandas
python3 scripts/plot.py results.csv

Produces (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

CSV Format

variant,workload_size_mb,depth,group_size,hint,run_id,ns_total,ns_per_lookup,lookups_per_sec,checksum

Architecture

  • src/node.rs — 64-byte cache-line-aligned node, random chain construction
  • src/prefetch.rs — Prefetch inline assembly (prfm on AArch64, prefetcht0 on x86-64)
  • src/executor.rs — Zero-alloc round-robin single-threaded future executor
  • src/variants.rs — Four benchmark variant implementations
  • src/main.rs — CLI, measurement harness, CSV output

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors