Our laptop said 37 MB. The scoreboard said 195 MB. That was the first of eight times in 72 hours that the live board told us we were wrong about our own code, and each time, being wrong taught us more than being right.
It also moved us up. On November 1 our Track C solution sat 43rd of the 91 teams that had scored it, at K 23.9 while the leaders were between 41 and 47. A day later it was K 32.4, and our Track D build had reached #9 of 117 on the board.
This post is about those corrections. Speed Demon 2026 is a 72-hour performance-engineering hackathon run by Hackathon Raptors. Four tracks each hand you a correct but naive Python program, and you make it as fast as you can without changing a byte of its output. Every push is built and run on one fixed machine, and a wrong output hash scores nothing. Our team, Bharat Labs (Aryan Verma and Lakshya Varshney), entered all four tracks. Everything is in the repo: github.com/codewitharyan29/speed-demon-2026.

Wall time on a log scale; the labels show exact times and speed-ups.
| Track | Language | Reference | Our best board run | Speed-up |
|---|---|---|---|---|
| A: Ingest (log to hourly percentiles) | Rust | 32.05 s, 228 MB | 23.6 ms, 18.4 MB | ~1,350x |
| B: Index (boolean and phrase search) | Rust | 56.35 s, 2,200 MB | 105.4 ms, 39.7 MB | ~535x |
| C: Join (quoted CSV join) | C++ | 14.44 s, 367 MB | 34.0 ms, 13.6 MB | ~425x |
| D: Traverse (shortest paths) | C++ | 72.69 s, 893 MB | 122.5 ms, 66.1 MB | ~593x |
Zero external dependencies on all four. These are our best logged board runs; the last section shows where to verify each one.
How scoring works, in one minute
Each track's score is a multiplier:
K = (T_ref/T)^0.5 x (M_ref/M)^0.2 x (S_ref/S)^0.15 x (1/D)^0.15 where T is wall time, M is peak memory, S is lines of source and D is the number of dependencies (the memory and speed bonuses are capped). A 10% improvement in each term moves K by:
| 10% better in... | K goes up by |
|---|---|
| time | about 5.4% |
| memory | about 2.1% |
| lines of code | about 1.6% |
So doubling K takes four times the speed, and a line of code is worth about a third of what a millisecond is. We kept that arithmetic in front of us all weekend, and it explains several decisions below.
How the 72 hours went
We were two people with one board. The board allows one run per team every five minutes, so every experiment had to be worth a push, and we took turns.
| Track | Owner | The idea that mattered most |
|---|---|---|
| A: Ingest | Aryan | 4-byte packed records, and a checked guess that the log is nearly sorted |
| B: Index | Lakshya | Compiling each boolean query into a 64-bit truth table |
| C: Join | Aryan | Finding quoted regions with one carry-less multiply, and never unescaping names |
| D: Traverse | Lakshya | Bidirectional search with both distances in one 32-bit word |
The repo has 266 commits: 110 from Lakshya, 104 from Aryan and 52 from Claude Code sessions we ran. 162 of them carry a Co-Authored-By: Claude Sonnet 5.5 trailer, so a large share of the code and measurement scripts was AI-assisted. We set the goals and ran and read every board result. Our last commit, a README edit, landed at 20:28 IST, two minutes before the freeze. Track B's decisive redesign landed at 19:12.

