ThreadStone

A CPU benchmark suite that shows its work.

Apple M4 Pro · 10P + 4E · macOS 26.6

Single-core
2216
vs. Reference Core = 1000
Multi-core
19373
14 threads
Workloads
6
each measuring something distinct
Stable results
6/6
run-to-run variation under 3%

Results

Six workloads, each measuring something the others cannot see. A CPU that is fast at all six is fast; one that is fast at a single one is fast at that one thing. The per-workload numbers are the actual result — the score is a convenience that throws away the shape.

Workload1 thread14 threadsScalingVariation
Dhrystone 2.1
Dhry/s
68.0M680M10.0×1.9% acceptable
SGEMM 256³
GFLOP/s
19.11869.7×2.8% acceptable
SHA-256
MiB/s
4223.75k8.9×2.7% acceptable
Sort 1Mi u64
Melem/s
1021.06k10.4×2.2% acceptable
STREAM Triad
GiB/s
1102141.9×1.1% acceptable
Memory Latency
ns
1180.9% stable

Variation is the coefficient of variation across 9 samples, after discarding 3 warmup rounds and rejecting outliers by median absolute deviation. Under 1% is stable; under 3% is usable; above that, the measurement is describing the room rather than the CPU.

What the extra cores buy

Speedup from one thread to 14. Compute-bound workloads approach the core count; STREAM does not, and that is the interesting result — a single core already saturates most of this machine's memory controller, so there is little headroom left for the others.

Speedup from 1 to 14 threadsSpeedup factor per workload, with perfect linear scaling marked as a reference line.05101520Dhrystone 2.1Dhrystone 2.1: 10.0×10.0×SGEMM 256³SGEMM 256³: 9.7×9.7×SHA-256SHA-256: 8.9×8.9×Sort 1Mi u64Sort 1Mi u64: 10.4×10.4×STREAM TriadSTREAM Triad: 1.9×1.9×Memory Latencysingle-thread onlyperfect scaling (14×)Speedup vs. 1 thread
Memory latency is absent because it is measured single-threaded only — see below.

Against the reference core

Each workload's single-thread result as a multiple of the ThreadStone Reference Core v1 — a nominal 3.0 GHz out-of-order core with 256-bit SIMD and one DDR4-3200 channel. It is a published definition, not a machine anyone owns: a reference derived from the author's own hardware would score exactly 1000 and make every other machine look like a deviation from it.

Single-thread performance relative to the reference coreEach workload's ratio to its published reference value.02.557.510Dhrystone 2.1Dhrystone 2.1: 3.1×3.1×SGEMM 256³SGEMM 256³: 1.6×1.6×SHA-256SHA-256: 1.7×1.7×Sort 1Mi u64Sort 1Mi u64: 2.0×2.0×STREAM TriadSTREAM Triad: 9.2×9.2×Memory LatencyMemory Latency: 0.8×0.8×referenceMultiple of the reference core
The score is the geometric mean of these ratios, ×1000. The geometric mean is used because the arithmetic mean of ratios depends on which machine sits in the denominator — so A could beat B under one reference and lose under another.

The memory hierarchy, measured

One dependent load at a time, over working sets from 4 KiB to 256 MiB. Nothing can hide the miss, so each plateau is a cache level and each step is a boundary. This is the shape of the machine's memory system.

Memory latency against working-set sizeLatency per dependent load, measured by pointer chasing over working sets from 4 KiB to 256 MiB. Plateaus mark cache levels.0.51251020501002004 KiB16 KiB64 KiB256 KiB1 MiB4 MiB16 MiB64 MiB256 MiBL1d 64 KiBL2 16 MiB4 KiB: 0.9 ns8 KiB: 1.0 ns16 KiB: 1.0 ns32 KiB: 1.0 ns64 KiB: 0.9 ns128 KiB: 0.9 ns256 KiB: 6.0 ns512 KiB: 6.4 ns1 MiB: 6.2 ns2 MiB: 6.2 ns4 MiB: 7.8 ns8 MiB: 8.1 ns16 MiB: 15.6 ns32 MiB: 51.1 ns64 MiB: 98.4 ns128 MiB: 111.1 ns256 MiB: 119.2 ns0.9 ns119 nsWorking setNanoseconds per access (log)
Latency per access against working-set size, log scale. Vertical lines mark this machine's reported cache capacities. The rise from 0.9 ns to 119 ns is the whole cost of missing every level of cache.
Table view
Working setLatency (ns)
4 KiB0.9
8 KiB1.0
16 KiB1.0
32 KiB1.0
64 KiB0.9
128 KiB0.9
256 KiB6.0
512 KiB6.4
1 MiB6.2
2 MiB6.2
4 MiB7.8
8 MiB8.1
16 MiB15.6
32 MiB51.1
64 MiB98.4
128 MiB111.1
256 MiB119.2

