Skip to content

Commit 0776626

Browse files
committed
onebrc-probe: lane T — HHTL trie group-by + trie-vs-RAM measurement report
Adds the HHTL trie group-by lane (name-as-prefix-descent instead of hash+slot) in two variants — 16-ary nibble trie (contract::hhtl NiblePath faithful) and 256-ary byte trie — plus a measurement report documenting the full RAM-table-vs-trie ladder. Measured, honest NEGATIVE result (10M rows, 4 workers, n=11 median, mrows/s): the trie is SLOWER than the flat table at ~400-station cardinality — T(16-ary)=54.2, T8(256-ary)=58.3 vs F(flat Morton)=84.6, R(flat radix)=87.7. At this cardinality a single hash + linear probe into a contiguous SoA table beats the trie's dependent-load descent. Parity: both tries produce aggregates identical to lane A (test both_tries_agree_with_lane_a_on_generated_corpus). The report (RESULTS_TRIE_VS_RAM.md) was corrected after adversarial review to report median/spread (n=11), not best-of-3; it flags the trie result as confounded by this arena implementation (runtime-field fanout + in-loop realloc), NOT the trie idea falsified, and marks the "trie wins at routing" claim as unmeasured CONJECTURE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MLBnPuScZy6w9di2QEjsXM
1 parent 427e63e commit 0776626

4 files changed

Lines changed: 404 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)