Skip to content

Commit 7a5c066

Browse files
authored
Merge pull request #638 from AdaWorldAPI/claude/onebrc-lane-t-trie
onebrc-probe: lane T — HHTL trie group-by + trie-vs-RAM measurement (honest negative result)
2 parents 427e63e + 6d624ca commit 7a5c066

4 files changed

Lines changed: 411 additions & 0 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# onebrc-probe — trie vs RAM-table methods and outcomes
2+
3+
> Measurement report. All numbers are **measured** on this machine, not
4+
> projected. Corpus: `/tmp/brc10m.txt`, 10,000,000 rows, seed 42,
5+
> sha256 `f1853caa30a765883aa655be1c304d956ad8b03e19b3557df2af431d9a955691`.
6+
> Metric: `throughput_mrows_s` (rows / compute-time). **Compute-only**
7+
> `main.rs` reads the file (`fs::read`, line 91) BEFORE `Instant::now()`
8+
> (line 94), so file I/O and any mmap lever are OUTSIDE the timer.
9+
> Build: `.cargo/config.toml` pins `target-cpu=x86-64-v3` (AVX2) unless a
10+
> row says `native`. 4 workers.
11+
>
12+
> **Statistical note (corrected after adversarial review).** An earlier
13+
> draft of this report headlined single **best-of-3** numbers. Three review
14+
> agents (truth-architect, overclaim-auditor, brutally-honest-tester)
15+
> correctly flagged that best-of-N reports the luckiest run and hides the
16+
> spread — at run-to-run variance of ~8–13%, that is a real reporting sin.
17+
> This version reports **median / min / max / sd over n=11** (v3) and n=7
18+
> (native) per lane. Cardinality is ~400 stations (`gen.rs STATION_COUNT`);
19+
> results are for THIS workload, THIS machine, ONE corpus — see "Scope" at
20+
> the end.
21+
>
22+
> **Lane S provenance.** Lane S (SWAR) shipped separately (PR #637, merged to
23+
> `main`); this branch is rebased on top of it, so `lane_s` and its
24+
> `lane_s_agrees_with_lane_a` parity test are present here and all lanes in the
25+
> ladder — `a c r f t8 t s` — are runnable. S is kept in the ladder because the
26+
> report's whole point is the full RAM-table-vs-trie comparison, and S is the
27+
> fastest RAM-table method.
28+
29+
## The methods (group-by-aggregate, min/max/sum/count per station)
30+
31+
Every lane runs the SAME workload and the SAME newline-aligned
32+
`chunk_bounds` split + commutative merge. What varies is (1) how a record's
33+
delimiters are found/parsed and (2) how the station identity becomes an
34+
accumulator slot — the "trie vs RAM-table" axis.
35+
36+
| Lane | Scan / parse | Group-by structure | Family |
37+
|---|---|---|---|
38+
| **A** scalar | byte-wise `;`/`\n`, int parse | `BTreeMap<String,Stats>` | baseline, 1 thread |
39+
| **C** threads | byte-wise, int parse | per-worker `BTreeMap`, merge | baseline, N threads |
40+
| **R** radix | byte-wise, int parse | flat SoA table, slot = `hash & 0xFFFF` | RAM flat table (control) |
41+
| **F** Morton | byte-wise, int parse | flat SoA table, slot = FNV-1a → nibble-interleaved 16-bit Morton tile | RAM flat table (substrate-native) |
42+
| **T8** byte-trie | byte-wise, int parse | 256-ary arena trie, one level per name byte | trie |
43+
| **T** nibble-trie | byte-wise, int parse | 16-ary arena trie (HHTL `NiblePath`-faithful), 2 levels per byte | trie |
44+
| **S** SWAR | **SWAR** `;`/`\n` (haszero u64 trick) + **branchless** int parse | flat SoA table (reuses F verbatim) | RAM flat table + SWAR |
45+
46+
All lanes are parity-checked: **every lane produces aggregates identical to
47+
lane A** on a generated corpus (unit tests `lane_a_and_lane_c_agree…`,
48+
`lane_f_and_lane_r_agree_with_lane_a…`, `both_tries_agree_with_lane_a…`,
49+
`lane_s_agrees_with_lane_a`, plus a forced-collision probe on the shared
50+
table). Verified in-code, not asserted.
51+
52+
## The outcomes (10M rows, 4 workers, mrows/s)
53+
54+
**v3 (x86-64-v3), n=11 per lane:**
55+
56+
| Lane | median | min | max | sd | vs C (median) |
57+
|---|---:|---:|---:|---:|---:|
58+
| C threads + BTreeMap | 31.2 | 29.4 | 32.0 | 0.7 | 1.0× (ref) |
59+
| T 16-ary nibble trie | 54.2 | 50.4 | 54.7 | 1.4 | 1.7× |
60+
| T8 256-ary byte trie | 58.3 | 54.5 | 66.5 | 3.8 | 1.9× |
61+
| F flat Morton table | 84.6 | 75.1 | 86.3 | 3.6 | 2.7× |
62+
| R flat radix table | 87.7 | 61.2 | 89.1 | 8.6 | 2.8× |
63+
| **S SWAR + flat table** | **103.9** | 76.6 | 105.5 | 7.9 | **3.3×** |
64+
65+
**native (target-cpu=native), n=7, controlled same-session (F, S only):**
66+
67+
| Lane | median | min | max | sd |
68+
|---|---:|---:|---:|---:|
69+
| F flat Morton table | 74.0 | 65.8 | 84.0 | 6.3 |
70+
| **S SWAR + flat table** | **96.9** | 90.6 | 106.2 | 5.3 |
71+
72+
## What the numbers actually say (each claim scoped to its evidence)
73+
74+
1. **SWAR (S) is the one real, robust win. [supported]** At the median, S
75+
beats F by **+23% on v3** (103.9 vs 84.6) and **+31% on native** (96.9 vs
76+
74.0). The gap (≈19 mrows/s) is ~2.4× S's own sd and clears F's max
77+
(86.3) at the median. Caveat kept honest: S is the noisier lane — its
78+
*worst* run (76.6) dips below F's median, so the guarantee is "typically
79+
+~25%, occasionally ties F," not a hard floor. The earlier best-of-3
80+
draft happened to draw an unlucky S run (77.4) that made the number look
81+
cherry-picked; the n=11 median vindicates the SWAR win but only with the
82+
spread disclosed.
83+
84+
2. **The trie is slower than the flat table here — but this is the arena-trie
85+
IMPLEMENTATION, not "the trie idea," and the distinction matters.
86+
[supported, confounded]** T (54.2) and T8 (58.3) medians both sit far
87+
below F (84.6) and R (87.7); the gap is large and robust across n=11. But
88+
two confounds are uncontrolled and inflate the trie's cost: (a) `Trie`
89+
carries `fanout` as a **runtime struct field** (`descend` computes
90+
`node*self.fanout+sym`), losing the strength-reduction/monomorphization
91+
the flat table gets from its `const SLOTS`; (b) `descend` does an
92+
**in-loop arena realloc** (`children.extend(...)` per new node, 256×u32 =
93+
1 KB/node for T8) *inside the timed scan*, while the flat table allocates
94+
once up front. So the honest claim is: **this arena-trie is not
95+
competitive with the flat table on dense small-cardinality group-by**
96+
NOT "the trie is falsified." The direction (a trie chases ~10–20
97+
dependent loads/record vs the table's ~1 hash + 1 near-L1 slot) is
98+
plausible, but a const-fanout, pre-sized-arena trie was not built, so the
99+
idea itself is untested. This does contradict the earlier-session
100+
hypothesis that the HHTL trie is what reached ~90 — no trie variant here
101+
reaches the flat table's throughput.
102+
103+
3. **Morton (F) vs plain radix (R): no measurable difference. [supported —
104+
corrected from the prior draft]** R actually medians *slightly above* F
105+
(87.7 vs 84.6), and that 3.1-mrows/s difference is well inside R's sd
106+
(8.6). The nibble-interleave is a **no-op on throughput** (possibly a
107+
marginal negative). The prior draft's "F beats R by a hair" was wrong and
108+
is retracted. The big structural win is flat-SoA-table-vs-BTreeMap
109+
(R−C ≈ +56 median), not the addressing scheme.
110+
111+
4. **`target-cpu=native` gives no benefit for these lanes — and this table
112+
contains NO SIMD lane, so it says nothing about AVX-512. [narrow claim
113+
supported; the broad one retracted]** Controlled same-session, native F
114+
(74.0) and S (96.9) medians are *below* their v3 counterparts (84.6,
115+
103.9) — native did not help and if anything ran slightly slower (likely
116+
codegen/thermal, within the noise band). The defensible statement is
117+
"the compiler's `native` flag does not speed up these SCALAR/SWAR lanes."
118+
The prior draft's "native SIMD is noise" overreached: lane S is *SWAR*
119+
(scalar u64 tricks), not vector SIMD, and the actual SIMD lane (B) is
120+
feature-gated and absent from this table. This probe cannot adjudicate
121+
any AVX-512 claim — it runs no AVX-512.
122+
123+
5. **mmap (lever a) is not measurable in this harness and was not faked.
124+
[verified in code]** The timer starts after `fs::read` (main.rs:91 → :94);
125+
mmap is a wall-clock / 13 GB-allocation lever for the full 1B file, on an
126+
axis this metric does not observe. Measuring it needs an end-to-end
127+
wall-clock mode + a memmap2 dep, which breaks the std-only, zero-dep
128+
contract of lanes A/C/F/R/T/S.
129+
130+
## The honest bottom line
131+
132+
For a dense, ~400-cardinality group-by at 10M rows on this machine: **flat
133+
SoA table + SWAR scan/parse is the fastest method measured (~104 mrows/s
134+
median, +~25% over the plain-scalar flat table); the arena-trie lanes are
135+
the slowest of the non-baseline group.** The Morton interleave buys nothing
136+
over plain radix. Native codegen buys nothing over v3.
137+
138+
**What this does NOT establish (explicit conjecture, unmeasured here):**
139+
- That a trie is the wrong structure *in general* — only that THIS arena
140+
trie loses on THIS workload; a const-fanout/pre-sized variant is untested.
141+
- That the trie "wins at prefix routing" — no prefix-routing / ancestor-query
142+
benchmark exists in this crate. That is the HHTL cascade's claimed job,
143+
but it is a CONJECTURE here, not a result.
144+
- That ~400-cardinality dense group-by is "the substrate's own aggregation
145+
shape" — unmeasured; `lane_f.rs` itself flags high-cardinality as a
146+
different regime.
147+
148+
## Scope / how to reproduce
149+
150+
Single machine, single corpus, one cardinality (~400 stations), one row
151+
count (10M). Not a claim about other CPUs, other cardinalities, or the full
152+
1B-row file.
153+
154+
```bash
155+
cd crates/onebrc-probe
156+
cargo build --release
157+
target/release/onebrc-probe gen /tmp/brc10m.txt 10000000 42 # if absent
158+
for lane in a c r f t8 t s; do
159+
for i in $(seq 1 11); do target/release/onebrc-probe run /tmp/brc10m.txt $lane 4; done
160+
done # take median + min/max/sd per lane, not best-of-N
161+
# native: RUSTFLAGS="-C target-cpu=native" cargo build --release --target-dir target-native
162+
```

0 commit comments

Comments
 (0)