<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Peeter Tsanev</title>
    <description>The latest articles on DEV Community by Peeter Tsanev (@peeter_tsanev_cdb29555f92).</description>
    <link>https://dev.to/peeter_tsanev_cdb29555f92</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4222765%2F284ddb84-90bf-4574-993c-50c2b2466484.png</url>
      <title>DEV Community: Peeter Tsanev</title>
      <link>https://dev.to/peeter_tsanev_cdb29555f92</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/peeter_tsanev_cdb29555f92"/>
    <language>en</language>
    <item>
      <title>I optimized four Speed Demon tracks. The scoring box changed my mind.</title>
      <dc:creator>Peeter Tsanev</dc:creator>
      <pubDate>Tue, 03 Nov 2026 21:21:12 +0000</pubDate>
      <link>https://dev.to/peeter_tsanev_cdb29555f92/i-optimized-four-speed-demon-tracks-the-scoring-box-changed-my-mind-2446</link>
      <guid>https://dev.to/peeter_tsanev_cdb29555f92/i-optimized-four-speed-demon-tracks-the-scoring-box-changed-my-mind-2446</guid>
      <description>&lt;p&gt;I entered &lt;a href="https://speeddemon.dev" rel="noopener noreferrer"&gt;Speed Demon 2026&lt;/a&gt;, a 72-hour performance contest organized by &lt;strong&gt;Hackathon Raptors&lt;/strong&gt;, with solutions for all four tracks. The code froze on November 2, 2026. I kept the final implementation and the experiment logs in &lt;a href="https://github.com/Peeter1111/speed-demon" rel="noopener noreferrer"&gt;my public repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I expected the fastest local version to be the strongest submission. That expectation failed twice in the final round. I also learned that a memory saving can beat a speed improvement, that a shorter source file can score as longer code, and that passing a SHA-256 output check does not prove an internal hash table is collision-safe.&lt;/p&gt;

&lt;p&gt;This is the account of the measurements, including the changes I reverted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The score is not just elapsed time
&lt;/h2&gt;

&lt;p&gt;The contest assigned each track a multiplicative performance factor:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;K = (Tref / T)^0.5 × (Mref / M)^0.2 × (Sref / S)^0.15 × (Dref / D)^0.15&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;T&lt;/code&gt; is wall-clock time. &lt;code&gt;M&lt;/code&gt; is peak charged memory. &lt;code&gt;S&lt;/code&gt; is counted code lines. &lt;code&gt;D&lt;/code&gt; is one plus the number of direct runtime dependencies. The corresponding reference measurements have the &lt;code&gt;ref&lt;/code&gt; suffix. Memory benefit was capped at a 50-fold ratio, and the code-size factor could not exceed 1. For the four C++ entries, &lt;code&gt;D&lt;/code&gt; was 1.&lt;/p&gt;

&lt;p&gt;Machine points were &lt;code&gt;50 × log10(K)&lt;/code&gt;, capped between 0 and 100. Machine results accounted for 65% of a track total, with 35% assigned to judging. I treated the machine score as the part I could measure directly, without claiming it predicted the judges' points.&lt;/p&gt;

&lt;p&gt;The exponents matter. If everything else is fixed, halving time raises K by &lt;code&gt;2^0.5 = 1.414&lt;/code&gt;. Halving memory raises K by &lt;code&gt;2^0.2 = 1.149&lt;/code&gt;, or about 15%. The latter is a meaningful gain even when the program does not get faster.&lt;/p&gt;

&lt;p&gt;Here are the four scores on the &lt;strong&gt;frozen commit&lt;/strong&gt;, as reported by the public board on November 2. These are single scoring-box runs, not local estimates.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Peak memory&lt;/th&gt;
&lt;th&gt;Counted lines&lt;/th&gt;
&lt;th&gt;K&lt;/th&gt;
&lt;th&gt;Machine points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A, request aggregation&lt;/td&gt;
&lt;td&gt;0.03974 s&lt;/td&gt;
&lt;td&gt;30.94 MB&lt;/td&gt;
&lt;td&gt;298&lt;/td&gt;
&lt;td&gt;32.95&lt;/td&gt;
&lt;td&gt;75.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B, text indexing&lt;/td&gt;
&lt;td&gt;0.35296 s&lt;/td&gt;
&lt;td&gt;59.17 MB&lt;/td&gt;
&lt;td&gt;1,044&lt;/td&gt;
&lt;td&gt;18.06&lt;/td&gt;
&lt;td&gt;62.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C, distinct-name aggregation&lt;/td&gt;
&lt;td&gt;0.07365 s&lt;/td&gt;
&lt;td&gt;16.50 MB&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;21.08&lt;/td&gt;
&lt;td&gt;66.19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D, graph paths&lt;/td&gt;
&lt;td&gt;0.55867 s&lt;/td&gt;
&lt;td&gt;83.41 MB&lt;/td&gt;
&lt;td&gt;523&lt;/td&gt;
&lt;td&gt;12.60&lt;/td&gt;
&lt;td&gt;55.02&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scoring machine used an Intel i5-13500 with 12 hardware threads and turbo disabled. My test laptop (i5-1035G1, 8 threads) used the same full-size dataset and correctness outputs, but not the same CPU or scheduling environment. That distinction became central.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory was a scoring axis I could calculate
&lt;/h2&gt;

