<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Hash Table on KbWen Blog</title>
    <link>https://www.kbwen.com/tags/hash-table/</link>
    <description>Articles and notes on AI systems, machine learning, Python, data engineering, and software development.</description>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <image>
      <url>https://www.kbwen.com/images/og-default.png</url>
      <title>KbWen Blog</title>
      <link>https://www.kbwen.com/</link>
    </image>
    
    <lastBuildDate>Tue, 20 Oct 2026 10:05:00 +0800</lastBuildDate><atom:link href="https://www.kbwen.com/tags/hash-table/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>CPython&#39;s dict layout</title>
      <link>https://www.kbwen.com/how-cpython-dict-stays-compact-and-ordered/</link>
      <pubDate>Tue, 20 Oct 2026 10:05:00 +0800</pubDate><dc:creator>KbWen</dc:creator>
      <guid>https://www.kbwen.com/how-cpython-dict-stays-compact-and-ordered/</guid>
      <description>A look at how CPython lays out a dict in memory.</description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR</strong> A CPython dict keeps two arrays: a sparse hash table (<code>dk_indices</code>) holding only small integer indices, and a dense array (<code>dk_entries</code>) of records appended in insertion order. The layout arrived in Python 3.6 and made dicts 20–25% smaller; insertion order became part of the language spec in 3.7.</p>
</blockquote>
<p>Here are two dictionaries built from the same three keys in different orders:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">d1</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;b&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s2">&#34;c&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">d2</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;c&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="s2">&#34;b&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s2">&#34;a&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">d1</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s1">&#39;a&#39;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s1">&#39;b&#39;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s1">&#39;c&#39;</span><span class="p">:</span> <span class="mi">3</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">d2</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s1">&#39;c&#39;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="s1">&#39;b&#39;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s1">&#39;a&#39;</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">d1</span> <span class="o">==</span> <span class="n">d2</span>
</span></span><span class="line"><span class="cl"><span class="kc">True</span>
</span></span></code></pre></div><p>Each one prints in the order its keys went in, and iterating either gives that same order. The two are still equal, because equality compares contents and ignores order. A dict is a hash table, and a hash table scatters keys by their hash values, which have no relation to the order you added them. So the order on screen is coming from somewhere other than the hashing.</p>
<h2 id="the-old-layout">The old layout</h2>
<p>Before CPython 3.6, a dict didn&rsquo;t keep that order at all. The dict was a single sparse array of entry structs, each slot holding a hash, a key pointer, and a value pointer, roughly 24 bytes on a 64-bit build, and most of the slots sat empty, because a hash table needs slack to keep collisions rare. Iteration walked that array in slot order, which followed the hashes, so the order you got back had little to do with the order you put things in.</p>
<h2 id="two-arrays">Two arrays</h2>
<p>The compact dict, which landed in 3.6, splits that single array in two. The source comment in <code>Objects/dictobject.c</code> says: &ldquo;dk_indices is actual hashtable. It holds index in entries, or DKIX_EMPTY(-1) or DKIX_DUMMY(-2).&rdquo; So the hash table proper, <code>dk_indices</code>, no longer holds the records. It holds one small integer per slot: an index into a second array, or one of two negative markers for an empty or a deleted slot. The records live in that second array, <code>dk_entries</code>, a dense run of (hash, key, value) entries, and a new key is appended to the end of it.</p>
<p>A lookup still hashes into <code>dk_indices</code> to find a slot, reads the small integer sitting there, and follows it to the entry in <code>dk_entries</code>.</p>
<h2 id="order-and-memory">Order and memory</h2>
<p>The order comes from that split. Because entries are appended, <code>dk_entries</code> holds them in the order they were inserted, so reading it from the top gives the keys back in that order, as with <code>d1</code> and <code>d2</code> in the opening example. The source comment says: &ldquo;Since dk_entries is mostly append only, we can get insertion order by just iterating dk_entries.&rdquo;</p>
<p>Then the memory. The empty slots are now in <code>dk_indices</code>, where a slot is a single byte instead of a 24-byte struct; the width steps up with the table, one byte up to 128 slots, then two, four, and eight bytes as it grows past each threshold. The full entries are packed together with no gaps between them. The 3.6 What&rsquo;s New says the new dict&rsquo;s memory usage is &ldquo;between 20% and 25% smaller compared to Python 3.5&rdquo;. The design came from a proposal by Raymond Hettinger and was first implemented in PyPy.</p>
<p>In 3.6 the order was there, but not guaranteed. The 3.6 What&rsquo;s New says the order-preserving aspect is &ldquo;considered an implementation detail and should not be relied upon&rdquo;. Then in 3.7, &ldquo;the insertion-order preservation nature of dict objects has been declared to be an official part of the Python language spec&rdquo;.</p>
<h2 id="deletions">Deletions</h2>
<p>Deletion is why the comment says &ldquo;mostly&rdquo;. It does not shift the later entries down to close the gap; it sets that slot&rsquo;s index in <code>dk_indices</code> to the <code>DKIX_DUMMY</code> marker and clears the record in <code>dk_entries</code>, leaving a hole. A later insert still appends at the end rather than filling that hole, and the holes are reclaimed only when the dict resizes and repacks <code>dk_entries</code>. The comment also names <code>popitem()</code>, which takes from the end rather than appending. So the dense array grows by appending, with a few gaps left where things were removed, and the order you iterate is the order of whatever is still in it.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