The six workloads

Each covers a dimension of CPU performance the others are blind to.

Dhrystone 2.1

68.0M Dhry/s
single thread · 680M Dhry/s at 14 threads

The 1984 integer benchmark, ported to Rust and verified against the reference implementation's published final state. Small integers, dense branching, procedure calls, and 30-byte string copies — a working set that never leaves L1.

SGEMM 256³

19.1 GFLOP/s
single thread · 186 GFLOP/s at 14 threads

Dense f64 matrix multiply, cache-blocked so three 256×256 matrices sit in L2. Loop order is i-k-j, which makes the inner loop a contiguous AXPY that vectorises into FMAs instead of stalling on a dot-product accumulator.

SHA-256

422 MiB/s
single thread · 3.75k MiB/s at 14 threads

The portable software path, no hardware SHA instructions — those are an order of magnitude faster and would measure the presence of one instruction rather than integer throughput. Verified against the NIST vectors.

Sort 1Mi u64

102 Melem/s
single thread · 1.06k Melem/s at 14 threads

The most realistic workload here. Unpredictable branches at every comparison and a recursive access pattern no prefetcher models — what ordinary application code actually does to a CPU.

STREAM Triad

110 GiB/s
single thread · 214 GiB/s at 14 threads

McCalpin's Triad over three 64 MiB arrays, partitioned across threads so the footprint stays constant at every thread count. Counts 24 bytes per element, following STREAM's convention of ignoring read-for-ownership traffic.

Memory Latency

118 ns
single thread only

A dependent-load pointer chase around one random Hamiltonian cycle: each step's address is the previous step's value, so nothing can hide the miss. Measured single-threaded only — splitting the buffer across threads would fit each slice in cache and report an LLC hit as DRAM latency.

Why these numbers can be trusted

Every design decision follows from one idea: a benchmark number is a claim, and a claim nobody can check is worthless.

Threads start together

A work-stealing pool hands out samples as slots free up, so early threads run against an idle machine and late ones against a loaded one. ThreadStone releases every thread from a barrier, so the window is exactly “time for all N threads, having started at the same instant.”

Iteration counts are calibrated

A fixed count that fills 300 ms on a laptop fills 3 ms on a server — close enough to the clock's granularity to be noise. Counts are discovered at run time, with every thread active, because a count tuned on an idle machine overshoots once memory is contended.

Nothing unmeasured is in the window

Allocation and page-faulting happen before the clock starts. Threads are spawned once for the whole workload, not once per sample.

Every number carries its uncertainty

Median rather than mean, because benchmark noise is one-sided — only ever making a sample slower. Outliers are rejected by median absolute deviation and counted, and each result states whether it should be believed.

Every number carries its provenance

CPU topology, cache sizes, OS, compiler version, target triple, optimisation flags, and the measured resolution of the clock itself. “2,300 Dhrystones/sec” is unfalsifiable; the same number with its machine attached is a claim you can refute.

What can't be measured well isn't reported

Memory latency is single-thread only. Splitting a 256 MiB chase buffer across 14 threads would give each a slice that fits in last-level cache, so the “multi-threaded latency” would be an LLC hit time — several times better than reality, and a straightforward lie about the machine.

Full methodology, including the reference values, the statistics, and a known-limitations section.

Run it yourself

git clone https://github.com/romankhadka/ThreadStone
cd threadstone
cargo install --path threadstone-cli --locked

threadstone run                  # the full suite, both passes
threadstone sweep                # map your cache hierarchy
threadstone run --out mine.json  # save the full document
threadstone compare theirs.json mine.json

Requires Rust 1.75 or newer. No C toolchain; three third-party crates in the binary. --locked matters: a benchmark built against different dependency versions is not the same benchmark, so Cargo.lock is committed and every install measures with the same code.