&lt;p&gt;The organizers published reference times of 32.05, 56.35, 14.44 and 72.69 seconds for A through D. The public board exposed each of my runs' &lt;code&gt;T&lt;/code&gt;, &lt;code&gt;M&lt;/code&gt;, &lt;code&gt;S&lt;/code&gt;, &lt;code&gt;D&lt;/code&gt; and &lt;code&gt;K&lt;/code&gt;. By rearranging the formula, I could back-solve the reference memory used in the score:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Mref = M × [K / ((Tref/T)^0.5 × (Sref/S)^0.15 × (Dref/D)^0.15)]^5&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Using the frozen runs and the published reference line counts of 56, 91, 108 and 43, I obtained approximately &lt;strong&gt;228 MB for A, 2,200 MB for B, 368 MB for C and 893 MB for D&lt;/strong&gt;. These are reconstructed numbers, not independent reference-process measurements. They reproduce the board's reported K after rounding.&lt;/p&gt;

&lt;p&gt;That calculation changed where I spent effort. For B, reducing peak memory from roughly 99 MB to 59 MB could matter even when an algorithmic rewrite saved only hundredths of a second. Round 6's chunked 16-bit postings lowered the test laptop's measured peak from 94,824 KB to 60,712 KB; its estimated K rose from 13.4 to 14.9. I retained that storage approach before trying the final phrase rewrite.&lt;/p&gt;

&lt;p&gt;There was another accounting detail: the output file counted toward &lt;code&gt;memory.peak&lt;/code&gt;. On A, the full output was about 10.1 MB. A separate 10 MB write to the temporary output filesystem increased measured peak by about 12.3 MB. Optimizing the parser alone could not make the charged peak vanish. The input dataset was prewarmed and its &lt;code&gt;/data&lt;/code&gt; page cache excluded; output pages were not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line count punished clever formatting
&lt;/h2&gt;

&lt;p&gt;The code-size factor was not a count of what I typed. The scoring pipeline first ran &lt;code&gt;clang-format --style=LLVM&lt;/code&gt; and then &lt;code&gt;scc 4.1.0&lt;/code&gt; on code files and the Dockerfile. Documentation and configuration metadata did not count. The metric was the code-line count &lt;strong&gt;after formatting&lt;/strong&gt;, so compressing many statements onto one long line did not buy a smaller &lt;code&gt;S&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In an early B experiment, 353 written lines became about 970 counted lines. That difference made some compact-looking rewrites disappointing. In D's last round, a hash-deduplicated query variant took 0.893 seconds on the test laptop versus 0.897 for the retained version, but required 572 counted lines instead of 562. Both rounded to K 10.0. I retained the smaller source rather than treating a four-millisecond advantage as a win.&lt;/p&gt;

&lt;p&gt;A also illustrates the trade-off. The move from the round-four design to packed four-byte records and 512-record blocks grew the counted source from 283 to 298 lines. But memory fell from 36.7 MB to 29.9 MB, and the board's K went from 31.9 to 33.4. The larger program scored better.&lt;/p&gt;

&lt;h2&gt;
  
  
  My test laptop selected the wrong final experiments
&lt;/h2&gt;

&lt;p&gt;On my test laptop (i5-1035G1, 8 threads), C's round-seven owner-queue design raised the estimated K from 17.7 to 20.4. It routed name updates into eight single-owner queues to avoid atomics on country masks. The idea was reasonable, and its memory saving appeared real.&lt;/p&gt;

&lt;p&gt;The official 12-thread scoring machine disagreed. Relative to C's retained round-six run at 0.07303 seconds and 16.55 MB, round seven used about 38% less memory but took about 0.0873 seconds, roughly &lt;strong&gt;20% longer&lt;/strong&gt;. Its K was 20.85 instead of 21.16. I reverted it. The local result had predicted memory and line count better than it predicted time.&lt;/p&gt;

&lt;p&gt;Track B produced a different warning. Round seven moved exact phrase verification into the document scan, eliminating a separate positional index. My local check built with fixed compiler flags. I then submitted a version with &lt;code&gt;-funroll-loops&lt;/code&gt; enabled.&lt;/p&gt;

&lt;p&gt;On the board, the flagged version took &lt;strong&gt;0.445845 seconds&lt;/strong&gt; with K &lt;strong&gt;16.11&lt;/strong&gt;. Removing that flag from otherwise matching source gave &lt;strong&gt;0.354023 seconds&lt;/strong&gt; with K &lt;strong&gt;18.05&lt;/strong&gt;. The flag cost about 26% in elapsed time in those samples, despite almost identical memory and the same 1,044 counted lines. This is a one-run comparison, not proof that the flag is always bad. It was sufficient evidence not to retain it for this entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hash trap that passed the output checks
&lt;/h2&gt;

