The Profiler We Weren't Allowed to Have: 72 Hours of Zero-Dependency Rust at Speed Demon 2026
What We Built
Four tracks, split into two halves so no one ever touched files outside their own half: one half took log ingest and the CSV join, both "parse a big text file fast, then group by a hashed string key"; the other took the text index and the shortest-path graph, both "build an in-memory index, then answer thousands of queries in parallel." Same tricks within each half, different tricks across the two.
All four are Rust, zero runtime dependencies, with mmap and madvise declared by hand against the C library the standard library already links, rather than pulled in as a crate.
The ingest track reads a 239MB access log with pread in 128KB chunks, hashes each line's endpoint with one AVX2 compare and a fixed run of vector steps, and summarizes each hour with a counting sort instead of a general-purpose one.
The join track mmaps a 200MB CSV, finds record boundaries by a single-byte guess the spec's own guarantees make safe, and gives every distinct name a dense id through one lock-free hash table shared across all twelve threads.
The text index builds its table out of only the 50,000 incoming queries' terms rather than the whole corpus's vocabulary, evaluates each boolean query against a chunk of documents the instant that chunk is indexed and while it's still warm in cache, then throws the chunk's index away.
The graph track parses 5 million edges twice in parallel straight off the memory map, packs up to four edges per node into one 16-byte row, and answers each shortest-path query with a bidirectional Dijkstra search using a radix heap, growing whichever of the two search frontiers is smaller until they meet.
Two of the four tracks - ingest and join, the pair that's mostly parsing - also use profile-guided optimization: an instrumented build run against two seeds we generated ourselves, never the board's own seed, feeding back into the real build to steer code layout. It can't change what the program computes, only how the compiler lays out the result, which is exactly the kind of lever you reach for once you've run out of algorithmic ones.
The Gate That Matters More Than the Clock
Speed Demon, run by Hackathon Raptors, builds your Docker image and runs it five times on one dedicated Intel box, drops the highest and lowest, and the mean is the only number that counts.
Speed Demon gives you four tracks, each a slow, correct Python reference you have to beat in any language:
- ingest 3.1 million log lines
- answer 50,000 text-search queries
- join a hostile 200MB CSV
- run 2,000 shortest-path queries over a 5-million-edge graph
Each track scores 65% machine, 35% judges, who read your code, your README, and a required optimization log called PERF.md - worth 8% of your total on its own, graded like prose, not like a benchmark. Your overall score is the geometric mean of your best three tracks.
The machine side is one formula: time matters most, memory matters less and caps out its benefit at 50x, and code size and dependencies can only ever cost you a little - every dependency you add is worth about minus ten percent, which is why we shipped all four tracks with zero.
Underneath all of it sits a gate that has nothing to do with speed: your output's SHA-256 has to match the reference's, byte for byte, on every one of the five runs, or the track scores zero, full stop, however fast it ran. A wrong answer isn't a worse score. It's no score.
And the final run happens on a private holdout seed none of us ever saw, generated after code freeze, that re-runs the reference before and after every batch specifically to catch the box drifting out from under its own numbers.
That gate is also, it turns out, the only microphone we had into the real machine.
The Machine We Weren't Allowed to Touch
We had three places to measure anything, and only one of them counted. A GitHub Actions runner gave us 2 vCPUs, for free, in seconds. An M-series Mac gave us 8, for free, also in seconds.
The actual scoring box - Intel i5-13500, six P-cores, twelve threads, turbo off, AVX2 only, 1.25MB of L2 per core, 24MB of L3 - we got maybe once every five minutes, shared with whatever else the live board was doing that hour, and the number it handed back was noisy by as much as 8% run to run, by our own board logs.
We wrote the rule down for ourselves on day one, in the handoff notes for whoever picked this up next: "Local timing lies. An M-series Mac has 12MB of L2 per cluster and a different core design. Several changes that were big local wins did nothing on the box, and one cache problem that cost a third of track B's time was invisible locally. Trust only the board."
We believed that rule going in. We did not expect to need it as literally as we did.
Borrowing Five Minutes at a Time
Here's the thing about the correctness gate: the live board reports wall time and memory on every build it runs, correct or not. That's not a loophole we found. It's just how the board works - pass or fail, you get a number back, because the board's whole job is to measure, and measuring doesn't care whether your hash matched.
So when we needed to know exactly how long one phase of a multi-phase pipeline was taking, and there was no way to split that apart from inside a single binary without shipping something broken, we shipped something broken on purpose. One line, usually right before the step we wanted to isolate:
std::fs::write(out.join("summary.txt"), b"").unwrap(); // TIMING PROBE
std::process::exit(0);
Wrong hash, zero on that track for one push - and a real timing number for the one fact we actually needed, measured on the only hardware whose opinion mattered.
We started every one of those commit messages with the words "TIMING PROBE," which our own pre-push gate was taught to recognize and let through even though it would otherwise block a known-wrong hash, logged the run, and reverted the probe immediately after. None of it ever touched the commit that actually got frozen and scored for real; it only ever ran against the public board, which exists precisely to be pushed to all week.
We did this dozens of times across all four tracks, because it was the only profiler we had.
A sample of what it told us, verbatim from the logs: parsing the ingest track's 239MB file costs about 10 milliseconds just to read; the per-line endpoint lookup costs another 10, "even though the table is a 64KB thread-local array that should be in cache," which we flagged as an open mystery and never fully solved; the index track's tokenizing pass alone eats 141 of its 244-millisecond run; the join track's name-interning step costs about 13 milliseconds on top of a 28-millisecond parse. Every one of those numbers came from a push that failed on purpose.
Two things it told us were worth building a whole section of this post around.
The Bug That Needed Twelve Threads to Exist
One lock we didn't even know was contended cost us 8.6 milliseconds - on a track whose entire run, by the end, was 27. That's close to a third of our whole budget, hiding behind a registry that looked completely fine on every machine we could afford to test on, because at two threads there was nothing there to find.
The ingest track looks up every log line against roughly 1,800 known endpoint templates, hashed into a table so the lookup is just array indexing. The first version gave every worker thread its own copy of that table, kept in sync through a locked registry. On the 2-vCPU GitHub runner, this cost nothing we could measure. Two threads barely contend for a lock. We moved on, same as we did with a dozen other small decisions that the free machines waved through.
On the real box, with twelve threads hitting that registry at once, a timing probe caught it directly: removing the shared table and going back to the registry cost the parse phase 28.5 milliseconds instead of 19.9. That's 8.6 milliseconds of pure lock contention, on a track whose entire run by the end was 27 milliseconds - close to a third of the budget, caused by a condition our free hardware was structurally incapable of producing. It wasn't rare on the 2-vCPU machine. It was absent.
We wrote the fix down the same way: "one shared lock-free endpoint table instead of a locked registry (28.5 -> 19.9ms of parse): contention the 2-CPU A/B runner cannot show."
A second version of the same lesson showed up a few commits later, in memory instead of time. The first draft freed each thread's own memory blocks individually when a batch of log lines finished - about 860 small munmap calls per run. Each one triggers a TLB shootdown that has to interrupt every core on the box, not just the one doing the freeing, and on twelve cores instead of two that cost roughly 3.5 milliseconds we'd never have found by staring at a flame graph on our own laptops, because our laptops don't have twelve cores fighting over the same address space.
The fix was almost insultingly simple once we knew to look for it: one reserved mapping per hour, released with a single munmap. Boring to describe. Invisible until the real box said otherwise, twice.
What a Bootstrap Confidence Interval Is For
Not every idea deserved a board slot, and by the second day, board slots were the scarcest thing we had - one push scored roughly every five minutes, shared across the team working on four tracks. So before anything touched the real box, it had to survive an x86-64 GitHub Actions runner we built specifically to lie to us less: both commits, old and new, built for the real target architecture, correctness-checked against the reference's hash, then timed across 15 alternating runs with a bootstrapped 95% confidence interval on the speedup.
Most ideas lost. We have the receipts: skipping AND/NOT lookups once a query's result was already empty measured at 0.999 (confidence interval 0.977 to 1.010) - noise, not a win, discarded before it ever reached the board. A per-thread cache of recent hash-table lookups measured 0.792 - actively slower - discarded. A branch-free rewrite of the term-table hash finalizer came back at 1.002, confidence interval crossing 1.0 in both directions, which is the CI's way of saying "we don't know, and neither do you," so we left it alone.
The runner also has its own blind spot, the same one the lock-contention bug lived in: a few genuine board wins measured as flat or negative on only 2 vCPUs, and we only kept them because we were willing to spend a real board slot checking a result the cheap machine had just told us was worthless.
The index track's biggest single cost - tokenizing 36 million tokens - never resolved into one clean fix. It just got cheaper in a dozen small, individually unglamorous steps, each one validated this way before it was allowed to cost us a correctness-gate failure to confirm.
The exact phrase in our own notes for the one we couldn't fully explain: a cache effect "invisible locally," costing the index track a third of its time, that we never pinned down to a specific line, only to a specific condition - the real box, under real load, doing something neither free machine could reproduce.
Fast, Wrong, and Worth Exactly Nothing
The gate cuts the other way too, and we nearly found that out the expensive way. Three of our four tracks identify repeated strings by a fast finalizing hash instead of comparing bytes outright - the same tradeoff rustc itself makes for its own compiler-internal hash maps, documented plainly as "fast, deterministic, not secure" (rustc-hash), on the theory that cryptographic hashing buys resistance to attacks that don't apply once you already trust your input.
In the join track, that hash collided on two real names in one of our own test seeds - "Dacom Mina" and "Dacom Maca," different people, same bucket. We caught it before freeze, for free, by checking our own output against the reference implementation's expected answer for that seed, not by any cleverness of our own.
The fix - a stronger folded-multiply hash step - cost us nothing measurable in time. The bug, if it had shipped, would have cost everything on that track: a collision like that doesn't crash, it just silently merges two records into one, and the correctness gate doesn't partially deduct for being almost right. It zeroes the whole track, no matter how many microseconds we'd saved getting there.
What We Didn't Do
We built for target-cpu=x86-64-v3 - a real, named baseline that guarantees AVX2, FMA, and the rest of the Haswell-era instruction set rather than an arbitrary guess (Red Hat's own breakdown of the level) - on purpose, even though native would have let the compiler use more, because native only works if you build on the exact machine that runs it, and our profile-guided builds needed to train anywhere, not just on the scoring box.
We used that profile-guided build on two of the four tracks, ingest and join, the two that are mostly "parse a big file, group by a hashed key." We never got to try it on the other two.
The traverse track spends about two-thirds of its time answering queries, and a profile put 22% of the entire run inside the distance-map probe loop of a bidirectional Dijkstra search. A goal-directed search using landmark heuristics would likely cut that, by visiting fewer nodes before the two search frontiers meet - we wrote the idea down and ran out of weekend before testing whether its own setup cost would fit the budget it was supposed to save.
We never fully explained where about 24 megabytes of memory came from in two separate tracks. It wasn't thread stacks - we shrank them to 256KB and nothing moved. It
Comments
No comments yet. Start the discussion.