K for every board run we logged. The green line is our best so far, each dip is an experiment we reverted, and the labelled jumps are corrections from this post.
Correction 1: our memory number was a lie
What we believed. Track A's first serious rewrite used 37 MB on our laptops.
What the board showed. 195 MB.
Why. The scoring machine runs transparent huge pages in always mode, and our Docker VM ran madvise. Our arena was one big sparse mapping with a small region per chunk and hour, so every 2 MiB region we touched was silently backed by a full 2 MiB huge page. We proved it by forcing huge pages on locally, and our own build jumped from 25 MB to 197 MB.
The fix was tiny. Track C gave the cleanest version. One line, prctl(PR_SET_THP_DISABLE), took the same code from 37.5 MB to 16.2 MB, and K from 18.9 to 22.4. In Track A, MADV_NOHUGEPAGE on the arena, plus a layout change, took 195 MB down to 30 MB.
What generalizes. A benchmark number measures a machine plus a kernel configuration. Match the scoring machine's settings before you trust a memory figure.
Correction 2: local numbers don't transfer, in either direction
What we believed. For Track A's 239 MB input, mmap must beat reading: no copies, no buffers.
What the board showed. The mmap build was 14% slower (29.8 ms against 26.2 ms). Page-faulting 239 MB costs real time, and so does tearing the mapping down, which we measured at about 24 ms in an early build. Reading 32 KB blocks with pread keeps data in cache and avoids both.
The same pattern kept appearing, and it cut both ways:
- Local wins that vanished. One reader thread per parser was about 19% slower on the board (30.4 against 25.7 ms). Writing Track A's output with
fadvisecut peak memory locally from 17.4 MB to 9.4 MB, and the board's M did not move (19.53 against 19.49 MB). In Track C, an "instruction diet" cut instructions by 14% and ran 2.1 ms slower. We never found the cause, so the log says "cause not found". - Peak memory is a maximum, not a sum. In Track D, query-phase memory fell from 74 MB to 36 MB locally, and the board's M did not budge, because the peak sat in the graph build. Reading
memory.currentat each barrier showed it: the five build arrays together. - A small local gain that was huge on the board. In Track B, moving serial work into parallel pieces saved about 5 ms on our laptops and 35 ms on the board (0.242 to 0.207 s). The scoring box has turbo off and runs a core at about 2.5 GHz against our laptops' ~4.8 GHz, so every serial millisecond costs it far more.
What generalizes. Fewer instructions, fewer copies and tidier code are hypotheses about time. Only the scoring machine decides, and it can disagree in either direction.
Correction 3: the log was "nearly sorted", and the spec didn't promise it
What we believed. Track A's log is sorted by time, so we can finish an hour early and free its memory.
What we found. We measured the data: 13% of lines sit in an hour behind the newest one, 26,000 lines have an earlier timestamp than their predecessor, and the largest lag is 70 minutes. That is nearly sorted. But the spec says the log is not sorted, with no bound on how far a line can be out of order.
What we did. We turned the observation into a guess that is checked. A worker summarises an hour as soon as the chunks still to come shouldn't contain it, and every chunk verifies that it never sees an hour already summarised. If one does, the program discards the speculative result and re-runs the whole file in a safe pass. We tested both paths on shuffled and reversed inputs and on 16 adversarial datasets.
Tracks C and D follow the same rule. Track C assumes a newline inside a quoted field is never followed by a digit or a BOM, verifies every chunk boundary, and re-parses unchunked if one fails. Track D answers over 99.98% of queries with a 16-bit bucket queue and escalates the rest to an exact 64-bit bidirectional Dijkstra.
What generalizes. Measure the data, but only rely on what the spec guarantees. For everything else, write the check and the fallback.
Correction 4: the profiler said 270, we had assumed 40
What we believed. Track C's parser finds which commas and newlines sit inside quotes with one carry-less multiply (PCLMULQDQ) per 64 bytes: the running parity of the quote bits is the "inside a quote" mask, and a doubled quote flips it twice. We assumed the rest of the record work would cost about 40 instructions per record. A cachegrind run on the full file said 270.
Why. Names containing a doubled quote (12.6% of all names) each ran a malloc plus a byte loop to unescape. A branch on the quote count was badly mispredicted, and Vec::push capacity checks sat in the hot loop.
The fix was a different question. We stopped asking how to unescape faster and asked what the output needs. It needs equality of names, and a quoted name with doubled quotes maps one-to-one onto the real string. So two names are equal exactly when their raw bytes are equal, and we never unescape anything.
What generalizes. Before optimising a step, ask whether its result is ever used. Often only a property of it is.
Correction 5: the spec was a data structure in disguise
What we believed. Track B, boolean and phrase search over a 254 MB corpus, needs an inverted index. Ours used 328 MB and scored K 10.3.
What changed. Two spec facts we had read but not used. The memory multiplier maxes out at 44 MB, so a global posting list could never fit. We switched to indexing and answering one chunk at a time on 12 workers, and memory fell from 171 MB to 62 MB in one step.
And boolean queries have at most 6 distinct terms. Six terms means 2^6 = 64 combinations of "present or absent", exactly the number of bits in a u64. Each query compiles into a 64-bit truth table, and answering a document is one expression:
hit = (table >> mask) & 1 where mask is six presence bits gathered from a small L1-resident bitset in one AVX2 instruction. No posting lists, no intersections. Time fell from 163 ms to 105 ms, and the source from 1,285 lines to 961.
What generalizes. Re-read the spec for suspiciously small numbers. A bound is often a license to replace a general structure with a specialised one.
Correction 6: the idea we rejected twice worked the third time
What we believed. Bidirectional search wasn't worth it for Track D. Lakshya had tried it twice in the Rust version and rejected it both times.
What changed. In the C++ rewrite he looked at why it had failed. Both attempts put a reverse graph of 8-byte edges next to a 40 MB forward graph, with separate distance arrays. The failures were verdicts on two implementations, not on the idea. The third version used a compact reverse graph, built after the build arrays were released so the memory peak did not move, and stored forward and backward distances in one 32-bit word, so checking whether the two searches have met costs no extra cache miss.
Query time fell from 95.6 ms to 39.5 ms (-59%), and the whole run by 27% locally. The board confirmed it in full: 0.2418 s to 0.1783 s, K 20.9 to 24.0. One bug on the way mixed u32 and int, so unreachable queries printed 4294967295 instead of -1. Exactness checks against the reference are what make a bug like that visible.
What generalizes. Write down why an idea failed, not just that it failed. When the surrounding design changes, the reason may no longer apply.
Correction 7: lines are cheap, but only if they are free
What we believed. Source size is part of the score, so shorter is always better.
What the formula says. Lines are worth about a third of time (see the table above), so a line-saving change has to cost almost no speed. Two experiments showed it.
In Track C, our C++ rewrite kept the Rust version's architecture and shrank it from 656 to 434 lines at about the same speed. The formula predicted K would rise from 29.63 to about 31.5, and the board measured 31.51. In Track A, a from-scratch minimum-lines rewrite (304 lines instead of about 500) earned x1.076 in K for the lines but cost x0.77 for the time, so K fell from 41.9 to 32.8.
What generalizes. Do the arithmetic of the scoring function before you pick a direction. Short code is a bonus on top of speed, never a substitute for it.
Correction 8: we trusted a single run
What we believed. If a change makes the board time go down, it worked.
What the board showed. The same Track C source, run three times, gave 52.6, 52.9 and 55.8 ms. Board noise is about 6%, so any "win" smaller than that was a coin flip. What we did about it:
- Interleaved board comparisons. 34 of our commits push a candidate and a control build of the last known-good code alternately, for example
a1, read2, parse2, a1, pct2, fmt2, a1. If the machine drifts during the session, it drifts under both. In Track C, a candidate called cx22 (clean under three sanitizers) measured slower than cx19b this way, so we restored cx19b. The builds we left in place at freeze for Tracks A and C, called LF3 and cx40c, came out of these comparisons. - Diagnostic builds. 11 more commits deliberately skip or repeat one stage just to time it. A build that read every block and did nothing else ran in 8.4 ms in Track A, so we knew the floor before optimising.
- Source hashes in commit messages. In the very last comparison push, a runner argument error copied a Rust file over our C++ source. Each commit message records the source's sha256 prefix, so the restore commit could name the exact file it put back, and anyone can check it.
- Labelled claims. Our competitor notes tag each statement
VERIFIEDorHYPOTHESIS, and they say plainly that we looked at nobody's code.
What generalizes. Measure your noise first. Then compare against a control in the same window, not against yesterday's number.
Eight lessons, one line each
- Match the scoring machine's kernel settings before trusting a memory number.
- Treat every local speed-up as a hypothesis; the scoring machine can disagree in either direction.
- Measure the data, but rely only on what the spec guarantees, and keep a checked fallback for the rest.
- Before optimising a step, ask whether its result is even needed.
- Read the spec's small numbers as hints for specialised data structures.
- Record why an idea failed, so you know when to try it again.
- Do the scoring arithmetic first: time is worth about three times as much as lines.
- Know your noise floor, and compare against a control in the same window.
If we did it again, we would do 1 and 8 on day one and pick each track's architecture within the first six hours. Our biggest gains came from design changes, not micro-tuning.
We accepted one risk knowingly. Track A's endpoint dictionary trusts a 64-bit hash and never compares key bytes, with a collision chance of about 1 in 10^13 across at most 1,842 endpoints. It measured about 7 ms faster locally. A byte-verified version exists and costs a few percent, and the README names both.
If you remember one thing from this post: every belief about performance is a hypothesis until the machine that scores you agrees. We were wrong eight times, and the leaderboard is where we found out.
The final run
After the freeze, Hackathon Raptors re-runs the top teams on a hidden holdout dataset. Our results are on the Showtime page.
Check our numbers
Every figure comes from the PERF.md logs and the commit history in the repo.
| Track | Logs | Board run / commit |
|---|---|---|
| A | PERF.md, README | run-d310f2ad455b (commit 66693b1) |
| B | PERF.md, README | run-f7d2d327f20c (commit 4a0b9ec) |
| C | PERF.md, README | run-d8a78803f209 (commit 9bfa8af) |
| D | PERF.md, README | commit 18c1dcf |
The results table lists our best logged board runs. The builds at freeze for Tracks A and C (LF3 and cx40c) were chosen by interleaved board comparisons whose numbers we did not copy into PERF.md, so check the live board for those. The 43rd-of-91 figure is from our docs/COMPETITORS.md snapshot of the public board feed (121 teams, 91 with a scored Track C run, generated 2026-11-01 01:57 UTC). The #9-of-117 ranking for Track D is from our log of the step-35 board result. Local timings came from a Windows Docker VM and only ranked ideas against each other, and board numbers carry roughly 6% noise.
Thank you to Hackathon Raptors for a brutal, fun and educational 72 hours. Team Bharat Labs: Aryan Verma and Lakshya Varshney. Code and logs: github.com/codewitharyan29/speed-demon-2026.
