<?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: Milan Pramod</title>
    <description>The latest articles on DEV Community by Milan Pramod (@iammilan).</description>
    <link>https://dev.to/iammilan</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%2F3891504%2F16ce414e-771b-425f-8946-f612653d468f.PNG</url>
      <title>DEV Community: Milan Pramod</title>
      <link>https://dev.to/iammilan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iammilan"/>
    <language>en</language>
    <item>
      <title>Building a Lossless File Compressor from Scratch in C: My Journey with Shannon-Fano, Bitstreams, and Low-Level Performance</title>
      <dc:creator>Milan Pramod</dc:creator>
      <pubDate>Sun, 06 Sep 2026 16:46:09 +0000</pubDate>
      <link>https://dev.to/iammilan/building-a-lossless-file-compressor-from-scratch-in-c-my-journey-with-shannon-fano-bitstreams-43b0</link>
      <guid>https://dev.to/iammilan/building-a-lossless-file-compressor-from-scratch-in-c-my-journey-with-shannon-fano-bitstreams-43b0</guid>
      <description>&lt;p&gt;Like most developers, I’ve used compression tools like &lt;code&gt;gzip&lt;/code&gt;, &lt;code&gt;zip&lt;/code&gt;, and &lt;code&gt;tar&lt;/code&gt; for years without ever really understanding how they work under the hood. You pass a file in, run a command, and out comes a file half the size. It feels like magic.&lt;/p&gt;

&lt;p&gt;Recently, I decided to pull back the curtain and learn low-level systems programming by building a lossless compression tool from scratch in C&lt;/p&gt;

