<?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: Isnaini</title>
    <description>The latest articles on DEV Community by Isnaini (@isnakode).</description>
    <link>https://dev.to/isnakode</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%2F4189167%2Fd00bd0c7-bd9b-48b9-83f2-8ab6dbe8b4e2.jpg</url>
      <title>DEV Community: Isnaini</title>
      <link>https://dev.to/isnakode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isnakode"/>
    <language>en</language>
    <item>
      <title>Saku: How I Went From Per-Line KV to 8M Writes/sec in One Week (While Learning C++)</title>
      <dc:creator>Isnaini</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:28:02 +0000</pubDate>
      <link>https://dev.to/isnakode/saku-how-i-went-from-per-line-kv-to-8m-writessec-in-one-week-while-learning-c-1i07</link>
      <guid>https://dev.to/isnakode/saku-how-i-went-from-per-line-kv-to-8m-writessec-in-one-week-while-learning-c-1i07</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Initial scope was simple: just make it fast. No fsync, no fancy durability. Just raw speed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was bored. Asked AI "what should I build in C++?" It said "KV store". Okay, let's do it.&lt;/p&gt;

&lt;p&gt;This is the full story. Not a clean tutorial, just a war story of all the dumb decisions and tradeoffs I made in one week.&lt;/p&gt;




&lt;h3&gt;
  
  
  Day 1-2: Per-Line KV - 300k w/s, Slow as Hell
&lt;/h3&gt;

&lt;p&gt;The dumbest version: &lt;code&gt;key:value\n&lt;/code&gt; per line. Read with &lt;code&gt;fgets&lt;/code&gt;, scan from top to bottom.&lt;/p&gt;

&lt;p&gt;Slow. Want to read 1 key? You have to scan everything. O(n). 100MB file = pain.&lt;/p&gt;

&lt;p&gt;Lesson: need binary format, not text.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Binary Format
&lt;/h3&gt;

&lt;p&gt;Ended up with my own format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Checksum 4B][Status 1B][Type 1B][Key Len 1B][Value Len Varint][Key][Value]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some decisions that looked dumb but actually made sense:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Key Len limited to 256 (1 byte).&lt;/strong&gt;&lt;br&gt;
Why? Just an engine decision. I haven't found a use case where keys are 1KB long. If 1 byte is enough, why use 4 bytes? Save 3 bytes per record x 10 million = 30MB saved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Value Len was 32 bytes at first.&lt;/strong&gt;&lt;br&gt;
I wanted to support 500MB values. Super wasteful. A small 10-byte value had to carry a 32-byte header.&lt;/p&gt;

&lt;p&gt;Then I learned varint. Manipulate MSB as a continuation flag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Small (&amp;lt;127): 0xxxxxxx -&amp;gt; 1 byte
Large: 1xxxxxxx 0xxxxxxx -&amp;gt; 2 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From 32 bytes to 1 byte for small data. Heaven.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Checksum as a recovery mode.&lt;/strong&gt;&lt;br&gt;
I only knew the concept as "compare". At first it was reversed, then XOR, then CRC. Not just for validation, but to know which record is corrupted after a crash.&lt;/p&gt;
&lt;h3&gt;
  
  
  Day 3-4: Indexing - From Scanning to HashMap
&lt;/h3&gt;

&lt;p&gt;Still slow. Every read still scans the file.&lt;/p&gt;

&lt;p&gt;Solution: HashMap. But at first I stored &lt;code&gt;key -&amp;gt; value&lt;/code&gt; in the HashMap. RAM exploded.&lt;/p&gt;

&lt;p&gt;Changed to: &lt;code&gt;key -&amp;gt; offset&lt;/code&gt;. HashMap only holds the position in the file, not the value itself. Bitcask-style. RAM-friendly, read is just &lt;code&gt;seek(offset)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Day 4-5: I/O - Ring Buffer &amp;amp; Page Faults
&lt;/h3&gt;

&lt;p&gt;Reading per payload was still slow. One read = one syscall.&lt;/p&gt;

&lt;p&gt;Tried ring buffer: read per chunk, erase-read method. If payload is incomplete at the end of the chunk, re-read from the last payload offset.&lt;/p&gt;

&lt;p&gt;Still slow. Learned about 4KB page faults. Every 4KB, the OS has to load a new page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4KB chunk: thousands of syscalls&lt;/li&gt;
&lt;li&gt;8MB chunk: hundreds of syscalls&lt;/li&gt;
&lt;li&gt;16MB chunk: crazy fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why? Because 16MB = 4096 page faults at once, not one by one.&lt;/p&gt;

&lt;p&gt;This is what took me from 300k to 8M w/s (raw, no safety).&lt;/p&gt;
&lt;h3&gt;
  
  
  Day 6-7: mmap - The Final Cheat Code
&lt;/h3&gt;

&lt;p&gt;AI recommended: "try mmap".&lt;/p&gt;

&lt;p&gt;Mind blown. The file doesn't need to be read anymore. The file &lt;em&gt;is&lt;/em&gt; RAM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROT_READ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAP_PRIVATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more string allocations. Everything is a full ref &lt;code&gt;&amp;amp;[u8]&lt;/code&gt;. Zero-copy.&lt;/p&gt;

&lt;p&gt;Raw speed: &lt;strong&gt;8 million writes/sec&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But not safe. Added checksum + active flag for recovery:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final: 1.3 million writes/sec, 31μs read on a 612MB file (10 million records).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dropped from 8M? Yeah. But that's the safety tax. A race car without brakes is useless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tradeoffs I Intentionally Made
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;No fsync / flush.&lt;/strong&gt; On purpose. My scope was just speed. If I used fsync, 1.3M would become 10k because you have to wait for the disk to actually spin. Fsync can be added later as &lt;code&gt;Saku::flush()&lt;/code&gt; if needed.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;No compaction.&lt;/strong&gt; Append-only piles up garbage. Compaction needs file rewrite, would drop write speed to 200k. Let it pile up first, benchmark pure write path.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Key max 256 bytes.&lt;/strong&gt; Size tradeoff. If you need more, just change that 1 byte to varint too.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Final Numbers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write: 10,000,000 ops in 7.47s -&amp;gt; 1,337,999 writes/sec
Read: 612.6 MB file, key "example long key 9999999" -&amp;gt; 31μs
CPU: AMD Ryzen 7 5825U, 32GB RAM, Debian 13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/isnakode/saku" rel="noopener noreferrer"&gt;https://github.com/isnakode/saku&lt;/a&gt;&lt;br&gt;
&lt;code&gt;make bench N=10000000&lt;/code&gt; to try it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Actually Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AI gives you the map, but you're the one walking in the jungle. Segfaults at 2 AM can't be AI'd.&lt;/li&gt;
&lt;li&gt;Varint, checksum, mmap are not memorization, they are tradeoffs: size vs speed vs safety.&lt;/li&gt;
&lt;li&gt;Limiting to 1 byte is an engineering decision, not laziness.&lt;/li&gt;
&lt;li&gt;I went from &lt;code&gt;read() read() read()&lt;/code&gt; -&amp;gt; ring buffer -&amp;gt; mmap, and watched syscalls drop from thousands to hundreds. You can't learn that from docs, you feel it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cpp</category>
      <category>architecture</category>
      <category>storage</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
