I optimised for time. Speed Demon scored me on something else.
My Speed Demon 2026 write-up — 72 hours, four tracks, zero-crate Rust, and the three lessons that actually moved the score.
I entered Speed Demon 2026, run by Hackathon Raptors, as team BaldheadBill (my team of me and the voices in my head). Four tracks, 72 hours (Oct 30 – Nov 2, 2026), one rule that shaped everything: Rust, standard library only, zero external crates. No shared code between track folders except by copying it in.
The code is here: https://github.com/BaldheadBill/speed-demon-2026
Final board, Nov 2:
| Track | Board time | Board memory | Lines | K | Points |
|---|---|---|---|---|---|
| A: Ingest | 0.177 s | 64 MB | 503 | 12.477476 | 54.806 |
| B: Index | 1.409 s | 501 MB | 741 | 6.206545 | 39.642 |
| C: Join | 0.377 s | 275 MB | 634 | 5.031920 | 35.087 |
| D: Traverse | 2.062 s | 524 MB | 356 | 4.810890 | 34.111 |
Best three tracks (A, B, C) give 42.401 points overall. Final ranks: overall 103, A 111, B 95, C 102, D 107.
That rank is not the story. The story is that I spent the first part of the sprint optimising the wrong thing, and the score formula kept telling me so — I just had to stop reading it as a stopwatch. My fault entirely because in my industry, i am rather known for creating lightening fast software; call me cocky or whatever but I honestly thought i was going to flat out smoke the competition on this one.... I was SO very wrong.
**The score is not time. It is K.**
Speed Demon's K blends time, memory, code size, and dependencies. Time carries the heaviest exponent (0.50), memory is next (0.20), and code size / dependencies are 0.15 each. Points are 50 × log10(K).
That matters because a change can win on the clock and lose on the board. Once I internalised that, most of my decisions got simpler: bank a hash-verified score early, then only push a change that beats it on K — not on one axis.
And correctness is not a phase, it is the gate. A run that fails correctness, the timeout, or the sandbox scores nothing. Every version below was byte-exact against the board's expected output hash before it went anywhere near a push. Wrong output is zero, however fast it is.
_ 1. The profiler reading I misread: mmap, memory, and the hot cache_
Track A is ingest: 3.1M log lines, per-endpoint counts, p50/p95/p99 latencies, hourly error rates. Reference: 32.05 s, 228 MB. My progression on the board:
| Version | Board time | Board memory | Lines | K |
|---|---|---|---|---|
| Parallel, whole file in memory | 0.319 s | 309 MB | 177 | 7.941 |
| Streaming ranges, per-thread buffered line reader | 0.259 s | 73 MB | 204 | 11.519 |
| Merged final: 1024-shard mutex table, inline small-group latency storage, SWAR parse | 0.181 s | 67 MB | 468 | 12.350 |
| Hot-cache variant of merged final | 0.170 s | 68 MB | 539 | 12.427 |
| Champion: mmap variant | 0.177 s | 64 MB | 503 | 12.477476 |
Two misreads in that table:
1st, the biggest single K jump was not a faster parser. Going from whole-file-in-memory to streaming ranges improved time by 19% — and cut memory from 309 MB to 73 MB. K went from 7.941 to 11.519, up 45%. I had been staring at parse speed. The binding axis was memory.
2nd, the hot-cache variant was faster on the clock (0.170 s vs 0.181 s) and I nearly banked it as the winner. Then the mmap variant came in slower than the hot cache (0.177 s) — and won on K anyway, because it was 36 lines shorter and 4 MB lighter. A 0.007 s time win lost to lines and memory. If you only watch time, you ship the wrong champion.
There was yet another, dumber misread underneath the mmap decision: I expected memory-mapped pages to count toward the board's memory number. The input is 239 MB; if mmap pages counted, the total should have blown up toward ~290 MB. It did not. The board showed 64 MB total. mmap pages do not count toward the board memory number, and that settled the question — no madvise fallback, no hedging. The measurement beat my mental model of the profiler, which is exactly what a measurement is for.
Lesson: profile the score, not the stopwatch. Find the binding axis first, and distrust any optimisation whose win lives entirely on the axis you happen to be watching.
_ 2. The rewrite that made it slower — and I shipped it anyway_
That mmap champion is also my "rewrite that made it slower" story, and it is the one I am glad I kept. It was slower than the hot-cache version I already had banked. In a normal sprint you do not replace a faster build with a slower one. Here, the slower build scored higher, used less memory, and was shorter. I shipped the slower program because the score beat out my pride in the stopwatch.
The same discipline killed several other "improvements" that never got pushed:
- Track B, memory variant: exact-size reads, a Bloom pre-filter, and CSR phrase assignments. Hash-verified locally. Its memory gain was about cancelled by the added lines, time was about the same, and pushing it risked a banked 39.587 points for a wash. Kept the champion.
- Track D, +7-line variant: 4.398 s → 4.314 s locally, hash-verified. Not pushed — after Track C's v2 result, D was the dropped track, so a fraction of a point on memory had no upside to the overall.
- Track A, a compression stage: I build compression algorithms (DEAD-Q is mine and published) so the temptation was real. I evaluated it and rejected it: the output must be bit-exact percentiles, latencies are only ~12 MB inside 67 MB, time is the binding axis, and compression adds CPU. Do not optimise an axis that is not binding, even if it is your favourite axis.
In a 72-hour competition, a banked, hash-verified score is an asset. A change that is merely "also good" is not an improvement; it is risk with no upside. That is a hard rule to follow when you are enjoying the work, and it is the rule that protected the final board.
_ 3. The change that more than halved it: parallelise the scan, don't rewrite the parser_
Track C is a hostile-CSV join: 3.32M records, quoted fields, embedded commas and newlines, a join on region_id, sorted aggregates. Reference: 14.44 s, 367 MB.
Version 1 was a single-threaded byte state machine: 1.272 s on the board, K 2.957. V2 kept that exact parser: scan_field, delimiter handling, integer-cents parsing, money formatting, all unchanged. This changed only how the work was split: the input is divided into chunks at record boundaries found by quote-parity, each thread runs the same state machine over its chunk, names are interned through a 64-way sharded table, and per-thread partial groups are merged at the end.
Locally that went 5.445 s → 2.461 s. On the board it went 1.272 s → 0.377 s, K 2.957 → 5.031920 — a 3.4× time cut, and the track that displaced D in my best three. In retrospect, i wish i had chased Track C a little harder rather than as a last resort when i got stuck on A, B, and D. Somewhere along the way i had made up my mind that C wasn't worth the trouble so I didn't bother with it until like hour 60ish.
Track B rhymes with it. Version 1, single-threaded, ran 8.33 s locally (scan 5.89 s, BOOL 0.59 s, PHRASE 1.67 s, 22,240,530 postings). V2 parallelised the doc scan over line-aligned ranges, ran BOOL chunks in parallel, and gave PHRASE queries a parallel second scan. First board score: 1.415 s, K 6.190689; a re-run of the same commit gave 1.409 s, K 6.206545. The core insight was not a cleverer index, it was a smaller one: only terms that appear in the queries get postings at all. The reference builds a full index and stores raw text (2.29 GB); the wanted-terms-only index landed at 501 MB.
The pattern across both tracks: the parser was already correct. The win was in the split — where chunk boundaries are safe, what state is per-thread, and what gets merged once at the end instead of locked on every line.
4. The spec traps were the real competition
Every track had a correctness trap that a fast-but-wrong solver walks straight into. These are the ones that cost time if you find them late:
-
Track A: error is 5xx only. 499 is not an error. My pre-sprint assumption was status ≥ 400; the sample data corrected it before kickoff. Percentiles are nearest-rank, duplicates kept,
error_ppm = floor(errors × 1,000,000 / count). -
Track B: tokenisation is ASCII bytes only.
Zürichbecomesz,rich, notzurich.007≠7. BOOL is left-associative with no precedence:a OR b AND cis(a OR b) AND c. Get that wrong and the "optimised" BOOL evaluator is just a fast wrong answer. -
Track C: a BOM is skipped only at file start or immediately after a record terminator. Quoted
""is one". Money is integer cents throughout, printed with exactly two decimals, and0.00is never-0.00. -
Track D: 1,979 distinct sources over 2,000 queries, so per-source caching buys at most ~1% reuse — evaluated and rejected. A* was rejected too: the graph has no coordinates. Bidirectional Dijkstra over a CSR graph, parallel across queries with atomic work-stealing and per-thread distance stores with u16 generation stamps, was the answer. And
s != tcan still be distance 0, via zero-weight paths.
All four outputs were byte-exact against the expected hashes:
- A
summary.txt:495ca09682b9673ef836a506496177680bb4a3af54255174befddb1f83481cd3 - B
results.txt:f86973c210228141bcb6f1f1b10188ad2a9b65f71e7cd7cf4590b38de4e80927 - C
aggregates.txt:d8088b1cd42ba1338b1345b107b55db7b96ef1f5066f3ff64fd26a8912a0f536 - D
distances.txt:a23dbdf0a3e49cc8901310a9ee1dc6a9a569a85297b6da75c2a7002cb2ed277e
What I would do differently
Three things, all cheap in hindsight:
- Start from K, not from time. Write the score formula on a sticky note before the first line of code, and judge every variant against it. My best Track A decision, streaming ranges, looked modest on time and enormous on K.
- Measure on the board, not locally. Local Docker times were for iteration only and are not comparable to board times. Same commit, re-run: Track B 1.415 s → 1.409 s, Track D 2.117 s → 2.062 s. That is box variance, and it is larger than some of the "improvements" I considered pushing.
- Do not trust an early rank. On the evening of Oct 30 I was overall #41 with 41.985 points. By Nov 2 I was #103 with 42.401 points, a better score and a much worse rank, because the board filled up over the sprint weekend. Early position is mostly a measure of who has submitted yet.
None of that is glamorous. It is the whole game, though: a correct program, a banked score, and the discipline to only replace it with something that beats it on the actual score.
Thanks to Hackathon Raptors for running Speed Demon 2026. The spec traps, the hash gates, and a scoring formula that punishes single-axis thinking are what made it worth writing about. Now the wait is on for the releases so i can see how in the $#%&##&# blue-blazes did those teams pull off what I could not even come close to.
Code: https://github.com/BaldheadBill/speed-demon-2026 Rust, zero crates, all four tracks, with a PERF.md optimisation log in each track folder.
Top comments (0)