&lt;p&gt;The project is called &lt;strong&gt;&lt;code&gt;kmprs&lt;/code&gt;&lt;/strong&gt; (available on &lt;a href="https://github.com/shadowmkj/kmprs" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In this post, I want to share the full journey: how the Shannon-Fano algorithm works, the practical challenges of reading and writing arbitrary bits to disk, how an innocent-looking function call created a 100-million-operation bottleneck, and why building this from zero taught me more about computer architecture than any textbook.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea: What is Entropy Encoding?
&lt;/h2&gt;

&lt;p&gt;In standard ASCII or raw binary data, every single character occupies a fixed size of &lt;strong&gt;8 bits (1 byte)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Whether a byte is the letter &lt;code&gt;'e'&lt;/code&gt; (which appears thousands of times in English prose) or the character &lt;code&gt;'~'&lt;/code&gt; (which might appear once), they both take up 8 bits of disk space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'e' -&amp;gt; 01100101 (8 bits)
'~' -&amp;gt; 01111110 (8 bits)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Entropy encoding&lt;/strong&gt; flips this premise on its head:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assign &lt;strong&gt;short bit codes&lt;/strong&gt; (e.g., 2 to 4 bits) to frequently occurring symbols.&lt;/li&gt;
&lt;li&gt;Assign &lt;strong&gt;longer bit codes&lt;/strong&gt; (e.g., 9 to 14 bits) to rare symbols.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because common characters appear millions of times, the average number of bits per character drops well below 8, compressing the overall file size.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Shannon-Fano Algorithm Works
&lt;/h2&gt;

&lt;p&gt;In 1948, Claude Shannon and Robert Fano introduced one of the earliest statistical prefix-coding algorithms.&lt;/p&gt;

&lt;p&gt;The algorithm builds a binary prefix tree using a &lt;strong&gt;top-down recursive splitting&lt;/strong&gt; approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         [ All Symbols (Sum = 100) ]
                                  /       \
                       Split at ~50       Split at ~50
                               /             \
                   '0' [ Group A (52) ]   '1' [ Group B (48) ]
                         /        \             /        \
                    '0' [e (30)] '1' [t (22)] '0' [a (28)] '1' [z (20)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Step-by-Step Algorithm
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Count Frequencies:&lt;/strong&gt; Perform a first pass over the input file to build a histogram of all 256 possible byte values ($0$ to $255$).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter &amp;amp; Sort:&lt;/strong&gt; Collect all symbols with non-zero counts and sort them in descending order of frequency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive Partitioning:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Find a split index $k$ such that the sum of frequencies on the left is as close as possible to the sum on the right:
$$\left| \sum_{i=\text{start}}^{k} \text{freq}[i] - \sum_{i=k+1}^{\text{end}} \text{freq}[i] \right| \text{ is minimized}$$&lt;/li&gt;
&lt;li&gt;Assign bit &lt;code&gt;0&lt;/code&gt; to the left group and bit &lt;code&gt;1&lt;/code&gt; to the right group.&lt;/li&gt;
&lt;li&gt;Recursively split both halves until every group contains a single symbol.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate Prefix Codes:&lt;/strong&gt; Each symbol receives a unique binary code corresponding to its path from the root.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Prefix-Free Property
&lt;/h3&gt;

&lt;p&gt;A crucial rule in data compression is that &lt;strong&gt;no code can be a prefix of another code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, if &lt;code&gt;'e'&lt;/code&gt; is encoded as &lt;code&gt;01&lt;/code&gt;, no other character can start with &lt;code&gt;01&lt;/code&gt; (like &lt;code&gt;011&lt;/code&gt;). This guarantees that when the decompressor reads incoming bits sequentially, it can instantaneously and unambiguously decode each character without needing delimiter markers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reality Check: Building Bit-Level I/O
&lt;/h2&gt;

&lt;p&gt;The math of Shannon-Fano is simple on paper. But as soon as you sit down to implement it in C, you run into your first major hardware hurdle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Computers do not read or write individual bits.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The OS filesystem and CPU architecture work in chunks of bytes (8 bits), words (64 bits), and pages (4 KiB). If symbol &lt;code&gt;'e'&lt;/code&gt; has the 3-bit codeword &lt;code&gt;101&lt;/code&gt; and symbol &lt;code&gt;'t'&lt;/code&gt; has the 5-bit codeword &lt;code&gt;01100&lt;/code&gt;, how do you write 8 bits across arbitrary boundaries?&lt;/p&gt;

&lt;p&gt;To solve this, I had to build custom &lt;code&gt;BitWriter&lt;/code&gt; and &lt;code&gt;BitReader&lt;/code&gt; abstractions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The BitWriter Architecture
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;BitWriter&lt;/code&gt; uses a 64-bit integer as a &lt;strong&gt;bit accumulator&lt;/strong&gt; (reservoir). It packs variable-length bits into the accumulator and siphons off completed 8-bit bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BitWriter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;FILE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;     &lt;span class="c1"&gt;// 4 KiB block buffer&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;buffer_pos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// Cursor in buffer&lt;/span&gt;
    &lt;span class="kt"&gt;uint64_t&lt;/span&gt; &lt;span class="n"&gt;accumulator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// 64-bit temporary bit reservoir&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;bits_in_buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// Unwritten bits count (0 to 64)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;BitWriter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When writing a codeword of length $L$:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Left-shift the accumulator by $L$ bits.&lt;/li&gt;
&lt;li&gt;Bitwise-OR the new codeword into the lower $L$ bits.&lt;/li&gt;
&lt;li&gt;Increment &lt;code&gt;bits_in_buffer&lt;/code&gt; by $L$.&lt;/li&gt;
&lt;li&gt;Whenever &lt;code&gt;bits_in_buffer &amp;gt;= 8&lt;/code&gt;, extract the top 8 bits, store them in the output buffer, and decrement &lt;code&gt;bits_in_buffer&lt;/code&gt; by 8.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;bit_writer_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BitWriter&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint64_t&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;32U&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFFULL&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1ULL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1ULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;accumulator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;accumulator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bits_in_buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bits_in_buffer&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8U&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bits_in_buffer&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;8U&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="p"&gt;)((&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;accumulator&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bits_in_buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFU&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer_pos&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer_pos&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Designing the Binary Container Format (&lt;code&gt;.shn&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;A raw stream of compressed bits is useless by itself. When decompressing, the program needs to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is this actually a valid compressed file?&lt;/li&gt;
&lt;li&gt;What codebook was used to encode the file?&lt;/li&gt;
&lt;li&gt;How many uncompressed bytes should we restore? (Since the final byte in a bitstream often contains trailing zero padding).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I designed a binary container format with a fixed metadata header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------------------------------------------------+
| Magic Bytes: "\x7fSHN\x01" (4 bytes)                        |
+-------------------------------------------------------------+
| Original File Size: uint64_t (8 bytes, little-endian)       |
+-------------------------------------------------------------+
| Symbol Count: uint16_t (2 bytes)                            |
+-------------------------------------------------------------+
| Serialized Codebook Entries: [Symbol (1B) | Len (1B) | ...] |
+-------------------------------------------------------------+
| Compressed Bitstream Payload ...                            |
+-------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During decompression, &lt;code&gt;kmprs&lt;/code&gt; parses the header, reconstructs the Shannon-Fano binary decode tree, and reads bits from the bitstream to traverse the tree from root to leaf, emitting exact original characters until the original file byte count is reached.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 100-Million-Call Bottleneck &amp;amp; Optimization
&lt;/h2&gt;

&lt;p&gt;Once the compressor worked end-to-end and passed verification roundtrips, I benchmarked it on a &lt;strong&gt;100 MB test dataset&lt;/strong&gt; (&lt;code&gt;dummy.data&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The first implementation felt surprisingly sluggish (~1.33 seconds).&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding the Bottleneck
&lt;/h3&gt;

&lt;p&gt;In my initial prototype of &lt;code&gt;BitWriter&lt;/code&gt;, whenever 8 bits accumulated, I emitted the byte immediately using &lt;code&gt;fputc()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Naive unbuffered approach&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bits_in_buffer&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bits_in_buffer&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;accumulator&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bits_in_buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;fputc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// &amp;lt;-- PROBLEM!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a 100 MB file, this meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~100,000,000 function calls&lt;/strong&gt; into the C standard library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100,000,000 thread lock/unlock operations&lt;/strong&gt; (since standard libc file streams like &lt;code&gt;fputc&lt;/code&gt; acquire internal reentrant locks per call).&lt;/li&gt;
&lt;li&gt;Cache thrashing and function prologue/epilogue overhead inside the innermost encoding loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Two-Tier Solution
&lt;/h3&gt;

&lt;p&gt;I re-architected the I/O pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inlined Bit Packing:&lt;/strong&gt; Moved &lt;code&gt;bit_writer_write()&lt;/code&gt; to &lt;code&gt;bit_io.h&lt;/code&gt; as a &lt;code&gt;static inline&lt;/code&gt; function so the compiler could optimize the bit shifts directly inside the encoding loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4 KiB Block Buffer:&lt;/strong&gt; Accumulated completed bytes into an internal &lt;code&gt;uint8_t buffer[4096]&lt;/code&gt; array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk &lt;code&gt;fwrite()&lt;/code&gt;:&lt;/strong&gt; Only flushed to the OS stream once every 4,096 bytes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Benchmark Results
&lt;/h3&gt;

&lt;p&gt;Testing with &lt;a href="https://github.com/sharkdp/hyperfine" rel="noopener noreferrer"&gt;&lt;code&gt;hyperfine&lt;/code&gt;&lt;/a&gt; on the 100 MB test payload:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Mean Execution Time&lt;/th&gt;
&lt;th&gt;User CPU Time&lt;/th&gt;
&lt;th&gt;System Time&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Buffered BitWriter (4 KiB + inline)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;710.5 ms ± 18.0 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;625.7 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;79.6 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1.87x faster (2.0x CPU reduction)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unbuffered BitWriter (per-byte &lt;code&gt;fputc&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;1.327 s ± 0.002 s&lt;/td&gt;
&lt;td&gt;1.249 s&lt;/td&gt;
&lt;td&gt;74.0 ms&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajyvcf2pczzxach3hlue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajyvcf2pczzxach3hlue.png" alt="Benchmark" width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cutting execution time in half simply by buffering bytes and avoiding function call overhead in hot loops was a huge practical lesson.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Comparison: &lt;code&gt;kmprs&lt;/code&gt; vs &lt;code&gt;gzip&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When benchmarked against standard &lt;code&gt;gzip -kf dummy.data&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Mean Execution Time&lt;/th&gt;
&lt;th&gt;User Time&lt;/th&gt;
&lt;th&gt;Compressed Size&lt;/th&gt;
&lt;th&gt;Space Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;kmprs dummy.data&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;690.1 ms ± 4.6 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;620.1 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;53.25 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gzip -kf dummy.data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.684 s ± 0.007 s&lt;/td&gt;
&lt;td&gt;2.645 s&lt;/td&gt;
&lt;td&gt;59.72 MB&lt;/td&gt;
&lt;td&gt;43.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1zh44otyzwix1l4vcde.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1zh44otyzwix1l4vcde.png" alt="Benchmark vs Gzip" width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is &lt;code&gt;kmprs&lt;/code&gt; faster than &lt;code&gt;gzip&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kmprs&lt;/code&gt; performs a single frequency pass, builds a small 256-element tree, and directly streams bits through an inlined bit-reservoir. It does very little memory allocation and has minimal computational complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  But why is &lt;code&gt;gzip&lt;/code&gt; the better general-purpose compressor?
&lt;/h3&gt;

&lt;p&gt;This brings us to an important distinction in data compression theory:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Order-0 Entropy vs. Dictionary Compression:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kmprs&lt;/code&gt; only looks at individual byte frequencies (order-0 entropy). It cannot detect repeated phrases, patterns, or words.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gzip&lt;/code&gt; uses &lt;strong&gt;DEFLATE&lt;/strong&gt;, which combines &lt;strong&gt;LZ77&lt;/strong&gt; sliding-window dictionary matching with Huffman coding. When compressing source code, JSON, logs, or prose, LZ77 replaces entire repeated strings (like &lt;code&gt;"function"&lt;/code&gt; or &lt;code&gt;&amp;lt;div class="..."&amp;gt;&lt;/code&gt;) with tiny &lt;code&gt;(distance, length)&lt;/code&gt; tokens, achieving vastly superior compression ratios.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Shannon-Fano is Suboptimal Compared to Huffman:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shannon-Fano is a top-down greedy heuristic that divides probabilities in half. It does not guarantee the minimum possible expected code length.&lt;/li&gt;
&lt;li&gt;David Huffman later proved that a bottom-up priority-queue approach generates the mathematically optimal prefix codebook.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What Writing C in 2026 Taught Me
&lt;/h2&gt;

&lt;p&gt;Building a low-level tool in C is unforgiving, but modern tooling makes it a fantastic learning experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AddressSanitizer &amp;amp; UBSan (&lt;code&gt;-fsanitize=address,undefined&lt;/code&gt;):&lt;/strong&gt; Caught subtle bugs immediately, including a 32-bit shift overflow when masking 32-bit codewords (&lt;code&gt;1ULL &amp;lt;&amp;lt; 32&lt;/code&gt; vs &lt;code&gt;(length == 32) ? 0xFFFFFFFF : ...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clang-Tidy:&lt;/strong&gt; Enforced clean typing, explicit conversions, and consistent header hygiene across all compilation units.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Testing:&lt;/strong&gt; Writing unit tests for truncated headers, corrupt magic bytes, and single-byte edge cases caught bugs before they hit production.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; What's Next
&lt;/h2&gt;

&lt;p&gt;Taking a compression algorithm from theoretical pseudocode to a working, optimized CLI binary gave me a deep appreciation for systems programming. Concepts like bitwise operations, cache locality, branch predictability, and I/O buffer management went from abstract textbook ideas to tangible, measurable engineering realities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roadmap for &lt;code&gt;kmprs&lt;/code&gt;:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Table-Driven Multi-Bit Peek Decoder:&lt;/strong&gt; Accelerate decompression using an 8-bit lookup table ($O(1)$ symbol resolution).&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Canonical Huffman Coding:&lt;/strong&gt; Replace Shannon-Fano with true Huffman coding and pack headers using canonical code lengths.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;CRC32 Checksum Verification:&lt;/strong&gt; Add stream integrity verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like to check out the code, run the benchmarks, or contribute:&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/shadowmkj/kmprs" rel="noopener noreferrer"&gt;github.com/shadowmkj/kmprs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you ever built a compression tool or worked with bit-level I/O? What were your biggest takeaways? I’d love to hear your thoughts in the comments!&lt;/p&gt;

</description>
      <category>c</category>
      <category>performance</category>
      <category>programming</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Solve Leetcode without leaving your terminal.</title>
      <dc:creator>Milan Pramod</dc:creator>
      <pubDate>Wed, 22 Apr 2026 01:03:39 +0000</pubDate>
      <link>https://dev.to/iammilan/solve-leetcode-without-leaving-your-terminal-gil</link>
      <guid>https://dev.to/iammilan/solve-leetcode-without-leaving-your-terminal-gil</guid>
      <description>&lt;p&gt;I built a terminal-based UI for LeetCode that lets you browse problems, code solutions, run tests, and submit — all without leaving your terminal.&lt;/p&gt;

&lt;p&gt;I made it because I wanted a faster, more focused workflow without constantly switching between the browser, editor, and terminal. If you like working in the CLI, this might be useful.&lt;/p&gt;

&lt;p&gt;The project is open source, and I’d love your:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feedback on the UX and workflow
bug reports&lt;/li&gt;
&lt;li&gt;feature suggestions&lt;/li&gt;
&lt;li&gt;contributions from anyone interested in TUIs, developer tools, or LeetCode integrations
If you want to contribute, check out the repo here: &lt;a href="https://github.com/shadowmkj/leetrs" rel="noopener noreferrer"&gt;leetrs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>leetcode</category>
      <category>tui</category>
      <category>rust</category>
      <category>vim</category>
    </item>
  </channel>
</rss>