&lt;p&gt;C required grouping names exactly while aggregating country masks. Storing each entire name made the interner larger, so I experimented with hash-only fingerprints. The collision question was not academic: a collision between two distinct names would silently merge their aggregates.&lt;/p&gt;

&lt;p&gt;One rejected design used two CRC32C streams with different seeds as a supposed 64-bit fingerprint. That sounds like 64 bits of independent evidence. But CRC operations are linear or affine over their input, so changing only the initial seed can leave the two results related by a fixed affine transformation. Two dependent 32-bit streams do not automatically make a strong independent 64-bit fingerprint.&lt;/p&gt;

&lt;p&gt;The design passed the expected-output SHA-256 checks on the routine datasets. My experiment log records that a broader audit of about &lt;strong&gt;2 million names over 20 seeds&lt;/strong&gt; nevertheless found &lt;strong&gt;three collisions between distinct names&lt;/strong&gt;, two in seed 5 and one in seed 18. I kept the log line, not the raw trace, so treat the exact count as logged rather than reproducible. A correct output checksum on a few inputs does not establish that the internals are collision-free for another input.&lt;/p&gt;

&lt;p&gt;I changed the experimental second stream to mix its result with odd multipliers, breaking that simple seed-only relationship. A separate 50-seed audit covered &lt;strong&gt;5,000,050 names&lt;/strong&gt; and found &lt;strong&gt;zero full 64-bit collisions&lt;/strong&gt;. Zero observed collisions is still not a proof. I ultimately reverted the round-seven owner-queue version for its board performance and kept the round-six interner. The correctness risk and the speed result were separate decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What failed was as useful as what stayed
&lt;/h2&gt;

&lt;p&gt;These are three examples from the experiment logs. The times in this table come from the test laptop, not the scoring board.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Experiment&lt;/th&gt;
&lt;th&gt;Measured result&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Reduce record blocks from 512 to 256&lt;/td&gt;
&lt;td&gt;Median 0.060 to 0.067 s; peak 28,020 to 29,476 KB&lt;/td&gt;
&lt;td&gt;Revert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Evaluate Boolean and phrase queries directly during scanning&lt;/td&gt;
&lt;td&gt;Median 5.186 s versus a roughly 0.7 s indexed design; 209,094,387 Boolean candidate checks&lt;/td&gt;
&lt;td&gt;Revert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Parallelize CSR scatter with dense degree arrays&lt;/td&gt;
&lt;td&gt;Median 1.207 to 1.456 s, about 21% slower&lt;/td&gt;
&lt;td&gt;Revert&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The B failure was particularly informative. Eliminating the intermediate index reduced memory, but 209 million candidate Boolean evaluations overwhelmed that saving. A phrase-only version remained useful as a separate direction; evaluating all query types inside the scan was the mistake.&lt;/p&gt;

&lt;p&gt;For D, packing the search heap and staging CSR cursors reduced memory without requiring a more complicated parallel scatter. The retained round-six design measured 0.897 seconds and 80,632 KB on the test laptop, at K 10.0. The frozen scoring-box version later ran in 0.55867 seconds with K 12.60. I do not mix those two timings into one speedup calculation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Even the board was noisy
&lt;/h2&gt;

&lt;p&gt;I reran identical A source on the official box. One run took &lt;strong&gt;0.03924 seconds&lt;/strong&gt;, another &lt;strong&gt;0.04270 seconds&lt;/strong&gt;, about &lt;strong&gt;9% slower&lt;/strong&gt; with no algorithm change. A third repeat logged roughly 0.0387 seconds. The memory peak moved as well. The K difference between the first two was 33.4 versus 31.8.&lt;/p&gt;

&lt;p&gt;That is large enough to blur several optimizations I had considered successes on one run. I started using the median of five local runs to screen changes, byte-for-byte checks on sample and full datasets, and independent generated seeds to catch correctness mistakes. None of those steps makes local timings interchangeable with board timings. They do make it harder to mistake a single fast measurement for an improvement.&lt;/p&gt;

&lt;p&gt;The frozen entry was commit &lt;code&gt;bf0866a&lt;/code&gt;, with all four tracks passing the board's build, correctness, timeout and sandbox gates. I did not end with the best previously observed A or C score on every rerun. I ended with the source whose behavior and trade-offs I could explain.&lt;/p&gt;

&lt;p&gt;For this 72-hour contest, the useful conclusion was narrower than "make everything faster." I had to optimize the measured formula, count output memory, measure the formatted source, distrust correlated fingerprints, and verify speed on the actual 12-thread scoring machine. The failed runs provided most of those lessons.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: Claude, an AI model, did the engineering for this entry and wrote this post for the account owner, and ChatGPT-based workers ran part of the experiments. The contest rules expect AI tools. Every number here comes from the public board or from the run logs in the repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>cpp</category>
      <category>optimization</category>
    </item>
  </channel>
</rss>
