<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://byroot.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://byroot.github.io/" rel="alternate" type="text/html" /><updated>2026-08-06T09:22:19+00:00</updated><id>https://byroot.github.io/feed.xml</id><title type="html">byroot’s blog</title><subtitle>Various ramblings.</subtitle><entry><title type="html">Shrinking Ruby Hashes</title><link href="https://byroot.github.io/ruby/performance/2026/08/05/shrinking-ruby-hashes.html" rel="alternate" type="text/html" title="Shrinking Ruby Hashes" /><published>2026-08-05T06:28:51+00:00</published><updated>2026-08-05T06:28:51+00:00</updated><id>https://byroot.github.io/ruby/performance/2026/08/05/shrinking-ruby-hashes</id><content type="html" xml:base="https://byroot.github.io/ruby/performance/2026/08/05/shrinking-ruby-hashes.html"><![CDATA[<p>As you may know, one area of Ruby performance optimization that particularly interests me is memory usage.
Given that most Ruby deployments rely on <code class="language-plaintext highlighter-rouge">fork</code>, improving Copy-on-Write performance is generally where you get the
biggest bang for your buck, but that only helps with the somewhat static part of an application heap.</p>

<p>A significant contributor to memory usage is also the transient memory that is allocated during a request or job
cycle and released soon after.
As such, it’s also interesting to keep an eye out for opportunities to make various Ruby objects smaller.</p>

<p>And the Ruby object type that’s probably the biggest contributor to memory usage is likely <code class="language-plaintext highlighter-rouge">Hash</code>.
<code class="language-plaintext highlighter-rouge">Hash</code> instances are extremely common in Ruby applications and libraries, from option hashes and keyword arguments
to logging and API responses.</p>

<p>They’re so convenient that they’re perhaps overused sometimes, especially since they’re really not very memory-efficient.</p>

<p>So let’s dig into how much memory they use, a bit of history of how we got there, and what we could do about it.</p>

<h3 id="measuring-memory-usage">Measuring Memory Usage</h3>

<p>If you’ve read some of my previous posts, you are probably already familiar with the Ruby APIs that allow digging into memory usage.</p>

<p>The simplest one is <code class="language-plaintext highlighter-rouge">ObjectSpace.memsize_of(obj)</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">Ruby</span><span class="o">::</span><span class="no">DESCRIPTION</span>
<span class="o">=&gt;</span> <span class="s2">"ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [arm64-darwin25]"</span>
<span class="o">&gt;&gt;</span> <span class="nb">require</span> <span class="s1">'objspace'</span>
<span class="o">&gt;&gt;</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({})</span>
<span class="o">=&gt;</span> <span class="mi">160</span>
</code></pre></div></div>

<p>So this tells us that an empty hash uses <code class="language-plaintext highlighter-rouge">160</code> bytes.
But without a comparison point, that doesn’t mean much, so let’s compare them to say, <code class="language-plaintext highlighter-rouge">Struct</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'objspace'</span>
<span class="nb">puts</span> <span class="s2">"Ruby: </span><span class="si">#{</span><span class="no">RUBY_VERSION</span><span class="si">}</span><span class="s2">"</span>

<span class="mi">11</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">size</span><span class="o">|</span>
  <span class="n">struct_class</span> <span class="o">=</span> <span class="n">size</span><span class="p">.</span><span class="nf">zero?</span> <span class="p">?</span> <span class="no">Object</span> <span class="p">:</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="o">*</span><span class="n">size</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="ss">:"m_</span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="ss">"</span> <span class="p">})</span>
  <span class="n">struct</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">(</span><span class="n">struct_class</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
  <span class="nb">hash</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">(</span><span class="no">Hash</span><span class="p">[</span><span class="n">size</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">i</span><span class="p">]</span> <span class="p">}])</span>
  <span class="n">diff</span> <span class="o">=</span> <span class="p">(</span><span class="nb">hash</span><span class="p">.</span><span class="nf">to_f</span> <span class="o">/</span> <span class="n">struct</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"size: </span><span class="si">#{</span><span class="n">size</span><span class="si">}</span><span class="s2"> </span><span class="se">\t</span><span class="s2">struct: </span><span class="si">#{</span><span class="n">struct</span><span class="si">}</span><span class="s2"> </span><span class="se">\t</span><span class="s2">hash: </span><span class="si">#{</span><span class="nb">hash</span><span class="si">}</span><span class="s2"> </span><span class="se">\t</span><span class="s2">diff: </span><span class="si">#{</span><span class="n">diff</span><span class="si">}</span><span class="s2">x"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Which gives us:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ruby 4.0.6
size:  0 	struct:  40 	hash: 160 	diff: 4.0x
size:  1 	struct:  40 	hash: 160 	diff: 4.0x
size:  2 	struct:  40 	hash: 160 	diff: 4.0x
size:  3 	struct:  40 	hash: 160 	diff: 4.0x
size:  4 	struct:  80 	hash: 160 	diff: 2.0x
size:  5 	struct:  80 	hash: 160 	diff: 2.0x
size:  6 	struct:  80 	hash: 160 	diff: 2.0x
size:  7 	struct:  80 	hash: 160 	diff: 2.0x
size:  8 	struct:  80 	hash: 160 	diff: 2.0x
size:  9 	struct: 160 	hash: 544 	diff: 3.4x
size: 10 	struct: 160 	hash: 544 	diff: 3.4x
</code></pre></div></div>

<p>As you can see, <code class="language-plaintext highlighter-rouge">Hash</code> uses 2 to 4 times as much memory as <code class="language-plaintext highlighter-rouge">Struct</code><sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> or a Plain Old Ruby Object (PORO).</p>

<p>I could almost say to stop using <code class="language-plaintext highlighter-rouge">Hash</code> when a PORO could do and leave it at that (and that would be good advice),
but the goal is to dig into why <code class="language-plaintext highlighter-rouge">Hash</code> uses so much memory and what we can do about it.</p>

<p>If you studied hash tables, that may seem like I’m stating the obvious here.
Of course, hash tables need more memory!</p>

<p>What may be less obvious is that in the above example, up until size 8, Ruby’s <code class="language-plaintext highlighter-rouge">Hash</code> instances aren’t exactly hash tables.</p>

<p>So let’s actually look at the implementation and its history to understand how we got here.</p>

<h3 id="open-addressing">Open Addressing</h3>

<p>The <code class="language-plaintext highlighter-rouge">Hash</code> implementation changed a lot over Ruby’s lifetime.</p>

<p>The first major change I remember was when the Hash-table implementation <a href="https://bugs.ruby-lang.org/issues/12142">was entirely rewritten by Vladimir Makarov for Ruby 2.4</a>.
It then changed from a more traditional design to <a href="https://en.wikipedia.org/wiki/Open_addressing">open-addressing</a>.
I’m not going to dig into the differences much, there are much better sources than me on that, but the thing I’ll point out, though,
is that while that change made hashes noticeably faster, it also significantly increased the “header” size.</p>

<p>By header, I mean the C struct that keeps track of the entries and bins.
Prior to the change, the <code class="language-plaintext highlighter-rouge">st_table</code> struct was <code class="language-plaintext highlighter-rouge">48B</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;limits.h&gt;</span><span class="cp">
</span>
<span class="k">struct</span> <span class="n">st_hash_type</span><span class="p">;</span>
<span class="k">struct</span> <span class="n">packed_entry</span><span class="p">;</span>
<span class="k">struct</span> <span class="n">st_table_entry</span><span class="p">;</span>

<span class="k">typedef</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span> <span class="n">st_index_t</span><span class="p">;</span>
<span class="cp">#define ST_INDEX_BITS (sizeof(st_index_t) * CHAR_BIT)
</span>
<span class="k">struct</span> <span class="n">st_table</span> <span class="p">{</span>
    <span class="k">const</span> <span class="k">struct</span> <span class="n">st_hash_type</span> <span class="o">*</span><span class="n">type</span><span class="p">;</span>
    <span class="n">st_index_t</span> <span class="n">num_bins</span><span class="p">;</span>
    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">entries_packed</span> <span class="o">:</span> <span class="mi">1</span><span class="p">;</span>
    <span class="n">st_index_t</span> <span class="n">num_entries</span> <span class="o">:</span> <span class="n">ST_INDEX_BITS</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">union</span> <span class="p">{</span>
      <span class="k">struct</span> <span class="p">{</span>
          <span class="k">struct</span> <span class="n">st_table_entry</span> <span class="o">**</span><span class="n">bins</span><span class="p">;</span>
          <span class="kt">void</span> <span class="o">*</span><span class="n">private_list_head</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
      <span class="p">}</span> <span class="n">big</span><span class="p">;</span>
      <span class="k">struct</span> <span class="p">{</span>
          <span class="k">struct</span> <span class="n">st_packed_entry</span> <span class="o">*</span><span class="n">entries</span><span class="p">;</span>
          <span class="n">st_index_t</span> <span class="n">real_entries</span><span class="p">;</span>
      <span class="p">}</span> <span class="n">packed</span><span class="p">;</span>
    <span class="p">}</span> <span class="n">as</span><span class="p">;</span>
<span class="p">};</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"sizeof(struct st_table) = %ld</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="k">struct</span> <span class="n">st_table</span><span class="p">));</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sizeof(struct st_table) = 48
</code></pre></div></div>

<p>In Vladimir’s initial patch, that struct grew to <code class="language-plaintext highlighter-rouge">88B</code>, but after some improvements, when the patch actually
landed, it only grew to <code class="language-plaintext highlighter-rouge">64B</code>, and then <a href="https://github.com/ruby/ruby/commit/5714a26b90b40846733fb2a5764d4c61285f5ea1">in a follow-up commit</a>,
Nobu shrunk it down further to <code class="language-plaintext highlighter-rouge">56B</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span>
<span class="k">struct</span> <span class="n">st_hash_type</span><span class="p">;</span>
<span class="k">struct</span> <span class="n">st_table_entry</span><span class="p">;</span>

<span class="k">typedef</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span> <span class="n">st_index_t</span><span class="p">;</span>

<span class="k">struct</span> <span class="n">st_table</span> <span class="p">{</span>
    <span class="cm">/* Cached features of the table -- see st.c for more details.  */</span>
    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">entry_power</span><span class="p">,</span> <span class="n">bin_power</span><span class="p">,</span> <span class="n">size_ind</span><span class="p">;</span>
    <span class="cm">/* How many times the table was rebuilt.  */</span>
    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">rebuilds_num</span><span class="p">;</span>
    <span class="k">const</span> <span class="k">struct</span> <span class="n">st_hash_type</span> <span class="o">*</span><span class="n">type</span><span class="p">;</span>
    <span class="cm">/* Number of entries currently in the table.  */</span>
    <span class="n">st_index_t</span> <span class="n">num_entries</span><span class="p">;</span>
    <span class="cm">/* Array of bins used for access by keys.  */</span>
    <span class="n">st_index_t</span> <span class="o">*</span><span class="n">bins</span><span class="p">;</span>
    <span class="cm">/* Start and bound index of entries in array entries.
       entries_starts and entries_bound are in interval
       [0,allocated_entries].  */</span>
    <span class="n">st_index_t</span> <span class="n">entries_start</span><span class="p">,</span> <span class="n">entries_bound</span><span class="p">;</span>
    <span class="cm">/* Array of size 2^entry_power.  */</span>
    <span class="k">struct</span> <span class="n">st_table_entry</span> <span class="o">*</span><span class="n">entries</span><span class="p">;</span>
<span class="p">};</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"sizeof(struct st_table) = %ld</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="k">struct</span> <span class="n">st_table</span><span class="p">));</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sizeof(struct st_table) = 56
</code></pre></div></div>

<p>Still an extra <code class="language-plaintext highlighter-rouge">16B</code> of baseline usage, yet, since the new implementation used some smart, dynamically sized integer offsets.
So, overall, small Hash memory usage was mostly decreased.</p>

<p>Back in Ruby 2.3, it was:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ruby 2.3.8
size: 0 	struct:  40 	hash:  40 	diff: 1.0x
size: 1 	struct:  40 	hash: 232 	diff: 5.8x
size: 2 	struct:  40 	hash: 232 	diff: 5.8x
size: 3 	struct:  40 	hash: 232 	diff: 5.8x
size: 4 	struct:  72 	hash: 232 	diff: 3.2x
size: 5 	struct:  80 	hash: 232 	diff: 2.9x
size: 6 	struct:  88 	hash: 232 	diff: 2.6x
size: 7 	struct:  96 	hash: 552 	diff: 5.8x
size: 8 	struct: 104 	hash: 600 	diff: 5.8x
size: 9 	struct: 112 	hash: 648 	diff: 5.8x
size: 10 	struct: 120 	hash: 696 	diff: 5.8x
</code></pre></div></div>

<p>And in 2.4:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ruby: 2.4.10
size: 0 	struct:  40 	hash:  40 	diff: 1.0x
size: 1 	struct:  40 	hash: 192 	diff: 4.8x
size: 2 	struct:  40 	hash: 192 	diff: 4.8x
size: 3 	struct:  40 	hash: 192 	diff: 4.8x
size: 4 	struct:  72 	hash: 192 	diff: 2.7x
size: 5 	struct:  80 	hash: 288 	diff: 3.6x
size: 6 	struct:  88 	hash: 288 	diff: 3.3x
size: 7 	struct:  96 	hash: 288 	diff: 3.0x
size: 8 	struct: 104 	hash: 288 	diff: 2.8x
size: 9 	struct: 112 	hash: 480 	diff: 4.3x
size: 10 	struct: 120 	hash: 480 	diff: 4.0x
</code></pre></div></div>

<p>A very positive change, but that’s still, a lot of memory for a few keys.</p>

<h3 id="array-tables">Array Tables</h3>

<p>The next major change to hashes happened in Ruby 2.6.
Yimin Zhao, a Google Summer of Code student mentored by Koichi Sasada worked on adapting hashes to work with <a href="https://bugs.ruby-lang.org/issues/14858">the “transient heap”</a>.
The ticket description explains the concept pretty well, but in short, the idea is that since “most objects die young”,
and that at the time, most Ruby objects would allocate extra memory with <code class="language-plaintext highlighter-rouge">malloc</code>. As such, allowing young objects to allocate memory
from a simple bump pointer allocator would speed things up.</p>

<p>Then at the end of GC marking, any surviving object pointing at memory allocated in the transient heap would reallocate it using the real <code class="language-plaintext highlighter-rouge">malloc</code> and copy the bytes over.
After that, the entire transient heap could be reset and re-used for free.
This is very much inspired by copying garbage collectors, but adapted for the Ruby VM of the time.</p>

<p>It’s in that context that Yimin Zhao started working on <a href="https://bugs.ruby-lang.org/issues/14989">integrating the transient heap with Ruby’s Hash</a>,
the problem, though, was that <code class="language-plaintext highlighter-rouge">st_table</code> isn’t the backing structure for the <code class="language-plaintext highlighter-rouge">Hash</code> class. It is also used as a generic hash table
across the virtual machine, and is even exposed in Ruby’s C API, so some extensions make use of it.</p>

<p>As such, refactoring it to use the transient heap would have been very tricky.
Instead, Yimin and Koichi took another approach.
They instrumented various benchmarks and <a href="https://docs.google.com/spreadsheets/d/1xAjO_qb5K49aLnvk8SypGwO5Avtbm2X12cYb1d-n6Xs/edit?gid=0#gid=0">saw that 80% of the hashes had 8 entries or less</a>.
For small tables like this, you don’t necessarily need a real hash table, even a linear search of <code class="language-plaintext highlighter-rouge">O(N)</code> complexity
can beat a <code class="language-plaintext highlighter-rouge">O(1)</code> hash-table lookup when <code class="language-plaintext highlighter-rouge">N</code> is small enough.</p>

<p>Based on that idea, they refactored Ruby’s <code class="language-plaintext highlighter-rouge">Hash</code> class to essentially be an Array up to 8 entries.
Now instead of always being backed by an <code class="language-plaintext highlighter-rouge">st_table</code>, <a href="https://github.com/ruby/ruby/commit/8f675cdd00e2c5b5a0f143f5e508dbbafdb20ccd#diff-d5867d8e382e49f5cdef27a4d24c1a4588954f96e00925092a586659bf1b1ba4R743-R744">hashes up to size 8 were now backed by an <code class="language-plaintext highlighter-rouge">li_table</code></a>,
for “linear table”, <a href="https://github.com/ruby/ruby/commit/e4c79d0d10429ac7d48641a66091f0292d807a9d">later renamed <code class="language-plaintext highlighter-rouge">ar_table</code></a> for “array table”.</p>

<p>At that point, each entry would be composed of a key, a value, and a recorded hashcode, for a total of <code class="language-plaintext highlighter-rouge">24B</code> per entry, so <code class="language-plaintext highlighter-rouge">192B</code> plus
the base <code class="language-plaintext highlighter-rouge">40B</code> overhead for all objects, so <code class="language-plaintext highlighter-rouge">232B</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ruby: 2.6.10
size: 0 	struct: 40 	hash: 232 	diff: 5.8x
size: 1 	struct: 40 	hash: 232 	diff: 5.8x
size: 2 	struct: 40 	hash: 232 	diff: 5.8x
size: 3 	struct: 40 	hash: 232 	diff: 5.8x
size: 4 	struct: 72 	hash: 232 	diff: 3.2x
size: 5 	struct: 80 	hash: 232 	diff: 2.9x
size: 6 	struct: 88 	hash: 232 	diff: 2.6x
size: 7 	struct: 96 	hash: 232 	diff: 2.4x
size: 8 	struct: 104 	hash: 232 	diff: 2.2x
size: 9 	struct: 112 	hash: 928 	diff: 8.3x
size: 10 	struct: 120 	hash: 928 	diff: 7.7x
</code></pre></div></div>

<p>So small hashes became bigger again.</p>

<h3 id="lower-byte-hashcode">Lower Byte Hashcode</h3>

<p>But thankfully, in Ruby 2.7, <a href="https://bugs.ruby-lang.org/issues/15602">Koichi managed to shrink them again</a> using a neat trick.
Instead of storing one <code class="language-plaintext highlighter-rouge">8B</code> hash code per entry, he changed it to only store the lower byte of the hash code.
The tradeoff is that this increases the collision probability to <code class="language-plaintext highlighter-rouge">0.39%</code> (<code class="language-plaintext highlighter-rouge">1/256</code>), but for such small tables,
that is likely already good enough.
The added upside is that it makes scanning the table much faster, and that it saves <code class="language-plaintext highlighter-rouge">7B</code> per entry, so <code class="language-plaintext highlighter-rouge">56B</code> per table,
quite a sizeable saving:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ruby: 2.7.8
size: 0 	struct: 40 	hash: 40 	diff: 1.0x
size: 1 	struct: 40 	hash: 168 	diff: 4.2x
size: 2 	struct: 40 	hash: 168 	diff: 4.2x
size: 3 	struct: 40 	hash: 168 	diff: 4.2x
size: 4 	struct: 72 	hash: 168 	diff: 2.3x
size: 5 	struct: 80 	hash: 168 	diff: 2.1x
size: 6 	struct: 88 	hash: 168 	diff: 1.9x
size: 7 	struct: 96 	hash: 168 	diff: 1.8x
size: 8 	struct: 104 	hash: 168 	diff: 1.6x
size: 9 	struct: 112 	hash: 928 	diff: 8.3x
size: 10 	struct: 120 	hash: 928 	diff: 7.7x
</code></pre></div></div>

<h3 id="variable-width-allocation">Variable Width Allocation</h3>

<p>The next notable change after was the generalization of Variable Width Allocation (VWA) in Ruby 3.3<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>.
Prior to it, all Ruby objects were allocated as a fixed-size <code class="language-plaintext highlighter-rouge">40B</code> slot, and if they needed to store
anything more, each object would have to allocate more using the system <code class="language-plaintext highlighter-rouge">malloc</code>.</p>

<p>VWA changed that to allow multiple different sizes, all powers of <code class="language-plaintext highlighter-rouge">40</code>, so <code class="language-plaintext highlighter-rouge">40</code>, <code class="language-plaintext highlighter-rouge">80</code>, <code class="language-plaintext highlighter-rouge">160</code>, <code class="language-plaintext highlighter-rouge">320</code> and <code class="language-plaintext highlighter-rouge">640</code>,
which is perfect for small hashes backed by <code class="language-plaintext highlighter-rouge">ar_table</code>.</p>

<p>Instead of the <code class="language-plaintext highlighter-rouge">ar_table</code> being stored in a buffer allocated with <code class="language-plaintext highlighter-rouge">malloc</code>, it could now be stored inline in the slot,
saving the pointer that used to point at the <code class="language-plaintext highlighter-rouge">malloc</code> buffer, making it exactly <code class="language-plaintext highlighter-rouge">160B</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ruby: 3.3.7
size: 0 	struct: 40 	hash: 160 	diff: 4.0x
size: 1 	struct: 40 	hash: 160 	diff: 4.0x
size: 2 	struct: 40 	hash: 160 	diff: 4.0x
size: 3 	struct: 40 	hash: 160 	diff: 4.0x
size: 4 	struct: 80 	hash: 160 	diff: 2.0x
size: 5 	struct: 80 	hash: 160 	diff: 2.0x
size: 6 	struct: 80 	hash: 160 	diff: 2.0x
size: 7 	struct: 80 	hash: 160 	diff: 2.0x
size: 8 	struct: 80 	hash: 160 	diff: 2.0x
size: 9 	struct: 160 	hash: 544 	diff: 3.4x
size: 10 	struct: 160 	hash: 544 	diff: 3.4x
</code></pre></div></div>

<p>In reality, this saved a bit more than <code class="language-plaintext highlighter-rouge">8B</code> per hash, because <code class="language-plaintext highlighter-rouge">ObjectSpace.memsize_of</code> only reports the amount of memory that was requested by Ruby to <code class="language-plaintext highlighter-rouge">malloc</code>.
Various allocators have different strategies, but in general, there is some memory lost because of padding, and also some extra metadata the allocator has to keep.
So it’s safe to assume the saving was in reality at least <code class="language-plaintext highlighter-rouge">24B</code> or even <code class="language-plaintext highlighter-rouge">32B</code> per hash.</p>

<p>The additional benefit is that previously, when the GC would find an unused Hash, it would need to call <code class="language-plaintext highlighter-rouge">free()</code> to release the allocated memory.
With VWA, since there’s no external buffer, the GC can just mark that slot as free and move on, which is dramatically faster.</p>

<p>With <code class="language-plaintext highlighter-rouge">Hash</code> and many other types being refactored to use Variable Width Allocation, the transient heap became mostly useless
and was <a href="https://bugs.ruby-lang.org/issues/19730">entirely removed by Peter Zhu in Ruby 3.3</a>.</p>

<p>One notable downside, though, is that empty hashes are now 4 times bigger than they used to be, as they always reserve
a slot big enough to fit an <code class="language-plaintext highlighter-rouge">ar_table</code>. Gain some, lose some…</p>

<h3 id="going-smaller">Going Smaller</h3>

<p>Recently, I started thinking whether we could make these small hashes even more compact.
In theory, <code class="language-plaintext highlighter-rouge">Hash</code> instances have a base footprint of <code class="language-plaintext highlighter-rouge">24B</code>: <code class="language-plaintext highlighter-rouge">16B</code> for the generic Ruby object header,
and<code class="language-plaintext highlighter-rouge">8B</code> for the default value (called <code class="language-plaintext highlighter-rouge">ifnone</code>).</p>

<p>Then hashes backed by an <code class="language-plaintext highlighter-rouge">ar_table</code> need an extra fixed <code class="language-plaintext highlighter-rouge">8B</code> to store the hash code.</p>

<p>From there, each entry in the hash needs <code class="language-plaintext highlighter-rouge">16B</code>: <code class="language-plaintext highlighter-rouge">8B</code> for the key reference, and <code class="language-plaintext highlighter-rouge">8B</code> for the value reference.
So the theoretical memory footprint of small hashes should be <code class="language-plaintext highlighter-rouge">32 + size * 2</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ruby: imaginary version
size: 0 	struct: 40 	hash:  32
size: 1 	struct: 40 	hash:  48
size: 2 	struct: 40 	hash:  64
size: 3 	struct: 40 	hash:  80
size: 4 	struct: 80 	hash:  96
size: 5 	struct: 80 	hash: 112
size: 6 	struct: 80 	hash: 128
size: 7 	struct: 80 	hash: 144
size: 8 	struct: 80 	hash: 160
size: 9 	struct: 160 	hash: 544
size: 10 	struct: 160 	hash: 544
</code></pre></div></div>

<p>But of course it’s not that easy because of two limitations.</p>

<p>First, we have to round up to a size offered by the GC (<code class="language-plaintext highlighter-rouge">40</code>, <code class="language-plaintext highlighter-rouge">80</code>, <code class="language-plaintext highlighter-rouge">160</code>, …).</p>

<p>Second, since you can append keys to a hash, it needs to be able to transition into an <code class="language-plaintext highlighter-rouge">st_table</code> if it runs out of space,
and as mentioned previously, a <code class="language-plaintext highlighter-rouge">st_table</code> is <code class="language-plaintext highlighter-rouge">56B</code>, so <code class="language-plaintext highlighter-rouge">24 + 56 =&gt; 80</code>, a Hash instance can’t possibly be smaller than 80B.</p>

<p>But is that really the case?</p>

<h3 id="knock-on-effects">Knock-On Effects</h3>

<p>Optimizations are fundamentally an iterative process.
An optimization that is impossible or too complex one day can become possible or even easy as the surrounding environment changes, and recently, quite a few things have changed in Ruby.</p>

<p>First, Ruby <code class="language-plaintext highlighter-rouge">4.1.0dev</code> now has more fine-grained slot sizes.
In addition to the existing power of <code class="language-plaintext highlighter-rouge">40</code> sizes, <a href="https://github.com/ruby/ruby/pull/16282">thanks to Matthew Valentine-House</a>,
it now offers powers of <code class="language-plaintext highlighter-rouge">32</code> all the way up to <code class="language-plaintext highlighter-rouge">1024</code>, allowing us to be much closer to the theoretical ideal I listed above.</p>

<p>The second recent change, I already mentioned in passing on this blog when I talked about a generic instance variable,
and how I shrunk the <code class="language-plaintext highlighter-rouge">set_table</code> struct by <code class="language-plaintext highlighter-rouge">8B</code>, well <code class="language-plaintext highlighter-rouge">set_table</code> is essentially a copy-paste of <code class="language-plaintext highlighter-rouge">st_table</code>, so I applied
the same optimization to it so they’d stay in sync, meaning <code class="language-plaintext highlighter-rouge">st_table</code> since Ruby 4.0 is actually <code class="language-plaintext highlighter-rouge">48B</code>.
Hence, if we could shave off another <code class="language-plaintext highlighter-rouge">8B</code>, <code class="language-plaintext highlighter-rouge">st_table</code> backed hashes could use a <code class="language-plaintext highlighter-rouge">64B</code> slot, which in turn would allow to
optimistically allocate <code class="language-plaintext highlighter-rouge">ar_table</code> backed hashes with only a capacity of 2, and they’d still be able to transition to an <code class="language-plaintext highlighter-rouge">st_table</code>
if needed.</p>

<p>And a third recent change in <code class="language-plaintext highlighter-rouge">4.1.0dev</code> is that <a href="https://github.com/ruby/ruby/pull/17572">Peter Zhu made a significant change to object shapes</a>,
so that now they always include the size of the slot, directly in the object itself.
It’s a change I wanted to make for a long time, because while it has always been possible to query an object’s slot size,
up until that refactoring, it was quite costly, hence it was better avoided in hotspots.
That being said, strings would frequently go through that codepath, but thanks to Peter’s change,
<a href="https://github.com/ruby/ruby/pull/17730">I was able to get rid of that and get a nice 8% speedup on micro-benchmarks</a>.</p>

<h3 id="data-locality">Data Locality</h3>

<p>The reason querying an object’s slot size was slow was mostly due to data locality.
The slot size was stored in the page metadata, and to get there, you first needed to find its address at the beginning
of the page.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"># gc/default.c
</span>
<span class="k">struct</span> <span class="n">heap_page_header</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">heap_page</span> <span class="o">*</span><span class="n">page</span><span class="p">;</span>
<span class="p">};</span>

<span class="k">struct</span> <span class="n">heap_page</span> <span class="p">{</span>
    <span class="c1">// snip...</span>
    <span class="kt">uint64_t</span> <span class="n">slot_size_reciprocal</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The problem is that processors have become incredibly fast at crunching data, but memory latency hasn’t progressed
as rapidly.
So when the processor needs to read data that isn’t already in its cache, it has to stall for what is comparatively a very long time.</p>

<p>Wheras now that the information is inside the object header, it is almost guaranteed to already be in the processor cache,
given CPU cache lines are generally 64B (<code class="language-plaintext highlighter-rouge">x86_64</code>) or <code class="language-plaintext highlighter-rouge">128B</code> (Apple Silicon).
Making it practically free to access because we most likely already read some other information that is stored right next to it, like the object’s type.</p>

<p>But that’s enough of a digression.</p>

<h3 id="immutability">Immutability</h3>

<p>The first idea I had to start tackling this problem with baby steps was that, given that the main blocker for allocating
hashes in smaller GC slots was the risk that they may need to grow, why not start with frozen hashes?</p>

<p>Since they’re immutable, there are no concerns whatsoever that they may run out of space.
The downside of course is that they’re not quite as frequent, but they’re probably still more frequent than you think.</p>

<p>For instance:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">some_method</span><span class="p">({</span> <span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span> <span class="p">})</span>
</code></pre></div></div>

<p>In that example, the hash passed as an argument is mutable, so we can’t optimize it.
However, there is a second, “hidden” hash in that snippet of code that you’re probably not seeing, and it is frozen.</p>

<p>Let’s disassemble this code:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">puts</span> <span class="no">RubyVM</span><span class="o">::</span><span class="no">InstructionSequence</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="s1">'some_method({ a: 1, b: 2 })'</span><span class="p">).</span><span class="nf">disasm</span>
<span class="o">==</span> <span class="ss">disasm: </span><span class="c1">#&lt;ISeq:&lt;compiled&gt;@&lt;compiled&gt;:1 (1,0)-(1,27)&gt;</span>
<span class="mo">0000</span> <span class="n">putself</span>                                                          <span class="p">(</span>   <span class="mi">1</span><span class="p">)[</span><span class="no">Li</span><span class="p">]</span>
<span class="mo">0001</span> <span class="n">duphash</span>                                <span class="p">{</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">}</span>
<span class="mo">0003</span> <span class="n">opt_send_without_block</span>                 <span class="o">&lt;</span><span class="n">calldata!mid</span><span class="ss">:some_method</span><span class="p">,</span> <span class="n">argc</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span> <span class="no">FCALL</span><span class="o">|</span><span class="no">ARGS_SIMPLE</span><span class="o">&gt;</span>
<span class="mo">0005</span> <span class="n">leave</span>
</code></pre></div></div>

<p>As you can see, Ruby generated bytecode with the <code class="language-plaintext highlighter-rouge">duphash</code> instruction.
As its name suggests, it makes a copy (<code class="language-plaintext highlighter-rouge">dup</code>) of an already existing hash, and this hash is hidden from user space.
You can never get a reference to it, even with <code class="language-plaintext highlighter-rouge">ObjectSpace.each_object</code>.
The best you can do is somehow find it in the <code class="language-plaintext highlighter-rouge">ObjectSpace.dump_all</code> output.</p>

<p>Another case where such optimization could be performed would be literal hashes that are explicitly frozen, such as:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">DEFAULT_OPTIONS</span> <span class="o">=</span> <span class="p">{</span>
  <span class="ss">host: </span><span class="s2">"localhost"</span><span class="p">,</span>
  <span class="ss">port: </span><span class="mi">3456</span><span class="p">,</span>
<span class="p">}.</span><span class="nf">freeze</span>
</code></pre></div></div>

<p>If we disassemble that expression:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">RubyVM</span><span class="o">::</span><span class="no">InstructionSequence</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="s1">'{ a: 1, b: 2 }.freeze'</span><span class="p">).</span><span class="nf">disasm</span>
<span class="o">==</span> <span class="ss">disasm: </span><span class="c1">#&lt;ISeq:&lt;compiled&gt;@&lt;compiled&gt;:1 (1,0)-(1,21)&gt;</span>
<span class="mo">0000</span> <span class="n">opt_hash_freeze</span>                        <span class="p">{</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">},</span> <span class="o">&lt;</span><span class="n">calldata!mid</span><span class="ss">:freeze</span><span class="p">,</span> <span class="n">argc</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span> <span class="no">ARGS_SIMPLE</span><span class="o">&gt;</span><span class="p">(</span>   <span class="mi">1</span><span class="p">)[</span><span class="no">Li</span><span class="p">]</span>
<span class="mo">0003</span> <span class="n">leave</span>
</code></pre></div></div>

<p>We can see the <code class="language-plaintext highlighter-rouge">opt_hash_freeze</code> instruction being used.
That’s <a href="https://bugs.ruby-lang.org/issues/20684">an optimization I added two years ago with Étienne Barrié</a>, similar
in concept to frozen string literals.
When the compiler sees that you are immediately freezing an entirely literal hash, instead of making a copy of the hidden
hash and then freezing it, it “reveals” the hidden hash, and pushes it on the stack directly with no zero allocations nor copies.</p>

<p>Of course, I wouldn’t expect even these two cases to amount to a massive amount of memory, but in my view, even a small gain is welcome,
granted it is easy to implement.
But more importantly, starting with frozen hashes would be a good way to validate the idea and start refactoring the code
to no longer assume all <code class="language-plaintext highlighter-rouge">ar_table</code> have a fixed size, before moving to harder changes.</p>

<p><a href="https://github.com/ruby/ruby/pull/16653">The patch itself was relatively simple</a>, but I merged it before the new <code class="language-plaintext highlighter-rouge">32</code> based
pool sizes were introduced.
Running the associated benchmark on the current Ruby <code class="language-plaintext highlighter-rouge">4.1.0dev</code> now shows that frozen hashes
can be as small as <code class="language-plaintext highlighter-rouge">64B</code> or even <code class="language-plaintext highlighter-rouge">32B</code> for empty hashes:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"objspace"</span>

<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 32</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 64</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 64</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">c: </span><span class="mi">3</span><span class="p">}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 80</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">c: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">d: </span><span class="mi">4</span><span class="p">}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 96</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">c: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">d: </span><span class="mi">4</span><span class="p">,</span> <span class="ss">e: </span><span class="mi">5</span><span class="p">,</span> <span class="p">}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 128</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">c: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">d: </span><span class="mi">4</span><span class="p">,</span> <span class="ss">e: </span><span class="mi">5</span><span class="p">,</span> <span class="ss">f: </span><span class="mi">6</span><span class="p">}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 128</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">c: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">d: </span><span class="mi">4</span><span class="p">,</span> <span class="ss">e: </span><span class="mi">5</span><span class="p">,</span> <span class="ss">f: </span><span class="mi">6</span><span class="p">,</span> <span class="ss">g: </span><span class="mi">7</span><span class="p">}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 160</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">c: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">d: </span><span class="mi">4</span><span class="p">,</span> <span class="ss">e: </span><span class="mi">5</span><span class="p">,</span> <span class="ss">f: </span><span class="mi">6</span><span class="p">,</span> <span class="ss">g: </span><span class="mi">7</span><span class="p">,</span> <span class="ss">h: </span><span class="mi">8</span><span class="p">}.</span><span class="nf">freeze</span><span class="p">)</span> <span class="c1"># =&gt; 160</span>
</code></pre></div></div>

<h3 id="struct-packing">Struct Packing</h3>

<p>Now, if I wanted to allow the same optimization for mutable hashes, I needed to shrink <code class="language-plaintext highlighter-rouge">st_table</code> further so
that <code class="language-plaintext highlighter-rouge">st_table</code> backed hashes would also fit in <code class="language-plaintext highlighter-rouge">64B</code> slots, otherwise we could never cross the <code class="language-plaintext highlighter-rouge">80B</code> barrier,
making the optimization much less interesting.</p>

<p>As mentioned before, they were already theoretically <code class="language-plaintext highlighter-rouge">72B</code>, so all I needed to find was <code class="language-plaintext highlighter-rouge">8B</code>, and I had a few different ideas.
Let’s look at the structs again:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">RHash</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">RBasic</span> <span class="n">basic</span><span class="p">;</span>    <span class="c1">// 16B</span>
    <span class="k">const</span> <span class="n">VALUE</span> <span class="n">ifnone</span><span class="p">;</span>     <span class="c1">// 8B</span>
<span class="p">};</span>

<span class="k">struct</span> <span class="n">st_table</span> <span class="p">{</span>
    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">entry_power</span><span class="p">,</span> <span class="n">bin_power</span><span class="p">,</span> <span class="n">size_ind</span><span class="p">;</span> <span class="c1">// 1B each, so 3B</span>
    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">rebuilds_num</span><span class="p">;</span>                      <span class="c1">// 4B</span>
    <span class="k">const</span> <span class="k">struct</span> <span class="n">st_hash_type</span> <span class="o">*</span><span class="n">type</span><span class="p">;</span>                <span class="c1">// 8B</span>
    <span class="n">st_index_t</span> <span class="n">num_entries</span><span class="p">;</span>                         <span class="c1">// 8B</span>
    <span class="n">st_index_t</span> <span class="n">entries_start</span><span class="p">,</span> <span class="n">entries_bound</span><span class="p">;</span>        <span class="c1">// 8B each, so 16B</span>
    <span class="k">struct</span> <span class="n">st_table_entry</span> <span class="o">*</span><span class="n">entries</span><span class="p">;</span>                 <span class="c1">// 8B</span>
<span class="p">};</span>
</code></pre></div></div>

<p>My first idea was to move the hash default value (<code class="language-plaintext highlighter-rouge">ifnone</code>) elsewhere.
Very few hashes have a default value, so I kind of find this wasteful to have this <code class="language-plaintext highlighter-rouge">8B</code> overhead in all hashes.</p>

<p>One possibility would have been to make it an instance variable of the hash, but as I explained in
<a href="/ruby/performance/2025/08/11/unlocking-ractors-generic-variables.html">my previous post about generic instance variables</a>,
the instance variables of most types, like <code class="language-plaintext highlighter-rouge">Hash</code>, are stored in a global table that requires synchronization between
ractors, so turning <code class="language-plaintext highlighter-rouge">ifnone</code> into an instance variable would have meant that default hash values would become a contention point for Ractors.
That felt too much like running some of my previous hard work, so I shelved the idea.
Perhaps one day the generic instance variables table will be contention-free, and then this idea will be more palatable.</p>

<p>My second idea was to get rid of <code class="language-plaintext highlighter-rouge">st_table.type</code>.
This member is a pointer to two functions, one for the hash function to use, the other for the comparison function to use.
In the case of <code class="language-plaintext highlighter-rouge">st_table</code> backed hashes, that pointer is always one of two values.
99% of the time, it’s the default Ruby object hashing function, and in rare cases, if for “identity hashes” created
with <code class="language-plaintext highlighter-rouge">Hash#compare_by_identity</code>, it’s a different pointer.</p>

<p>So, in theory, it would make a whole lot of sense not to store that in the <code class="language-plaintext highlighter-rouge">st_stable</code> and instead just pass it as an argument
whenever it is needed.</p>

<p>But here again it isn’t that simple, because <code class="language-plaintext highlighter-rouge">st_table</code> is a public C API, hence it is used by a bunch of native gems,
and changing the API would break all of them.
The only possibility would be to essentially fork <code class="language-plaintext highlighter-rouge">st.h</code> to have a private internal version used solely by Ruby itself.
But even then, it could cause issues because the Ruby C API exposes the <code class="language-plaintext highlighter-rouge">RHASH_TBL</code> macro that by contract returns an
<code class="language-plaintext highlighter-rouge">st_table</code> pointer.
Even currently, when this macro is called on an <code class="language-plaintext highlighter-rouge">ar_table</code> backed hash, we have to convert it into an <code class="language-plaintext highlighter-rouge">st_table</code>.
So here again it would be very hard to pull off.</p>

<p><code class="language-plaintext highlighter-rouge">RHASH_TBL</code> should really be deprecated and removed, but that would take a long time.</p>

<p>Yet another idea, this time suggested by John Hawthorn, was to use 32-bit integers for <code class="language-plaintext highlighter-rouge">num_entries</code>, <code class="language-plaintext highlighter-rouge">entries_start</code> and <code class="language-plaintext highlighter-rouge">entries_bound</code>.
This would have shrunk the struct by <code class="language-plaintext highlighter-rouge">12B</code> (more likely <code class="language-plaintext highlighter-rouge">8B</code> because of alignment rules), but would also have limited
hashes to ~4 billion entries.</p>

<p>To be fair, such a big hash would require <code class="language-plaintext highlighter-rouge">96GiB</code> of memory just for the entries list,
I can hardly imagine anyone processing such an amount of data using Ruby hashes, but still, having such an arbitrary limitation felt wrong to me.
And amusingly, <a href="https://bugs.ruby-lang.org/issues/12142#note-17">that question was debated 10 years ago already, when Vladimir Makarov submitted his patch</a>.</p>

<p>That’s when, after chatting a bit more with John, we figured we could shrink <code class="language-plaintext highlighter-rouge">entries_start</code> without restricting the maximum size
of the tables.
But to explain why, I need to explain its purpose.</p>

<p>As mentioned previously, even in <code class="language-plaintext highlighter-rouge">st_table</code>, entries are stored linearly in the <code class="language-plaintext highlighter-rouge">entries</code> array:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">table</span><span class="o">-&gt;</span><span class="n">entries</span> <span class="o">=</span> <span class="p">{</span>
  <span class="p">{.</span><span class="n">hash</span> <span class="o">=</span> <span class="mh">0x12</span><span class="p">,</span> <span class="p">.</span><span class="n">key</span> <span class="o">=</span> <span class="mh">0x34</span><span class="p">,</span> <span class="p">.</span><span class="n">value</span> <span class="o">=</span> <span class="mh">0x56</span><span class="p">},</span>
  <span class="p">{.</span><span class="n">hash</span> <span class="o">=</span> <span class="mh">0x78</span><span class="p">,</span> <span class="p">.</span><span class="n">key</span> <span class="o">=</span> <span class="mh">0x90</span><span class="p">,</span> <span class="p">.</span><span class="n">value</span> <span class="o">=</span> <span class="mh">0x12</span><span class="p">},</span>
  <span class="c1">// snip...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is the reason why Ruby hashes preserve their insertion order, and iterating a Hash with <code class="language-plaintext highlighter-rouge">Hash#each</code> is (almost)
as simple as iterating over that contiguous array.</p>

<p>Except that you can not only append to hashes, you can also delete from them, and when you do, you create holes
in that contiguous array.</p>

<p>So naively, whenever you would delete from a hash, you’d need to shift all the following entries to cover the gap,
and then update all the offsets.
That would be pretty costly, but more importantly, wouldn’t have <code class="language-plaintext highlighter-rouge">O(1)</code> complexity.</p>

<p>So instead, to know which entries are present and which are deleted, the hash code of the entry is set to a special value (<code class="language-plaintext highlighter-rouge">0</code>):</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* The reserved hash value and its substitution.  */</span>
<span class="cp">#define RESERVED_HASH_VAL (~(st_hash_t) 0)
#define RESERVED_HASH_SUBSTITUTION_VAL ((st_hash_t) 0)
</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="n">st_hash_t</span>
<span class="nf">normalize_hash_value</span><span class="p">(</span><span class="n">st_hash_t</span> <span class="n">hash</span><span class="p">)</span>
<span class="p">{</span>
    <span class="cm">/* RESERVED_HASH_VAL is used for a deleted entry.  Map it into
       another value.  Such mapping should be extremely rare.  */</span>
    <span class="k">return</span> <span class="n">hash</span> <span class="o">==</span> <span class="n">RESERVED_HASH_VAL</span> <span class="o">?</span> <span class="n">RESERVED_HASH_SUBSTITUTION_VAL</span> <span class="o">:</span> <span class="n">hash</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/* Macros for marking and checking deleted entries given by their
   pointer E_PTR.  */</span>
<span class="cp">#define MARK_ENTRY_DELETED(e_ptr) ((e_ptr)-&gt;hash = RESERVED_HASH_VAL)
#define DELETED_ENTRY_P(e_ptr) ((e_ptr)-&gt;hash == RESERVED_HASH_VAL)
</span></code></pre></div></div>

<p>The consequence is that the iteration routine needs to skip over <code class="language-plaintext highlighter-rouge">RESERVED_HASH_VAL</code>.
In itself, that’s not a huge deal, if you somehow delete a lot of elements from the start of the Hash,
which can happen if, for some reason, you are relying on the <code class="language-plaintext highlighter-rouge">Hash#shift</code> method,
that can cause a lot of extra work for the iteration routine.</p>

<p>It’s to speed that case up that <code class="language-plaintext highlighter-rouge">entries_start</code> was added.
When you delete the first entry, <code class="language-plaintext highlighter-rouge">st</code> does recompute <code class="language-plaintext highlighter-rouge">entries_start</code> to save work later:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Update the entries start of table TAB after removing an entry
   with index N in the array entries.  */</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span>
<span class="nf">update_range_for_deleted</span><span class="p">(</span><span class="n">st_table</span> <span class="o">*</span><span class="n">tab</span><span class="p">,</span> <span class="n">st_index_t</span> <span class="n">n</span><span class="p">)</span>
<span class="p">{</span>
    <span class="cm">/* Do not update entries_bound here.  Otherwise, we can fill all
       bins by deleted entry value before rebuilding the table.  */</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">tab</span><span class="o">-&gt;</span><span class="n">entries_start</span> <span class="o">==</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">st_index_t</span> <span class="n">start</span> <span class="o">=</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
        <span class="n">st_index_t</span> <span class="n">bound</span> <span class="o">=</span> <span class="n">tab</span><span class="o">-&gt;</span><span class="n">entries_bound</span><span class="p">;</span>
        <span class="n">st_table_entry</span> <span class="o">*</span><span class="n">entries</span> <span class="o">=</span> <span class="n">tab</span><span class="o">-&gt;</span><span class="n">entries</span><span class="p">;</span>
        <span class="k">while</span> <span class="p">(</span><span class="n">start</span> <span class="o">&lt;</span> <span class="n">bound</span> <span class="o">&amp;&amp;</span> <span class="n">DELETED_ENTRY_P</span><span class="p">(</span><span class="o">&amp;</span><span class="n">entries</span><span class="p">[</span><span class="n">start</span><span class="p">]))</span> <span class="n">start</span><span class="o">++</span><span class="p">;</span>
        <span class="n">tab</span><span class="o">-&gt;</span><span class="n">entries_start</span> <span class="o">=</span> <span class="n">start</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So this part of the struct isn’t strictly required, and if present, it doesn’t even need to be 100% accurate as long as it’s never higher than it should be.
In other words, it doesn’t need to be able to address the entirety of the <code class="language-plaintext highlighter-rouge">entries</code> array, and we can reasonably assume that it almost never really goes very high.</p>

<p>As such, it would be fine to make it smaller.
Worst-case scenario, for the odd hash with lots of deleted entries, it would be a little bit slower until the hash is rebuilt the next time another entry is inserted into it.</p>

<p>However, you might think that shrinking it isn’t enough.
After all, I needed to reclaim a full <code class="language-plaintext highlighter-rouge">8B</code>, so making it smaller wouldn’t work.</p>

<p>Well, if you haven’t noticed, when I showed the <code class="language-plaintext highlighter-rouge">st_table</code> struct above, there was some free unused space in it:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">st_table</span> <span class="p">{</span>
    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">entry_power</span><span class="p">,</span> <span class="n">bin_power</span><span class="p">,</span> <span class="n">size_ind</span><span class="p">;</span> <span class="c1">// 1B each, so 3B</span>
    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">rebuilds_num</span><span class="p">;</span>                      <span class="c1">// 4B</span>
</code></pre></div></div>

<p>Here, the <code class="language-plaintext highlighter-rouge">3</code> <code class="language-plaintext highlighter-rouge">unsigned char</code> are followed by an <code class="language-plaintext highlighter-rouge">unsigned int</code>.
CPUs have all sorts of restrictions on which addresses they can read, called <a href="https://en.wikipedia.org/wiki/Data_structure_alignment">alignment rules</a>.
They vary a bit from one architecture to another, but the core of the idea is that you can’t just read a word of a given size at any address.
The address must match the size of the memory being read.
For instance, <code class="language-plaintext highlighter-rouge">8B</code> values (like pointers) must be aligned on <code class="language-plaintext highlighter-rouge">8B</code>, meaning their address must be divisible by <code class="language-plaintext highlighter-rouge">8</code>.
Otherwise, depending on the CPU, it will either not work at all or be slower than it could be.</p>

<p>That’s why compilers will sometimes “pad” structs, as in insert implicit holes between the different members.</p>

<p>In the case above, <code class="language-plaintext highlighter-rouge">rebuilds_num</code> is <code class="language-plaintext highlighter-rouge">4B</code> large and on most archs needs to be aligned to <code class="language-plaintext highlighter-rouge">4</code>.
But before it, we have <code class="language-plaintext highlighter-rouge">3</code> <code class="language-plaintext highlighter-rouge">1B</code> large members, so the compiler inserts a <code class="language-plaintext highlighter-rouge">1B</code> padding of essentially wasted space there.</p>

<p>So by making <code class="language-plaintext highlighter-rouge">entries_start</code> <code class="language-plaintext highlighter-rouge">1B</code> large, and moving it alongside <code class="language-plaintext highlighter-rouge">size_ind</code>, we actually save a full <code class="language-plaintext highlighter-rouge">8B</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">st_table</span> <span class="p">{</span>
    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">entry_power</span><span class="p">,</span> <span class="n">bin_power</span><span class="p">,</span> <span class="n">size_ind</span><span class="p">,</span> <span class="n">entries_start</span><span class="p">;</span> <span class="c1">// 1B each, so 4B</span>
    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">rebuilds_num</span><span class="p">;</span>                                     <span class="c1">// 4B</span>
    <span class="k">const</span> <span class="k">struct</span> <span class="n">st_hash_type</span> <span class="o">*</span><span class="n">type</span><span class="p">;</span>                               <span class="c1">// 8B</span>
    <span class="n">st_index_t</span> <span class="n">num_entries</span><span class="p">;</span>                                        <span class="c1">// 8B</span>
    <span class="n">st_index_t</span> <span class="n">entries_bound</span><span class="p">;</span>                                      <span class="c1">// 8B</span>
    <span class="k">struct</span> <span class="n">st_table_entry</span> <span class="o">*</span><span class="n">entries</span><span class="p">;</span>                                <span class="c1">// 8B</span>
<span class="p">};</span>
</code></pre></div></div>

<p>That’s almost the whole patch.
The only extra complication was to add a condition to ensure <code class="language-plaintext highlighter-rouge">entries_start</code> will cap
at <code class="language-plaintext highlighter-rouge">255</code> rather than to overflow.
You can see <a href="https://github.com/ruby/ruby/pull/18149">the full patch</a>.</p>

<p>With that fairly small change, <code class="language-plaintext highlighter-rouge">st_table</code> backed hashes now fit in <code class="language-plaintext highlighter-rouge">64B</code> slot, making it more attractive to
make it possible to allocate <code class="language-plaintext highlighter-rouge">ar_table</code> hashes in smaller slots.</p>

<p>Could we go further and fit in a <code class="language-plaintext highlighter-rouge">40B</code> slot though?
As mentioned above, getting rid of <code class="language-plaintext highlighter-rouge">ifnone</code> and <code class="language-plaintext highlighter-rouge">type</code> could be doable in the long term if instance variables improve
and some C APIs are deprecated.
That would only leave 8 more bytes to find, which I’m sure is doable.</p>

<p>But for now, I think it’s too soon.
We’ll have to wait for some other parts of the codebase to evolve first.</p>

<h3 id="dynamic-ar-table-sizes">Dynamic AR Table Sizes</h3>

<p>But either way, fitting <code class="language-plaintext highlighter-rouge">st_table</code> inside a <code class="language-plaintext highlighter-rouge">40B</code> slot would only allow for shrinking empty hashes.
Other than that, <code class="language-plaintext highlighter-rouge">64B</code> is enough to be as close as possible to the optimal size.</p>

<p>The problem now is that ever since <code class="language-plaintext highlighter-rouge">ar_table</code> was introduced, all the code has been written around it with that fixed size in mind.
So now there’s a bit of a refactoring to do, to make it dynamic and update some assumptions.</p>

<p>I have <a href="https://github.com/byroot/ruby/commit/e351efe54f5660f38192cb04f5f33fc19d25aa08">an unfinished patch</a> that still
has a bunch of bugs that I need to iron out, but it does work as expected on the happy path:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ruby: ruby 4.1.0dev (2026-08-05T19:51:05Z hash-dynamic-ar-bo.. 095039c7b1) +PRISM [arm64-darwin25]
size: 0 	struct:  32 	hash:  64 	diff: 2.0x
size: 1 	struct:  32 	hash:  64 	diff: 2.0x
size: 2 	struct:  40 	hash:  64 	diff: 1.6x
size: 3 	struct:  64 	hash:  80 	diff: 1.3x
size: 4 	struct:  64 	hash:  96 	diff: 1.5x
size: 5 	struct:  64 	hash: 128 	diff: 2.0x
size: 6 	struct:  80 	hash: 128 	diff: 1.6x
size: 7 	struct:  80 	hash: 160 	diff: 2.0x
size: 8 	struct:  96 	hash: 160 	diff: 1.7x
size: 9 	struct:  96 	hash: 448 	diff: 4.7x
size: 10 	struct: 128 	hash: 448 	diff: 3.5x
</code></pre></div></div>

<p>The flip side of the coin, however, is that when starting from a smaller slot, hashes need to evacuate to a <code class="language-plaintext highlighter-rouge">st_table</code>
sooner, hence ending up using more memory than 160B, but also no longer being fully embedded, hence requiring some
extra work to be reclaimed by the garbage collector.</p>

<p>For instance, in the following case:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">hash</span> <span class="o">=</span> <span class="p">{</span><span class="ss">a: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">b: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">c: </span><span class="mi">3</span><span class="p">}</span>       <span class="c1"># 80B</span>
<span class="nb">hash</span><span class="p">[</span><span class="ss">:d</span><span class="p">]</span> <span class="o">=</span> <span class="mi">4</span>
<span class="nb">p</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">(</span><span class="nb">hash</span><span class="p">)</span>  <span class="c1"># 176B</span>
</code></pre></div></div>

<p>The hash starts in an <code class="language-plaintext highlighter-rouge">80B</code> slot, but then needs to transition and end up allocating an <code class="language-plaintext highlighter-rouge">entries</code> array of capacity 4,
for a total of <code class="language-plaintext highlighter-rouge">176B</code>.
So, as it’s often the case, this optimization saves memory in places, but increases memory usage in others.
The hard question is whether it’s a gain or a loss overall.
And that’s the sort of question that is very hard, if not impossible to answer with certainty, given that it really depends on what
the code you are running looks like.
Even if it’s positive for the vast majority of users, there will certainly be some pathological cases in someone’s codebase
that causes it to be negative overall.</p>

<p>That being said, even in the worst case, the degradation really isn’t that bad, given that this mostly concerns small hashes.</p>

<p>Usually, we answer this type of question by measuring the impact on <a href="https://github.com/ruby/ruby-bench/">the <code class="language-plaintext highlighter-rouge">ruby-bench</code> suite</a>,
which does include a couple of real-world applications, so that gives us some confidence in the results,
but aside from the rare cases where the results are really strikingly good, this sort of work always end up on a leap of faith.</p>

<h3 id="takeaways">Takeaways</h3>

<p>I still have roughly 4 months to  polish, measure, and merge that last patch, so hopefully it ships with Ruby 4.1.0 in December,
and it may yield some decent memory savings for Ruby users.</p>

<p>But regardless, I also think this post was a good occasion to showcase how expensive hashes actually are.
As Ruby developers, we tend to overuse them a bit.
There are these convenient sort-of schema-less structs at your fingertips with a dedicated syntax.
But when the structure of the data is known, it’s generally preferable to bother defining a Struct or a class with
instance variables as they are way more compact, and way more efficient to acess.</p>

<p>Similarly, the “array of hashes” pattern is really best avoided in performance-sensitive areas,
instead you can often use flat arrays with a mapping of indexes, like in <a href="https://github.com/rails/rails/pull/51744">this Active Record patch</a> I’m quite proud of.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Or <code class="language-plaintext highlighter-rouge">Data.define</code> which is basically a <code class="language-plaintext highlighter-rouge">Struct</code> in a trenchcoat. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>It was first introduced in 3.2, but many types, like hashes, only started benefiting from it in 3.3. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="ruby" /><category term="performance" /><summary type="html"><![CDATA[As you may know, one area of Ruby performance optimization that particularly interests me is memory usage. Given that most Ruby deployments rely on fork, improving Copy-on-Write performance is generally where you get the biggest bang for your buck, but that only helps with the somewhat static part of an application heap.]]></summary></entry><entry><title type="html">Optimizing Ruby’s JSON, Part 8</title><link href="https://byroot.github.io/ruby/json/2026/07/26/optimizing-ruby-json-part-8.html" rel="alternate" type="text/html" title="Optimizing Ruby’s JSON, Part 8" /><published>2026-07-26T08:28:51+00:00</published><updated>2026-07-26T08:28:51+00:00</updated><id>https://byroot.github.io/ruby/json/2026/07/26/optimizing-ruby-json-part-8</id><content type="html" xml:base="https://byroot.github.io/ruby/json/2026/07/26/optimizing-ruby-json-part-8.html"><![CDATA[<p>It has now been about 18 months since I concluded my post series on optimising Ruby’s json.</p>

<p>Back then, I covered interesting performance patches that happened between version <code class="language-plaintext highlighter-rouge">2.7.2</code>,
the last version published before I took over maintenance, and version <code class="language-plaintext highlighter-rouge">2.9.0</code>, the latest release at that time.</p>

<p>During that span, both the parser and the generator became twice as fast on the infamous <code class="language-plaintext highlighter-rouge">twitter.json</code> benchmark.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== Parsing twitter.json (466906 bytes)
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin25]
 2.7.2    575.498  (± 2.1%) i/s    (1.74 ms/i) -      2.880k in   5.004363s
 2.9.0      1.109k (± 0.7%) i/s  (901.86 μs/i) -      5.564k in   5.017923s

Comparison:
2.7.2:      575.5 i/s
2.9.0:     1108.8 i/s - 1.93x  faster
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== Encoding twitter.json (466906 bytes)
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin25]
 2.7.2      1.108k (± 0.9%) i/s  (902.19 μs/i) -      5.635k in   5.083851s
 2.9.0      2.219k (± 0.2%) i/s  (450.69 μs/i) -     11.220k in   5.056759s

Comparison:
2.7.2:     1108.4 i/s
2.9.0:     2218.8 i/s - 2.00x  faster
</code></pre></div></div>

<p>All this happened over about 6 weeks, and back then, performance was the main focus by far,
and there were numerous low-hanging fruits.</p>

<p>Since then, I’ve released almost 40 new versions, and even though the focus was mostly on deprecating some dangerous parts of the API,
and introducing a few more convenient and efficient APIs,
a number of interesting performance optimizations were implemented.
Most were by myself, but it also appears that the post series did motivate a handful of other people into contributing.</p>

<p>Now the parser is another <code class="language-plaintext highlighter-rouge">1.4x</code> faster:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== Parsing twitter.json (466906 bytes)
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin25]
               2.7.2    586.494  (± 0.7%) i/s    (1.71 ms/i) -      2.950k in   5.029886s
              master      1.570k (± 0.4%) i/s  (637.13 μs/i) -      7.956k in   5.069045s

Comparison:
 2.7.2:      586.5 i/s
master:     1569.5 i/s - 2.68x  faster
</code></pre></div></div>

<p>And the generator <code class="language-plaintext highlighter-rouge">1.8x</code> faster:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== Encoding twitter.json (466906 bytes)
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
               2.7.2   109.000 i/100ms
Calculating -------------------------------------
               2.7.2      1.103k (± 0.5%) i/s  (906.46 μs/i) -      5.559k in   5.039002s

== Encoding twitter.json (466906 bytes)
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
              master   393.000 i/100ms
Calculating -------------------------------------
              master      3.966k (± 0.8%) i/s  (252.17 μs/i) -     20.043k in   5.054161s

Comparison:
 2.7.2:     1103.2 i/s
master:     3965.6 i/s - 3.59x  faster
</code></pre></div></div>

<p>But that’s just for the main <code class="language-plaintext highlighter-rouge">twitter.json</code> benchmark.
Even though the JSON grammar is small, the cost of parsing a document can vary widely.</p>

<p>For instance, the <code class="language-plaintext highlighter-rouge">canada.json</code> benchmark contains a lot of floating-point numbers,
which are surprisingly hard to parse both efficiently and correctly.
That benchmark is now almost <code class="language-plaintext highlighter-rouge">10x</code> faster:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== Parsing canada.json (2090234 bytes)
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin25]
   2.7.2     32.352 (± 0.0%) i/s   (30.91 ms/i) -    162.000  in   5.007433s
   2.9.0     37.698 (± 0.0%) i/s   (26.53 ms/i) -    189.000  in   5.013513s
  master    312.939 (± 3.2%) i/s    (3.20 ms/i) -      1.568k in   5.010569s

Comparison:
 2.7.2:       32.4 i/s
 2.9.0:       37.7 i/s - 1.17x  faster
master:      312.9 i/s - 9.67x  faster
</code></pre></div></div>

<p>So I figured it would be interesting to look back at some of the optimizations that moved the needle.
Now, please do keep in mind some of these are now 18 months old, so I don’t always have as much fresh context
I mind than I had when I wrote the previous parts.
But hopefully it’s still interesting.</p>

<h3 id="more-specialization">More Specialization</h3>

<p>The very first performance improvement that landed after <code class="language-plaintext highlighter-rouge">2.9.0</code> was about more efficiently scanning strings in the generator.
<a href="/ruby/json/2024/12/27/optimizing-ruby-json-part-3.html#dont-ignore-the-not-so-happy-path">Back in part 3, I covered how I previously optimized that part of the generator</a>,
and back then, I noted that there was still some unnecessary overhead:</p>

<blockquote>
  <p>I also realize now that I’m writing this, that I could have used something other than 3 in the lookup table for 0xE2 so
that we don’t do any extra work for the 15 other bytes that mark the start of a 3-byte wide codepoint we’re not
interested in, and also so that only the script safe version of the escape table would ever enter this branch of the code.</p>
</blockquote>

<p>For context, when generating a JSON string, there are three types of characters you need to escape: double-quotes (<code class="language-plaintext highlighter-rouge">"</code>), backslashes (<code class="language-plaintext highlighter-rouge">\</code>)
and ASCII control characters (newline, tab, etc).</p>

<p>But given it’s relatively common to interpolate JSON documents into JavaScript code, the JSON gem also provides a <code class="language-plaintext highlighter-rouge">script_safe: true</code>
escaping option, to also escape forward slashes (<code class="language-plaintext highlighter-rouge">/</code>), line separator (<code class="language-plaintext highlighter-rouge">U+2028</code>), and paragraph separator (<code class="language-plaintext highlighter-rouge">U+2029</code>).</p>

<p>The first one is to protect from XSS attacks, as an unescaped <code class="language-plaintext highlighter-rouge">/</code> could allow the injection of elements in the HTML document, e.g.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="n">script</span><span class="o">&gt;</span>
  <span class="n">var</span> <span class="n">config</span> <span class="o">=</span> <span class="o">&lt;</span><span class="sx">%= config.to_json %&gt;;
&lt;/script&gt;
</span></code></pre></div></div>

<p>Could allow an attacker to inject arbitrary scripts in the page:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
  <span class="kd">var</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">title</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="nt">&lt;/script&gt;&lt;script&gt;</span><span class="nf">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">pwned!</span><span class="dl">'</span><span class="p">)</span><span class="nt">&lt;/script&gt;</span>" };
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>As for the two separator characters, they aren’t really a security concern, but up until recently, they would cause JavaScript syntax errors,
as the JavaScript spec treats them as newlines, and unescaped newlines are not valid inside strings.
Conceptually, JSON was meant to be a subset of JavaScript, but these two characters broke that promise.</p>

<p>Thankfully, <a href="https://github.com/tc39/proposal-json-superset">the ES2019 specification now allows these unescaped characters in Javascript strings</a>,
so it’s no longer really necessary to escape them unless you want to support fairly old JavaScript engines.
We already <a href="https://github.com/rails/rails/pull/55800">stopped doing it by default in Rails</a>, and I’ll probably make that change in <code class="language-plaintext highlighter-rouge">JSON</code> at some point as well.</p>

<p>But anyway, the problem isn’t so much that there are a few more characters to escape, that alone has a negligible performance impact.
The issue is that these are multi-byte characters, hence they cause noticeably more branching, and as we’ve seen in previous parts, branching is bad for performance.</p>

<p>Previously the lookup table was:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">escape_table</span><span class="p">[</span><span class="mi">256</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c1">// ASCII Control Characters</span>
    <span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span>
    <span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span>
    <span class="c1">// ASCII Characters</span>
    <span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span> <span class="c1">// '"'</span>
    <span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span>
    <span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span>
    <span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span> <span class="c1">// '\\'</span>
    <span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span>
    <span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span>
    <span class="c1">// Continuation byte</span>
    <span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span>
    <span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span>
    <span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span>
    <span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span>
    <span class="c1">// First byte of a 2-byte code point</span>
    <span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span>
    <span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span>
    <span class="c1">// First byte of a 3-byte code point</span>
    <span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span>
    <span class="c1">//First byte of a 4+ byte code point</span>
    <span class="mi">4</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">6</span><span class="p">,</span><span class="mi">6</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>

<p>And whenever we’d find the first byte of a 3-byte code point, we’d check whether we have to escape in “script safe” mode
and whether the code point is the one we care about:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// snip...</span>
  <span class="k">case</span> <span class="mi">3</span><span class="p">:</span> <span class="p">{</span>
      <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">b2</span> <span class="o">=</span> <span class="n">ptr</span><span class="p">[</span><span class="n">pos</span> <span class="o">+</span> <span class="mi">1</span><span class="p">];</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">RB_UNLIKELY</span><span class="p">(</span><span class="n">out_script_safe</span> <span class="o">&amp;&amp;</span> <span class="n">ch</span> <span class="o">==</span> <span class="mh">0xE2</span> <span class="o">&amp;&amp;</span> <span class="n">b2</span> <span class="o">==</span> <span class="mh">0x80</span><span class="p">))</span> <span class="p">{</span>
          <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">b3</span> <span class="o">=</span> <span class="n">ptr</span><span class="p">[</span><span class="n">pos</span> <span class="o">+</span> <span class="mi">2</span><span class="p">];</span>
          <span class="k">if</span> <span class="p">(</span><span class="n">b3</span> <span class="o">==</span> <span class="mh">0xA8</span><span class="p">)</span> <span class="p">{</span>
              <span class="n">FLUSH_POS</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
              <span class="n">fbuffer_append</span><span class="p">(</span><span class="n">out_buffer</span><span class="p">,</span> <span class="s">"</span><span class="se">\\</span><span class="s">u2028"</span><span class="p">,</span> <span class="mi">6</span><span class="p">);</span>
              <span class="k">break</span><span class="p">;</span>
          <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">b3</span> <span class="o">==</span> <span class="mh">0xA9</span><span class="p">)</span> <span class="p">{</span>
              <span class="n">FLUSH_POS</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
              <span class="n">fbuffer_append</span><span class="p">(</span><span class="n">out_buffer</span><span class="p">,</span> <span class="s">"</span><span class="se">\\</span><span class="s">u2029"</span><span class="p">,</span> <span class="mi">6</span><span class="p">);</span>
              <span class="k">break</span><span class="p">;</span>
          <span class="p">}</span>
      <span class="p">}</span>
      <span class="c1">// fallthrough</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>So we’d run extra checks every time we run into a 3-byte code point, even when not in script safe-mode.</p>

<p>To speed that up, I packed an extra bit of information in the lookup table.
Now, the lower 3 bits contain the size of the codepoint, and the 4th bit is a boolean telling us whether escaping is required:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 0 - single byte char that don't need to be escaped.</span>
<span class="c1">// (x | 8) - char that needs to be escaped.</span>
<span class="k">static</span> <span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">CHAR_LENGTH_MASK</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span>

<span class="k">static</span> <span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">ascii_only_escape_table</span><span class="p">[</span><span class="mi">256</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c1">// ASCII Control Characters</span>
     <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span>
     <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span>
    <span class="c1">// ASCII Characters</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="c1">// '"'</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="c1">// '\\'</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
    <span class="c1">// Continuation byte</span>
     <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span>
     <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span>
     <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span>
     <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span>
    <span class="c1">// First byte of a  2-byte code point</span>
     <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span>
     <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span>
    <span class="c1">// First byte of a 3-byte code point</span>
     <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span>
    <span class="c1">//First byte of a 4+ byte code point</span>
     <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>

<p>So that now, the <code class="language-plaintext highlighter-rouge">if (RB_UNLIKELY(out_script_safe &amp;&amp; ch == 0xE2 &amp;&amp; b2 == 0x80))</code> and the subsequent branches only
need to be taken when we encounter <code class="language-plaintext highlighter-rouge">0xE2</code>, and not for the many other 3-byte code points.</p>

<p>This resulted in a 15% gain on the <code class="language-plaintext highlighter-rouge">twitter.json</code> benchmark:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== Encoding twitter.json (466906 bytes)
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin23]
Warming up --------------------------------------
               after   244.000 i/100ms
Calculating -------------------------------------
               after      2.436k (± 0.9%) i/s  (410.43 μs/i) -     12.200k in   5.007702s

Comparison:
              before:     2125.9 i/s
               after:     2436.5 i/s - 1.15x  faster
</code></pre></div></div>

<p>Which isn’t too surprising because it contains a lot of Japanese characters in the 3-byte range, e.g.:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"元野球部マネージャー❤︎…最高の夏をありがとう…❤︎"</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<p>You can read <a href="https://github.com/ruby/json/pull/724">the full patch on GitHub</a>.</p>

<h3 id="further-escaping-optimization">Further Escaping Optimization</h3>

<p>Not long after that, <a href="https://github.com/samyron">Scott Myron</a> opened <a href="https://github.com/ruby/json/pull/730">a pull request to speed up escaping further using SIMD</a>.</p>

<p>If you’re unfamiliar with what SIMD is, I briefly <a href="https://byroot.github.io/ruby/json/2025/01/12/optimizing-ruby-json-part-6.html#a-note-on-simd">explained it in part 6</a>,
that’s also where I explained that even though the <code class="language-plaintext highlighter-rouge">simdjson</code> project had shown SIMD can be used to accelerate JSON parsing very significantly,
I wasn’t very keen on taking the associated maintenance burden of maintaining at least three implementations of the same routines,
two of which are particularly cryptic, because SIMD intrinsics have such eyesore names like <code class="language-plaintext highlighter-rouge">_mm_cmplt_epu8</code> or <code class="language-plaintext highlighter-rouge">vcltq_u8</code>.</p>

<p>So Scott’s pull request was a bit of a maintainer’s dilemma for me.
On one hand, his first prototype was showing some very substantial gains, but on the other, I was reticent about including
hard-to-maintain code, given I’m no SIMD expert myself, and I also don’t even have a proper <code class="language-plaintext highlighter-rouge">x86</code> development environment to debug potential issues.</p>

<p>I was also worried about giving him false expectations, causing him to put a lot of work into something that may never be merged,
which, from my point of view, is a very rude thing for a maintainer to do.</p>

<p>So <a href="https://github.com/ruby/json/pull/730#issuecomment-2612031717">I explained to him that his initial pull request was more than I could chew</a>,
and that I’d try to carefully extract the simplest possible SIMD routine in order to make sure it’s not causing issues to some users
before considering any more SIMD usage.
Thankfully, he was very understanding of the situation and was willing to work on this regardless of whether it had any chance of being merged.</p>

<p>After a couple of weeks of back and forth, we settled on just two routines, a NEON one that works on all <code class="language-plaintext highlighter-rouge">arm64</code> CPUs,
and an SSE2 onethat works on all <code class="language-plaintext highlighter-rouge">x86_64</code> CPUs, and we left more powerful <code class="language-plaintext highlighter-rouge">x86</code> implementations such as <code class="language-plaintext highlighter-rouge">SSE4.2</code> and <code class="language-plaintext highlighter-rouge">AVX2</code> for the future.</p>

<p>The <code class="language-plaintext highlighter-rouge">SSE2</code> (Intel) implementation:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kr">inline</span> <span class="n">TARGET_SSE2</span> <span class="n">FORCE_INLINE</span> <span class="kt">int</span> <span class="nf">sse2_update</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">ptr</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">__m128i</span> <span class="n">chunk</span>         <span class="o">=</span> <span class="n">_mm_loadu_si128</span><span class="p">((</span><span class="n">__m128i</span> <span class="k">const</span><span class="o">*</span><span class="p">)</span><span class="n">ptr</span><span class="p">);</span>

    <span class="k">const</span> <span class="n">__m128i</span> <span class="n">lower_bound</span> <span class="o">=</span> <span class="n">_mm_set1_epi8</span><span class="p">(</span><span class="sc">' '</span><span class="p">);</span> 
    <span class="k">const</span> <span class="n">__m128i</span> <span class="n">backslash</span>   <span class="o">=</span> <span class="n">_mm_set1_epi8</span><span class="p">(</span><span class="sc">'\\'</span><span class="p">);</span>
    <span class="k">const</span> <span class="n">__m128i</span> <span class="n">dblquote</span>    <span class="o">=</span> <span class="n">_mm_set1_epi8</span><span class="p">(</span><span class="sc">'\"'</span><span class="p">);</span>

    <span class="n">__m128i</span> <span class="n">too_low</span>       <span class="o">=</span> <span class="n">_mm_cmplt_epu8</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">lower_bound</span><span class="p">);</span>
    <span class="n">__m128i</span> <span class="n">has_backslash</span> <span class="o">=</span> <span class="n">_mm_cmpeq_epi8</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">backslash</span><span class="p">);</span>
    <span class="n">__m128i</span> <span class="n">has_dblquote</span>  <span class="o">=</span> <span class="n">_mm_cmpeq_epi8</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">dblquote</span><span class="p">);</span>
    <span class="n">__m128i</span> <span class="n">needs_escape</span>  <span class="o">=</span> <span class="n">_mm_or_si128</span><span class="p">(</span><span class="n">too_low</span><span class="p">,</span> <span class="n">_mm_or_si128</span><span class="p">(</span><span class="n">has_backslash</span><span class="p">,</span> <span class="n">has_dblquote</span><span class="p">));</span>
    <span class="k">return</span> <span class="n">_mm_movemask_epi8</span><span class="p">(</span><span class="n">needs_escape</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And here’s the <code class="language-plaintext highlighter-rouge">NEON</code> (Arm64) one:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kr">inline</span> <span class="n">FORCE_INLINE</span> <span class="kt">uint64_t</span> <span class="nf">neon_rules_update</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">ptr</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">uint8x16_t</span> <span class="n">chunk</span> <span class="o">=</span> <span class="n">vld1q_u8</span><span class="p">((</span><span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">ptr</span><span class="p">);</span>

    <span class="k">const</span> <span class="n">uint8x16_t</span> <span class="n">lower_bound</span> <span class="o">=</span> <span class="n">vdupq_n_u8</span><span class="p">(</span><span class="sc">' '</span><span class="p">);</span> 
    <span class="k">const</span> <span class="n">uint8x16_t</span> <span class="n">backslash</span>   <span class="o">=</span> <span class="n">vdupq_n_u8</span><span class="p">(</span><span class="sc">'\\'</span><span class="p">);</span>
    <span class="k">const</span> <span class="n">uint8x16_t</span> <span class="n">dblquote</span>    <span class="o">=</span> <span class="n">vdupq_n_u8</span><span class="p">(</span><span class="sc">'\"'</span><span class="p">);</span>

    <span class="n">uint8x16_t</span> <span class="n">too_low</span>       <span class="o">=</span> <span class="n">vcltq_u8</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">lower_bound</span><span class="p">);</span>
    <span class="n">uint8x16_t</span> <span class="n">has_backslash</span> <span class="o">=</span> <span class="n">vceqq_u8</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">backslash</span><span class="p">);</span>
    <span class="n">uint8x16_t</span> <span class="n">has_dblquote</span>  <span class="o">=</span> <span class="n">vceqq_u8</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">dblquote</span><span class="p">);</span>
    <span class="n">uint8x16_t</span> <span class="n">needs_escape</span>  <span class="o">=</span> <span class="n">vorrq_u8</span><span class="p">(</span><span class="n">too_low</span><span class="p">,</span> <span class="n">vorrq_u8</span><span class="p">(</span><span class="n">has_backslash</span><span class="p">,</span> <span class="n">has_dblquote</span><span class="p">));</span>

    <span class="k">return</span> <span class="n">neon_match_mask</span><span class="p">(</span><span class="n">needs_escape</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can see, the SIMD intrinsic names make this code quite cryptic for the uninitiated,
yet, what they do is conceptually fairly simple, so as usual, I’ll try to explain it using Ruby code.</p>

<p>The first key thing to keep in mind is that SIMD is all about applying transformation onto fixed-size <em>vectors</em>,
or simply put, arrays.
So in most cases, the Ruby equivalent would be some <code class="language-plaintext highlighter-rouge">Array</code> or <code class="language-plaintext highlighter-rouge">Enumerable</code> method like <code class="language-plaintext highlighter-rouge">#map</code>,
but you have to imagine that the <code class="language-plaintext highlighter-rouge">map</code> is applied to all the elements simultaneously.</p>

<p>With that said, let’s decompose it line by line, using the <code class="language-plaintext highlighter-rouge">NEON</code> implementation.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">uint8x16_t</span> <span class="n">chunk</span> <span class="o">=</span> <span class="n">vld1q_u8</span><span class="p">((</span><span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">ptr</span><span class="p">);</span>
</code></pre></div></div>

<p>That first line loads (<code class="language-plaintext highlighter-rouge">vld</code> -&gt; vector load) <code class="language-plaintext highlighter-rouge">16</code> 8-bit unsigned integers (<code class="language-plaintext highlighter-rouge">u8</code>) into a register.
The Ruby equivalent would be something like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">ruby_rules_update</span><span class="p">(</span><span class="n">str</span><span class="p">,</span> <span class="n">offset</span><span class="p">)</span>
  <span class="n">chunk</span> <span class="o">=</span> <span class="n">str</span><span class="p">.</span><span class="nf">byteslice</span><span class="p">(</span><span class="n">offset</span><span class="p">,</span> <span class="mi">16</span><span class="p">).</span><span class="nf">bytes</span>
  <span class="c1"># ...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Then the three <code class="language-plaintext highlighter-rouge">vdupq_n_u8</code> calls are defining constant vectors with the characters we care about, using Ruby arrays
it would look like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">LOWER_BOUND</span> <span class="o">=</span> <span class="p">[</span><span class="s2">" "</span><span class="p">.</span><span class="nf">ord</span><span class="p">]</span>   <span class="o">*</span> <span class="mi">16</span>
<span class="no">BACKSLASH</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"</span><span class="se">\\</span><span class="s2">"</span><span class="p">.</span><span class="nf">ord</span><span class="p">]</span>  <span class="o">*</span> <span class="mi">16</span>
<span class="no">DBLQUOTE</span>    <span class="o">=</span> <span class="p">[</span><span class="s1">'"'</span><span class="p">.</span><span class="nf">ord</span><span class="p">]</span>   <span class="o">*</span> <span class="mi">16</span>
</code></pre></div></div>

<p>After that, we compare each element of <code class="language-plaintext highlighter-rouge">chunk</code> against our constants.
First using <code class="language-plaintext highlighter-rouge">&lt;</code> (<code class="language-plaintext highlighter-rouge">lt</code> -&gt; less than), then <code class="language-plaintext highlighter-rouge">==</code> (<code class="language-plaintext highlighter-rouge">eq</code> -&gt; equal):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">too_low</span>       <span class="o">=</span> <span class="n">chunk</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="no">LOWER_BOUND</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">char</span><span class="p">,</span> <span class="n">bound</span><span class="o">|</span> <span class="n">char</span> <span class="o">&lt;</span> <span class="n">bound</span> <span class="p">}</span>
<span class="n">has_backslash</span> <span class="o">=</span> <span class="n">chunk</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="no">BACKSLASH</span><span class="p">).</span><span class="nf">map</span>   <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span> <span class="o">==</span> <span class="n">b</span> <span class="p">}</span>
<span class="n">has_dblquote</span>  <span class="o">=</span> <span class="n">chunk</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="no">DBLQUOTE</span><span class="p">).</span><span class="nf">map</span>    <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span> <span class="o">==</span> <span class="n">b</span> <span class="p">}</span>
</code></pre></div></div>

<p>Which produces three arrays of booleans, which we combine with <code class="language-plaintext highlighter-rouge">|</code> (<code class="language-plaintext highlighter-rouge">vorrq_u8</code> -&gt; “vector or”):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">needs_escape</span> <span class="o">=</span> <span class="n">too_low</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">has_backslash</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span> <span class="o">|</span> <span class="n">b</span> <span class="p">}</span>
<span class="n">needs_escape</span> <span class="o">=</span> <span class="n">needs_escape</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">has_dblquote</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span> <span class="o">|</span> <span class="n">b</span> <span class="p">}</span>
</code></pre></div></div>

<p>Putting it all together in a single method:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">VECTOR_SIZE</span> <span class="o">=</span> <span class="mi">16</span>
<span class="no">LOWER_BOUND</span> <span class="o">=</span> <span class="p">[</span><span class="s2">" "</span><span class="p">.</span><span class="nf">ord</span><span class="p">]</span>   <span class="o">*</span> <span class="no">VECTOR_SIZE</span>
<span class="no">BACKSLASH</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"</span><span class="se">\\</span><span class="s2">"</span><span class="p">.</span><span class="nf">ord</span><span class="p">]</span>  <span class="o">*</span> <span class="no">VECTOR_SIZE</span>
<span class="no">DBLQUOTE</span>    <span class="o">=</span> <span class="p">[</span><span class="s1">'"'</span><span class="p">.</span><span class="nf">ord</span><span class="p">]</span>   <span class="o">*</span> <span class="no">VECTOR_SIZE</span>

<span class="k">def</span> <span class="nf">ruby_rules_update</span><span class="p">(</span><span class="n">str</span><span class="p">,</span> <span class="n">offset</span><span class="p">)</span>
  <span class="n">chunk</span> <span class="o">=</span> <span class="n">str</span><span class="p">.</span><span class="nf">byteslice</span><span class="p">(</span><span class="n">offset</span><span class="p">,</span> <span class="no">VECTOR_SIZE</span><span class="p">).</span><span class="nf">bytes</span>

  <span class="n">too_low</span>       <span class="o">=</span> <span class="n">chunk</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="no">LOWER_BOUND</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">char</span><span class="p">,</span> <span class="n">bound</span><span class="o">|</span> <span class="n">char</span> <span class="o">&lt;</span> <span class="n">bound</span> <span class="p">}</span>
  <span class="n">has_backslash</span> <span class="o">=</span> <span class="n">chunk</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="no">BACKSLASH</span><span class="p">).</span><span class="nf">map</span>   <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span> <span class="o">==</span> <span class="n">b</span> <span class="p">}</span>
  <span class="n">has_dblquote</span>  <span class="o">=</span> <span class="n">chunk</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="no">DBLQUOTE</span><span class="p">).</span><span class="nf">map</span>    <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span> <span class="o">==</span> <span class="n">b</span> <span class="p">}</span>

  <span class="n">needs_escape</span> <span class="o">=</span> <span class="n">too_low</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">has_backslash</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span> <span class="o">|</span> <span class="n">b</span> <span class="p">}</span>
  <span class="n">needs_escape</span> <span class="o">=</span> <span class="n">needs_escape</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">has_dblquote</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span> <span class="o">|</span> <span class="n">b</span> <span class="p">}</span>

  <span class="n">needs_escape</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And as you can see, just like the SIMD implementation, that Ruby method returns a vector of booleans, telling us
which characters need to be escaped.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="n">ruby_rules_update</span><span class="p">(</span><span class="s1">'___\\___""_________'</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="p">[</span><span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">true</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">true</span><span class="p">,</span> <span class="kp">true</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="o">...</span><span class="p">]</span>
</code></pre></div></div>

<p>Even though in C we repack that vector into a normal 64-bit number that we use as a bitfield, conceptually it’s the same thing as an array of booleans.</p>

<p>The very counter-intuitive part of SIMD is that when you’re very used to scalar code, it really looks like SIMD is wasting a lot
of time computing useless things.
By that I mean that the idiomatic scalar equivalent would likely short-circuit to avoid computing <code class="language-plaintext highlighter-rouge">c == '"'</code> if we previously established
that <code class="language-plaintext highlighter-rouge">c &lt; ' '</code>.
Yet, since SIMD instructions operate on 16 (or more) bytes at once, the amortized cost of the check on a per-byte basis is so low
that we win anyway.</p>

<p>For me, that’s one of the challenging things with writing or reviewing SIMD code, it requires having a fairly different mindset.</p>

<p>You can have a look at <a href="https://github.com/ruby/json/pull/743">the final pull request</a>, it contains a lot of extra boilerplate
required to test for SIMD support, etc, but I explained the core of it just above.</p>

<p>Overall, between the very first version and when the first SIMD code landed, there were over three months of iteration and back-and-forth.
We also included <a href="https://github.com/ruby/json/pull/769">some code from radiospiel, who happened to also look at introducing SIMD</a>, but didn’t notice Scott’s work.</p>

<p>In the end, the gain on <code class="language-plaintext highlighter-rouge">twitter.json</code> was quite substantial:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== Encoding twitter.json (466906 bytes)
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [arm64-darwin24]
               after      3.115k (± 3.1%) i/s  (321.01 μs/i) -     15.759k in   5.063776s

Comparison:
              before:     2508.3 i/s
               after:     3115.2 i/s - 1.24x  faster
</code></pre></div></div>

<h3 id="a-brand-new-parser">A Brand New Parser</h3>

<p>Another change that was foreshadowed on this blog was the re-implementation of the parser.
I ended part 7 with this comment:</p>

<blockquote>
  <p>I’d like to drop Ragel and replace it with a simpler recursive descent parser.
The existing one could certainly be improved, but I find it much harder to work with generated parsers than to write them manually.</p>
</blockquote>

<p>When I wrote this, I actually meant it, but as a nice-to-have.
Something I’d eventually work on when I’d get the time, but that wasn’t pressing by any means.</p>

<p>But I suspect publishing this combination of keywords on the Internet must have lit some sort of bat-signal in
<a href="https://github.com/kddnewton">Kevin Newton</a>’s office,
because not long after he pinged me with just that, a reimplementation of the JSON parser using recursive descent.</p>

<p>The implementation wasn’t quite complete, all sorts of corner cases weren’t handled yet, but <a href="https://github.com/byroot/json/blob/5694cff0b773ed786da7c0f97521714fef4fee54/ext/json/ext/parser/parser.c">the code was clean, barely 200 lines</a> and much, much easier
to work with than the previous 1400 lines of Ragel implementation.
Even though it was mostly just prototype quality, you could really feel that there were lots of lessons he learned while
working on <a href="https://github.com/ruby/prism/">Prism</a> baked in that prototype.</p>

<p>That being said, after adding the missing parts and refactoring the code quite heavily to familiarize myself with it,
the size grew to a bit less than 600 lines, and <a href="https://github.com/ruby/json/pull/729#issuecomment-2593780223">the performance impact was mixed</a>.
The parser performed about the same on <code class="language-plaintext highlighter-rouge">twitter.json</code>, and even <code class="language-plaintext highlighter-rouge">6%</code> faster on <code class="language-plaintext highlighter-rouge">citm_catalog.json</code>, but <code class="language-plaintext highlighter-rouge">8%</code> slower on <code class="language-plaintext highlighter-rouge">activitypub.json</code>.
So, gain some, lose some, however it did still replace 1400 lines of Ragel code with less than 600 lines of pure C, so it was really worth it in my opinion.</p>

<p>But ultimately, a code that is easy to understand and work with is also easier to optimize,
so I was confident that this new implementation would allow me to find optimization faster than with Ragel, so it was worth pursuing.</p>

<p>I just didn’t want to merge a measurable regression, so I needed to find enough performance to be on par with Ragel on all benchmarks.</p>

<p>And like several times in the past, I found some nice wins using lookup tables.</p>

<p>In <a href="https://github.com/byroot/json/blob/5694cff0b773ed786da7c0f97521714fef4fee54/ext/json/ext/parser/parser.c">Kevin’s initial prototype</a>, there was two fairly hot loop.</p>

<p>One was for skipping whitespaces:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span>
<span class="nf">j2_eat_whitespace</span><span class="p">(</span><span class="n">j2_parser_t</span> <span class="o">*</span><span class="n">parser</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span> <span class="o">&lt;</span> <span class="n">parser</span><span class="o">-&gt;</span><span class="n">end</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">switch</span> <span class="p">(</span><span class="o">*</span><span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">case</span> <span class="sc">' '</span><span class="p">:</span>
            <span class="k">case</span> <span class="sc">'\t'</span><span class="p">:</span>
            <span class="k">case</span> <span class="sc">'\n'</span><span class="p">:</span>
            <span class="k">case</span> <span class="sc">'\r'</span><span class="p">:</span>
                <span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="o">++</span><span class="p">;</span>
                <span class="k">break</span><span class="p">;</span>
            <span class="k">case</span> <span class="sc">'/'</span><span class="p">:</span>
                <span class="c1">// snip... deal with comments</span>
                <span class="k">break</span><span class="p">;</span>
            <span class="nl">default:</span>
                <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Since JSON allows for whitespace between all tokens, this function is called a lot, making it quite the hotspot.
That being said, it’s almost always a noop, given that we benchmark against minified JSON documents, hence there are no comments
not whitespace in any of the benchmarks.</p>

<p>Which somewhat maps the reality, as aside from the occasional configuration file, most JSON documents in the wild
are produced as compactly as possible to save bandwidth.</p>

<p>Hence, checking for 5 different characters, when it will almost never match, is a bit of a waste.</p>

<p>And here again, it looked like a good case for using <a href="/ruby/json/2024/12/15/optimizing-ruby-json-part-1.html#lookup-tables">a lookup table</a>,
so I changed the code:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">const</span> <span class="n">bool</span> <span class="n">whitespace</span><span class="p">[</span><span class="mi">256</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">[</span><span class="sc">' '</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="p">[</span><span class="sc">'\t'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="p">[</span><span class="sc">'\n'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="p">[</span><span class="sc">'\r'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="p">[</span><span class="sc">'/'</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
<span class="p">};</span>

<span class="k">static</span> <span class="kt">void</span>
<span class="nf">json_eat_comments</span><span class="p">(</span><span class="n">JSON_ParserState</span> <span class="o">*</span><span class="n">state</span><span class="p">)</span>
<span class="p">{</span>
  <span class="c1">// snip...</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span>
<span class="nf">json_eat_whitespace</span><span class="p">(</span><span class="n">JSON_ParserState</span> <span class="o">*</span><span class="n">state</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">state</span><span class="o">-&gt;</span><span class="n">cursor</span> <span class="o">&lt;</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">end</span> <span class="o">&amp;&amp;</span> <span class="n">RB_UNLIKELY</span><span class="p">(</span><span class="n">whitespace</span><span class="p">[(</span><span class="kt">unsigned</span> <span class="kt">char</span><span class="p">)</span><span class="o">*</span><span class="n">state</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="p">]))</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">RB_LIKELY</span><span class="p">(</span><span class="o">*</span><span class="n">state</span><span class="o">-&gt;</span><span class="n">cursor</span> <span class="o">!=</span> <span class="sc">'/'</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">state</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="o">++</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">json_eat_comments</span><span class="p">(</span><span class="n">state</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I also extracted the comment handling part into its own function, so that the compiler is less likely to inline it<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.
This is because comments in JSON documents are rare.
They’re mostly just present in configuration files, which really aren’t that performance sensitive.
So it’s best to discourage the compiler from inlining rarely called functions, because compilers have some sort of soft limit
to inlining.</p>

<p>They try to avoid creating functions that are too big using various heuristics, so you can sometimes witness some really bad
performance regressions by just adding a few lines of code in an inlined function, causing the compiler to stop inlining
some other functions, tanking the performance.</p>

<p>This is a particularly common problem for large dispatch loops, like the one found in recursive descent parsers and interpreter loops.</p>

<p>Similarly, in the initial prototype, the string parsing code was relying on sequential character comparisons to either
the end of the string or escape sequences:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">case</span> <span class="sc">'"'</span><span class="p">:</span> <span class="p">{</span>
      <span class="c1">// %r{\A"[^"\\\t\n\x00]*(?:\\[bfnrtu\\/"][^"\\]*)*"}</span>
      <span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="o">++</span><span class="p">;</span>
      <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">start</span> <span class="o">=</span> <span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="p">;</span>

      <span class="k">while</span> <span class="p">(</span><span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span> <span class="o">&lt;</span> <span class="n">parser</span><span class="o">-&gt;</span><span class="n">end</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span> <span class="o">==</span> <span class="sc">'"'</span><span class="p">)</span> <span class="p">{</span>
              <span class="n">VALUE</span> <span class="n">string</span> <span class="o">=</span> <span class="n">rb_enc_str_new</span><span class="p">((</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">)</span> <span class="n">start</span><span class="p">,</span> <span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span> <span class="o">-</span> <span class="n">start</span><span class="p">,</span> <span class="n">rb_utf8_encoding</span><span class="p">());</span>
              <span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="o">++</span><span class="p">;</span>
              <span class="k">return</span> <span class="n">string</span><span class="p">;</span>
          <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span> <span class="o">==</span> <span class="sc">'\\'</span><span class="p">)</span> <span class="p">{</span>
              <span class="c1">// TODO: Parse escape sequence</span>
              <span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="o">++</span><span class="p">;</span>
          <span class="p">}</span>

          <span class="n">parser</span><span class="o">-&gt;</span><span class="n">cursor</span><span class="o">++</span><span class="p">;</span>
      <span class="p">}</span>

      <span class="n">rb_raise</span><span class="p">(</span><span class="n">rb_eRuntimeError</span><span class="p">,</span> <span class="s">"unexpected end of input"</span><span class="p">);</span>
      <span class="k">break</span><span class="p">;</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>Which at first performed OK, but was actually incomplete because a conforming JSON parser must also fail to parse if it encounters
an unescaped ASCII escape code.</p>

<p>Once I added an <code class="language-plaintext highlighter-rouge">} else if (*parser-&gt;cursor &lt; ' ') {</code> branch, the performance lowered to be significantly worse than the previous parser.</p>

<p>So there too, I introduced a lookup table:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">const</span> <span class="n">bool</span> <span class="n">string_scan</span><span class="p">[</span><span class="mi">256</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c1">// ASCII Control Characters</span>
     <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span>
     <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span>
    <span class="c1">// ASCII Characters</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="c1">// '"'</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="c1">// '\\'</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
     <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>

<p>With these two changes and a few more minor tweaks, the new parser was finally <a href="https://github.com/ruby/json/pull/729#issuecomment-2595328158">a bit faster than the old one</a>.
<code class="language-plaintext highlighter-rouge">15%</code> on <code class="language-plaintext highlighter-rouge">twitter.json</code>, <code class="language-plaintext highlighter-rouge">7%</code> on <code class="language-plaintext highlighter-rouge">activitypub.json</code> and <code class="language-plaintext highlighter-rouge">11%</code> on <code class="language-plaintext highlighter-rouge">citm_catalog.json</code>.
Nothing major, but there again, the goal was merely to match the previous performance, with the hope that it would be a better
basis for future gains.</p>

<h3 id="to-be-continued">To Be Continued</h3>

<p>I think that’s enough content for now.
When I get the time to write part 9, I’ll talk about how <a href="https://github.com/radiospiel">radiospiel</a> sped up number generation.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Many versions later, I even marked it with <code class="language-plaintext highlighter-rouge">__attribute__((noinline))</code> to really discourage inlining. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="ruby" /><category term="json" /><summary type="html"><![CDATA[It has now been about 18 months since I concluded my post series on optimising Ruby’s json.]]></summary></entry><entry><title type="html">The Missing Bundler Features</title><link href="https://byroot.github.io/ruby/bundler/2026/04/20/bundle-features.html" rel="alternate" type="text/html" title="The Missing Bundler Features" /><published>2026-04-20T00:03:51+00:00</published><updated>2026-04-20T00:03:51+00:00</updated><id>https://byroot.github.io/ruby/bundler/2026/04/20/bundle-features</id><content type="html" xml:base="https://byroot.github.io/ruby/bundler/2026/04/20/bundle-features.html"><![CDATA[<p>Over the last few months, there has been a lot of talk about making Bundler faster,
both by improving it directly, or by reimplementing it in another language, and while it may surprise some, that didn’t excite me much.</p>

<p>Don’t get me wrong, all other things being equal, faster is better, so if Bundler gets faster without me having to change my toolchain one bit,
I’ll happily take it.
But I certainly would not bother migrating to something else just for speed.</p>

<p>Instead, there are a number of features I believe Bundler is missing, and that over the years,
I tried to convince Bundler’s maintainers to consider them, but without any success.</p>

<h2 id="why-bundler-is-fast-enough-for-me">Why Bundler Is Fast Enough For Me</h2>

<p>Given how much I talk about performance on this blog, it might surprise you to hear I don’t care about Bundler performance.</p>

<p>The reality is that I very rarely, if ever, have to wait for Bundler.
Most of the time, all gems are already installed locally, so it completes instantly. A few times, a couple of gems are missing, and it completes in mere seconds, this is nowhere near a bottleneck in my workflow.</p>

<p>It’s only once every few months, when I upgrade Ruby or checkout a new big project, that I have to wait a couple of minutes
for the installation to complete, so it’s not like it’s driving me up the wall.</p>

<p>Similarly, for CI and deploys, the gems installed by Bundler are heavily cached, so <code class="language-plaintext highlighter-rouge">bundle install</code> is very fast.</p>

<p>And even if Bundler was magically able to install everything instantly without a cache, I’d argue that caching gems on CI
would remain absolutely essential simply for resilience purposes.</p>

<p>I remember a few years back, when <a href="https://github.com/rails/rails/issues/41750">the <code class="language-plaintext highlighter-rouge">mimemagic</code> gem was yanked because of licensing issues</a>,
a lot of Rails users complained that their CI pipeline was broken and they couldn’t work anymore.
Well, Shopify CI and image builder continued uninterrupted, as it kept re-using the gem from its cache.
It’s also entirely resilient to rubygems.org outages.</p>

<p>That’s why I was quite amused a few months back, when some people were floating the idea of making rubygems.org pay by usage,
thinking big players would have to pay for most of the infrastructure.
The CIs of big Ruby companies actually hit rubygems.org much less than even a small project CI with no cache would.</p>

<p>So yes, it’s very rare that I find myself staring at the output of <code class="language-plaintext highlighter-rouge">bundle install</code> for more than a few seconds.</p>

<p>However, <strong>I frequently waste hours, if not days</strong>, dealing with dependency hell in big gemfiles, <strong>because Bundler is missing some important features</strong>.</p>

<h2 id="dependency-hell">Dependency Hell</h2>

<p>To better illustrate what I mean, let me describe one of the things I was working on just last week before leaving for Ruby Kaigi.</p>

<p>For various reasons, I wanted to upgrade the <code class="language-plaintext highlighter-rouge">openssl</code> gem in Intercom’s monolith, as it’s still running a 2.x version.</p>

<p>Except <code class="language-plaintext highlighter-rouge">bundle update openssl</code> would silently do nothing, because the Gemfile contained the <a href="https://rubygems.org/gems/web-push/versions/2.0.0"><code class="language-plaintext highlighter-rouge">web-push 2.0.0</code> gem</a>,
which has an <code class="language-plaintext highlighter-rouge">openssl ~&gt; 2.2</code> dependency constraint.
So I first tried to upgrade to the latest version of <code class="language-plaintext highlighter-rouge">web-push</code>, except that the later version has a <code class="language-plaintext highlighter-rouge">jwt ~&gt; 3.0</code> dependency constraint,
which was a problem for 4 other gems in the Gemfile, which have a <code class="language-plaintext highlighter-rouge">jwt ~&gt; 2.0</code> constraint, and some of these gems
haven’t been released in years, so while <a href="https://github.com/sumoheavy/jira-ruby/pull/482">I did submit pull requests</a>
to unblock all of that, I have no clue when the situation will be unblocked (if ever).</p>

<p>So now, I can either patiently wait and perhaps try to ping the multiple maintainers every couple of weeks, or I’ll have to use
unreleased versions of these gems using <code class="language-plaintext highlighter-rouge">gem 'name', github: '...'</code>, which works, but is annoying for various reasons.</p>

<h2 id="upper-constraints">Upper Constraints</h2>

<p>The root cause of most, if not all, of these dependency hell problems is upper version constraints.
<code class="language-plaintext highlighter-rouge">openssl ~&gt; 2.2</code> is a shorthand for <code class="language-plaintext highlighter-rouge">openssl &gt;= 2.2, &lt; 3</code>, and the problem here is the <code class="language-plaintext highlighter-rouge">&lt; 3</code>.</p>

<p>This <code class="language-plaintext highlighter-rouge">~&gt;</code> operator (often named the pessimistic operator) essentially assumes that the dependency follows <a href="https://semver.org/">Semantic Versioning</a>,
and that the next major version will not work with the current version of the gem.</p>

<p>But I would argue that upper version constraints are almost always wrong, because when you are writing a gem, that next major version of your dependency doesn’t yet exist, hence you don’t actually know it won’t work.</p>

<p>A SemVer major just tells us that there are some breaking changes, it doesn’t tell us that absolutely every caller will be broken.
Dependencies like <code class="language-plaintext highlighter-rouge">openssl</code> have a very large API, just because they bumped the major to signal that some things were broken doesn’t mean they broke your usage of it.</p>

<p>Hence, the “pessimistic operator” name is quite apt, but I’d argue it’s wrong to be this pessimistic.</p>

<p>If it was possible to update the dependencies of a gem after it has been released, then yes,
it would make sense to warn your user that your gem doesn’t work well with this new major version of OpenSSL,
but doing so in advance is likely to be more harmful than helpful.</p>

<p>But while I have strong opinions on how maintainers should declare their dependencies, ultimately, they are king in their domain,
so they are free to do as they wish.</p>

<p>However, I also consider that my Gemfile is my domain.
Therefore, I shall be empowered to disregard any such constraint in my Gemfile as I see fit, but Bundler won’t let me.
That I think is utterly wrong.</p>

<p>So let’s talk about the Bundler features I think would save me loads of time.</p>

<h2 id="forcing-a-version">Forcing A Version</h2>

<p>Using the same <code class="language-plaintext highlighter-rouge">openssl</code> example, my initial goal was just to upgrade that particular gem, so I believe I should have been
able to tell Bundler to just do that, even if it meant not respecting some other gem’s wishes.</p>

<p>In terms of actual feature design, what I’d like would be to be able to just add <code class="language-plaintext highlighter-rouge">force: true</code> on a gem:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'openssl'</span><span class="p">,</span> <span class="s1">'&gt;= 3.0'</span><span class="p">,</span> <span class="ss">force: </span><span class="kp">true</span>
</code></pre></div></div>

<p>This would cause Bundler to replace all <code class="language-plaintext highlighter-rouge">openssl</code> dependency constraints from other gems with this one.
Bundler would, of course, be free to print as many warnings as it wishes, but it should install the gems I want.</p>

<p>Perhaps it would turn out that indeed, <code class="language-plaintext highlighter-rouge">web-push 2.0.0</code> is truly incompatible with <code class="language-plaintext highlighter-rouge">openssl 3.x</code>, but that’s my Gemfile,
that’s on me to test this properly, and to deal with the consequences if I didn’t.</p>

<p>It’s my machine, my project, and I shall be allowed to break it if that’s what I want to do.</p>

<p>In practice, they didn’t have to change anything when <a href="https://github.com/pushpad/web-push/commit/b1b60bb2a2ab0f4bd9331b944117b110ef94c3bf">they relaxed the dependency to allow <code class="language-plaintext highlighter-rouge">openssl 3.x</code></a>
and <a href="https://github.com/pushpad/web-push/pull/19">again for <code class="language-plaintext highlighter-rouge">openssl 4.x</code></a>, which further proves my point
that pessimistic constraints are almost always wrong.</p>

<h2 id="subtituting-a-gem">Subtituting A Gem</h2>

<p>Another dependency hell sort of problem is outdated, broken gems.
And the most well-known occurrence of that problem in the last few years was <a href="https://rubygems.org/gems/httpclient">the <code class="language-plaintext highlighter-rouge">httpclient</code> gem</a>.</p>

<p>This was a relatively popular gem for a long time, even used as a dependency of <code class="language-plaintext highlighter-rouge">google-cloud</code> gems, but until recently, the
last version was published in 2016.
The problem, however, was that by default the gem would set up SSL using vendored root certificates, which worked fine until they
expired in 2023, and suddenly, many Ruby applications just broke.
Then later on, when Ruby 3.4 was released, the gem started throwing various warnings.</p>

<p>All this time, many people had to fork the gem and point their Gemfile at their own fork.</p>

<p>Ultimately, in 2025, <a href="https://github.com/yahonda/">Yasuo Honda</a> managed to contact the former maintainer and get the gem ownership, which allowed him to fix all these problems and cut a new release.
But the fact remains that this gem had been a thorn in many Rubyists’ sides for several years.</p>

<p>This is just an example.
There are other gems that are no longer actively maintained, and that could cause similar issues at any time if they’d ever hit a backward compatibility issue of some sort.</p>

<p>As mentioned, the ability of Bundler to pull a gem from a Git repository is what unblocked everyone, but I can’t help but think it’s a big waste of effort, as pointing your Gemfile to a git repo you don’t control is a bit risky.
It would have been much better to be able to substitute the vanilla <code class="language-plaintext highlighter-rouge">httpclient</code> gem with another gem published on rubygems.org.</p>

<p>Since I had to fork <code class="language-plaintext highlighter-rouge">httpclient</code> to fix its issues for my employer, I would have much preferred to release my fixes
as a gem named <code class="language-plaintext highlighter-rouge">byroot-httpclient</code>, and for everyone who trusts me to have been able to tell Bundler to use that instead.</p>

<p>That’s the feature <a href="https://github.com/rubygems/rfcs/issues/54">I tried to suggest to bundler maintainers over two years ago</a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s2">"byroot-httpclient"</span><span class="p">,</span> <span class="ss">as: </span><span class="s2">"httpclient"</span>
</code></pre></div></div>

<p>As a way to tell Bundler that my gem is a substitute for the vanilla one.</p>

<p>Unfortunately, like almost every time I tried to interact with Bundler, I wasn’t able to get anything moving.</p>

<p>Two years later, I still think that feature would be tremendously helpful, but it could even be simplified.
In practice, you don’t even need Bundler to substitute a gem for another, you just need it not to install a particular
gem:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ban</span> <span class="s1">'httpclient'</span> <span class="c1"># Broken because ....</span>
<span class="n">gem</span> <span class="s1">'byroot-httpclient'</span> <span class="c1"># Replace `httpclient` </span>
</code></pre></div></div>

<p>Which makes for an even simpler feature, and would also be useful to prune some dependencies that are being pulled, but
that you know you’re not actually using.</p>

<p>It would also help the community pick over the maintenance of abandoned gems under different names, as it would allow people
to migrate to alternative implementations.
In a sense, it solves the need for gem namespacing much better than what has been explored so far.</p>

<h2 id="its-all-about-control">It’s All About Control</h2>

<p>Ultimately, the two features I described above serve the same purpose: giving control back to the user.</p>

<p>It’s quite apparent in Bundler design that it trusts gem publishers more than the actual Bundler user, which, to me, is wrong.
That impression is reinforced by the concerns raised when I requested that feature:</p>

<blockquote>
  <p>The biggest worry that comes immediately to mind for me is “how do we keep a feature like this from harming maintainers?”
For example, if I maintain left-pad, but someone else is using left-padder, how do we ensure that errors are reported to left-padder and not to left-pad?</p>
</blockquote>

<p>Which might come from a good place, but doesn’t make much sense to me, as it’s already possible to substitute upstream code
via git gems, private gem servers, or even simply monkey patches, yet as the maintainer of numerous gems, this has never been
a significant harm to me.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I very sincerely believe Bundler is still one of the best, if not the best, package manager out there.
And the fact that it has been used as a model for other package managers is proof of that.</p>

<p>But as a mere user, I have the feeling it has stagnated over the last decade.</p>

<p>Don’t get me wrong, it changed a lot internally, probably became more reliable and now faster, but in terms of user
ergonomics, I haven’t personally witnessed significant improvements since the early 2010’s.</p>

<p>I think these two features (or any feature solving these use cases) could be a major advancement for Ruby’s developer experience.</p>]]></content><author><name></name></author><category term="ruby" /><category term="bundler" /><summary type="html"><![CDATA[Over the last few months, there has been a lot of talk about making Bundler faster, both by improving it directly, or by reimplementing it in another language, and while it may surprise some, that didn’t excite me much.]]></summary></entry><entry><title type="html">Optimizing Ruby Path Methods</title><link href="https://byroot.github.io/ruby/performance/2026/04/18/faster-paths.html" rel="alternate" type="text/html" title="Optimizing Ruby Path Methods" /><published>2026-04-18T12:03:51+00:00</published><updated>2026-04-18T12:03:51+00:00</updated><id>https://byroot.github.io/ruby/performance/2026/04/18/faster-paths</id><content type="html" xml:base="https://byroot.github.io/ruby/performance/2026/04/18/faster-paths.html"><![CDATA[<p>Back in November last year, I started a new job at Intercom, and one of the first projects I got to work on was
improving the Intercom monolith CI with some of my new colleagues.</p>

<p>Interestingly, I never got around to talking about CI on this blog, even though I consider it to be one of my main areas of expertise.
That topic is way beyond the subject I’d like to talk about here, but just to give a bit of context, a key driver in CI performance
and user experience is how fast you can get a Ruby process ready to run tests.</p>

<p>When working with very large test suites, it becomes essential to run tests in parallel.
If you have a test suite that runs in say, 1 hour, on paper, you can run it in 15 minutes on 4 workers, or in 6 minutes on 10 workers,
and 1 minute on 60 workers.</p>

<p>But that’s a bit too simplistic, in practice, a CI test runner has two phases.</p>

<p>First, a setup phase that all runners have to go through, which includes fetching the source code,
getting backing services like the database ready, and booting the application.
Once the setup phase is done, the workers can start doing the actually useful work of running tests.</p>

<p>So using the same 1-hour test suite, but now with a 1-minute setup phase, will now take 16 minutes if you are using 4 workers
but 2 minutes if you are using 60 parallel workers.
That’s a much worse user experience, but also means half of your compute isn’t spent doing the actual work, likely increasing
your costs.</p>

<p>All this to say that parallelizing test suites has diminishing returns that are entirely tied to how costly setting up a worker is.
The worker setup time is like a fixed cost toll, hence reducing it both improves user experience and reduces cost.</p>

<p>Given that the Intercom monolith CI runs with 1350 parallel workers by default, one second is optimized out of the setup
time has 1350 times more impact than a second optimized out of a particular test, and saves over 20 minutes of compute per build.</p>

<p>Hence, while the team also worked on speeding up various slow tests and factories, I personally was very focused on reducing the setup time,
shaving every second or even split seconds I could find.</p>

<p>As part of this effort, I looked into speeding up the application boot time, and if you’re a Rubyist, you probably know about Bootsnap.</p>

<h2 id="what-does-bootsnap-even-do">What Does Bootsnap Even Do?</h2>

<p>While Bootsnap <a href="https://github.com/rails/rails/pull/29313">has been in the default Rails gemfile for almost a decade now</a>,
and is very popular even in non-Rails codebases, based on chats I had with people online or at conferences, I suspect many
people don’t quite understand exactly what it is doing.
So let me explain just one of the optimizations it performs.</p>

<p>When you require a file (what is internally referred to as a “feature”), Ruby has to perform a very expensive linear search
in its load path, something that looks a bit like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">search_load_path</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>
  <span class="k">if</span> <span class="n">path</span><span class="p">.</span><span class="nf">end_with?</span><span class="p">(</span><span class="s2">".rb"</span><span class="p">,</span> <span class="s2">".so"</span><span class="p">)</span>
    <span class="vg">$LOAD_PATH</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">load_path</span><span class="o">|</span>
      <span class="n">absolute_path</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">load_path</span><span class="p">,</span> <span class="n">feature</span><span class="p">)</span>
      <span class="k">return</span> <span class="n">absolute_path</span> <span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="n">absolute_path</span><span class="p">)</span>
    <span class="k">end</span>
    <span class="k">return</span> <span class="kp">nil</span>
  <span class="k">else</span>
    <span class="n">search_load_path</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">path</span><span class="si">}</span><span class="s2">.rb"</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">path</span><span class="si">}</span><span class="s2">.so"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">require_internal</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>
  <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="vg">$LOADED_FEATURES</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>

  <span class="n">absolute_path</span> <span class="o">=</span> <span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">absolute_path?</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>
    <span class="n">feature</span>
  <span class="k">else</span>
    <span class="n">search_load_path</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">unless</span> <span class="n">absolute_path</span>
    <span class="k">raise</span> <span class="no">LoadError</span><span class="p">,</span> <span class="s2">"cannot load such file: </span><span class="si">#{</span><span class="n">feature</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>

  <span class="nb">load</span><span class="p">(</span><span class="n">absolute_path</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The problem with this code loading mechanism is that, while simple, it scales very badly.</p>

<p>A clean Ruby process starts with approximately 8 paths in its load path, so <code class="language-plaintext highlighter-rouge">require</code> is relatively cheap,
worst case scenario, Ruby will query the file system 16 times.</p>

<p>But then, every single gem you add to your gemfile adds one extra entry in <code class="language-plaintext highlighter-rouge">$LOAD_PATH</code>, and will in turn likely call <code class="language-plaintext highlighter-rouge">require</code>
even more times.
Meaning that starting a Ruby program isn’t linear, but much worse. The cost is roughly <code class="language-plaintext highlighter-rouge">O(N*M)</code> with <code class="language-plaintext highlighter-rouge">N</code> being <code class="language-plaintext highlighter-rouge">$LOAD_PATH.size</code> and <code class="language-plaintext highlighter-rouge">M</code> being <code class="language-plaintext highlighter-rouge">$LOADED_FEATURES.size</code>.</p>

<p>In other words, an application with 400 gems is likely way more than twice as slow to boot compared to an application with 200 gems.</p>

<p>That’s a problem <a href="https://www.youtube.com/watch?v=kwkbrOwLsZY">Aaron Patterson explained in his GORUCO 2015 talk</a>,
and that talk in turn inspired me to write <a href="https://github.com/byroot/bootscale/"><code class="language-plaintext highlighter-rouge">bootscale</code></a>,
which we used with success in the Shopify monolith for a while, but while very effective, it was quite brittle, so it remained
mostly confidential.</p>

<p>Later on, my former colleague Burke Libbey, reimplemented the same idea, but in a much more robust and cleaner way,
giving birth to <code class="language-plaintext highlighter-rouge">bootsnap</code>, and facilitating its adoption across the community.</p>

<p>But enough history, let’s dive into what it does.</p>

<h3 id="load-path-caching">Load Path Caching.</h3>

<p>While this is not the only thing Bootsnap does, its main feature is load path caching.</p>

<p>The idea is simple, instead of repeatedly testing the existence of files over and over, Bootsnap eagerly
scan all directories in <code class="language-plaintext highlighter-rouge">$LOAD_PATH</code> to build a large map of all potentially requirable files, as to provide a way to
look them up with just a <code class="language-plaintext highlighter-rouge">O(1)</code> hash lookup.</p>

<p>It’s a bit over-simplified, but in essence, Bootsnap’s cache is just a big hash:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="vi">@cache</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s2">"active_support/core_ext.rb"</span> <span class="o">=&gt;</span> <span class="s2">"/gems/activesupport-8.1.2/lib/active_support/core_ext.rb"</span><span class="p">,</span>
  <span class="s2">"active_support/json.rb"</span> <span class="o">=&gt;</span> <span class="s2">"/gems/activesupport-8.1.2/lib/active_support/json.rb"</span><span class="p">,</span>
  <span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Which then allows to decorate <code class="language-plaintext highlighter-rouge">Kernel.require</code>, as to cheaply translate relative paths into absolute ones,
hence entirely sidestepping Ruby’s slow search mechanism:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">require</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>
  <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">absolute_path?</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">feature</span><span class="p">.</span><span class="nf">end_with</span><span class="p">(</span><span class="s2">".rb"</span><span class="p">,</span> <span class="s2">".so"</span><span class="p">)</span>
      <span class="n">feature</span> <span class="o">=</span> <span class="no">Bootsnap</span><span class="o">::</span><span class="no">LoadPathCache</span><span class="p">.</span><span class="nf">lookup</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="n">feature</span> <span class="o">=</span> <span class="no">Bootsnap</span><span class="o">::</span><span class="no">LoadPathCache</span><span class="p">.</span><span class="nf">lookup</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">feature</span><span class="si">}</span><span class="s2">.rb"</span><span class="p">)</span> <span class="o">||</span> <span class="no">Bootsnap</span><span class="o">::</span><span class="no">LoadPathCache</span><span class="p">.</span><span class="nf">lookup</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">feature</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
  <span class="n">require_without_bootsnap</span><span class="p">(</span><span class="n">feature</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There are, of course, many subtle corner cases Bootsnap has to deal with to reproduce Ruby’s behavior as accurately as possible,
but conceptually, Bootsnap is quite simple and reliable.</p>

<p>Thanks to this cache, instead of scanning and checking the existence of up to <code class="language-plaintext highlighter-rouge">2*N</code> files in every <code class="language-plaintext highlighter-rouge">require</code> call, we only
need to pay a one-time cost, which is quickly amortized.</p>

<h3 id="cache-invalidation">Cache Invalidation</h3>

<p>Now, the problem with adding a cache is that you need to know when it’s no longer valid, and
as the famous maxim says, cache invalidation is one of the hardest problems in programming.</p>

<p>Hence, Bootsnap can’t just persist its cache across CI builds, as any added or removed file in any of the load paths
MUST invalidate the cache.</p>

<p>The way Bootsnap does it is that in the cache, it records the <code class="language-plaintext highlighter-rouge">mtime</code> of the scanned directories.
Whenever you add or remove a file in a directory, the directory’s <code class="language-plaintext highlighter-rouge">mtime</code> is updated.
However, its parent directory <code class="language-plaintext highlighter-rouge">mtime</code> is left unchanged:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"fileutils"</span>

<span class="no">FileUtils</span><span class="p">.</span><span class="nf">rm_rf</span><span class="p">(</span><span class="s2">"/tmp/test/"</span><span class="p">)</span>
<span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="s2">"/tmp/test/dir/subdir"</span><span class="p">)</span>

<span class="nb">p</span> <span class="no">File</span><span class="p">.</span><span class="nf">mtime</span><span class="p">(</span><span class="s2">"/tmp/test/dir"</span><span class="p">).</span><span class="nf">to_f</span>        <span class="c1"># 1776351416.5027087</span>
<span class="nb">p</span> <span class="no">File</span><span class="p">.</span><span class="nf">mtime</span><span class="p">(</span><span class="s2">"/tmp/test/dir/subdir"</span><span class="p">).</span><span class="nf">to_f</span> <span class="c1"># 1776351416.5027075</span>

<span class="no">File</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="s2">"/tmp/test/dir/subdir/file.txt"</span><span class="p">,</span> <span class="s2">"1"</span><span class="p">)</span>

<span class="nb">p</span> <span class="no">File</span><span class="p">.</span><span class="nf">mtime</span><span class="p">(</span><span class="s2">"/tmp/test/dir"</span><span class="p">).</span><span class="nf">to_f</span>        <span class="c1"># 1776351416.5027087</span>
<span class="nb">p</span> <span class="no">File</span><span class="p">.</span><span class="nf">mtime</span><span class="p">(</span><span class="s2">"/tmp/test/dir/subdir"</span><span class="p">).</span><span class="nf">to_f</span> <span class="c1"># 1776351416.502805</span>
</code></pre></div></div>

<p>If file and directory <code class="language-plaintext highlighter-rouge">mtime</code> were updated recursively, that would be extremely powerful for many tools like Bootsnap,
however I suspect it wasn’t done this way out of performance concerns.</p>

<p>As such, Bootsnap has to recursively check the <code class="language-plaintext highlighter-rouge">mtime</code> of all directories in all load paths whenever it needs to revalidate the cache.
That’s cheaper than rebuilding the full cache, but still potentially thousands of <code class="language-plaintext highlighter-rouge">stat(2)</code> syscalls, so quite costly.</p>

<p>More importantly, on CI systems it’s relatively common to check out code using <code class="language-plaintext highlighter-rouge">git</code>, and <code class="language-plaintext highlighter-rouge">git</code> doesn’t care about <code class="language-plaintext highlighter-rouge">mtime</code><sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.
Which means in many cases, this cache won’t be re-usable across builds or machines, hence it will need to be rebuilt every time,
making the scanning performance important.</p>

<h3 id="n1-syscalls">N+1 Syscalls</h3>

<p>On the Intercom monorepo, scanning all load paths was just shy of a second<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>, and as I said previously, even a split
second was important to me, given it was part of the “setup time”.
So I was keen on finding ways to improve it.</p>

<p>To understand the issue, let’s look at a very simplified implementation of Bootsnap’s load path scanner:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">scan</span><span class="p">(</span><span class="n">dir_path</span><span class="p">,</span> <span class="n">requirables</span> <span class="o">=</span> <span class="p">[],</span> <span class="n">directories</span> <span class="o">=</span> <span class="p">[])</span>
  <span class="no">Dir</span><span class="p">.</span><span class="nf">foreach</span><span class="p">(</span><span class="n">dir_path</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">name</span><span class="o">|</span>
    <span class="n">path</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">absolute_dir_path</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
      <span class="n">directories</span> <span class="o">&lt;&lt;</span> <span class="n">path</span>
      <span class="nb">scan</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">requirables</span><span class="p">,</span> <span class="n">directories</span><span class="p">)</span>
    <span class="k">elsif</span> <span class="nb">name</span><span class="p">.</span><span class="nf">end_with?</span><span class="p">(</span><span class="s2">".rb"</span><span class="p">,</span> <span class="s2">".so"</span><span class="p">)</span>
      <span class="n">requirables</span> <span class="o">&lt;&lt;</span> <span class="nb">name</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Simply put, for each entry of a directory, if it’s also a directory, we record it in the list and recurse, otherwise,
if it has an extension we care about (<code class="language-plaintext highlighter-rouge">.rb</code>, <code class="language-plaintext highlighter-rouge">.so</code>, <code class="language-plaintext highlighter-rouge">.bundle</code>, etc) we add it to the list of requirable files.</p>

<p>At that stage, you might wonder why all this code isn’t just <code class="language-plaintext highlighter-rouge">Dir["**/*.{rb,so}"]</code>, but Bootsnap needs to exactly match Ruby’s behavior
otherwise it could change the behavior of programs.
So while it is far fetched to imagine a project with directories named <code class="language-plaintext highlighter-rouge">something.rb</code> or <code class="language-plaintext highlighter-rouge">somethingelse.so</code>, assuming such
directories don’t exist would be a correctness bug.</p>

<p>There’s also some other subtleties, like needing to keep a list of all directories, even the ones not yet containing requirable files,
so as to be able to revalidate the cache.</p>

<p>As mentioned, <a href="https://github.com/rails/bootsnap/blob/d4ad1673943b416cd46491ffe236df29862ce37f/lib/bootsnap/load_path_cache/path_scanner.rb#L23-L66">the real version is noticeably more complex</a>,
but from a performance standpoint, they’re equivalent.</p>

<p>Anyways, the problem with that path scanner, is that it’s essentially the system programming equivalent to what N+1 queries
are to web programming, except with system calls.</p>

<p>While they’re much faster than a database query, syscalls are still something you wish to avoid or minimize
when working at this level of abstraction, as they involve a context switch into the kernel.</p>

<p>The actual cost of a syscall depends on which system the program work on.
For instance, Linux syscalls are generally much faster than macOS ones, and <a href="https://en.wikipedia.org/wiki/VDSO">some calls don’t even need a context switch</a>.
On the other side, <a href="https://discuss.rubyonrails.org/t/why-is-rails-boot-so-slow-on-macos/74021/45">some macOS syscalls like <code class="language-plaintext highlighter-rouge">open(2)</code> have a massive overhead because of some security features</a>.</p>

<p>In this specific case, for each entry in the directory, we’ll call <code class="language-plaintext highlighter-rouge">File.directory?</code>, which results in a <code class="language-plaintext highlighter-rouge">stat(2)</code> syscall.
But this <code class="language-plaintext highlighter-rouge">N+1</code> syscall was a long-known issue even in early UNIX programs written in C, that’s why at least on Linux and BSD,
<code class="language-plaintext highlighter-rouge">readdir(3)</code>, which is the API to read the content of a directory, exposes a <code class="language-plaintext highlighter-rouge">d_type</code> member, allowing us to know whether that
directory entry is a directory, a regular file, or something else without needing to issue a <code class="language-plaintext highlighter-rouge">stat(2)</code> call.</p>

<p>Unfortunately, while Ruby does use <code class="language-plaintext highlighter-rouge">d_type</code> internally to speed up methods like <code class="language-plaintext highlighter-rouge">Dir[]</code>, it doesn’t expose it to the <code class="language-plaintext highlighter-rouge">Dir.foreach</code>
block.</p>

<p>This wasn’t a new issue for me, I knew about that problem back in 2020, as back then, I was already looking at speeding
up Bootsnap and Zeitwerk (<a href="https://github.com/fxn/zeitwerk/blob/806795d302840a7e96612b88ff45f231ea4318b0/lib/zeitwerk/loader.rb#L376-L391">which have the same issue</a>).
That’s why back then, I opened <a href="https://bugs.ruby-lang.org/issues/17001">a feature request for a <code class="language-plaintext highlighter-rouge">Dir.scan</code> method</a>,
unfortunately that ticket never got any traction.</p>

<p>So I thought it was time to try again.</p>

<h3 id="implementing-dirscan">Implementing <code class="language-plaintext highlighter-rouge">Dir.scan</code></h3>

<p>Instead of reviving the old ticket, I decided to try again from scratch, and this time to include a prototype implementation.
Instead of adding a new method, I decided extend the existing <code class="language-plaintext highlighter-rouge">Dir</code> methods like <code class="language-plaintext highlighter-rouge">foreach</code>, so that they’d yield a second parameter to
represent the file type as a symbol.</p>

<p>This initial prototype sped up recursively walking directories by about 2x.
Soon after, Nobuyoshi Nakada, AKA <a href="https://github.com/nobu">nobu</a> noticed my pull request and implemented
<a href="https://github.com/ruby/ruby/commit/9acf67057b9bc6f855b2c37e41c1a2f91eae643a">an alternative version that yielded <code class="language-plaintext highlighter-rouge">File::Stat</code> objects instead of symbols</a>,
which I thought was much more elegant, so I opened <a href="https://bugs.ruby-lang.org/issues/21800">a new feature request proposing his API</a>.</p>

<p>But from experience, I knew that even in the best-case scenario, I’d need to wait for the next developer meeting before I’d
get an OK from Matz, which means it would only make it into Ruby 4.1.</p>

<p>Waiting a full year to improve Bootsnap wasn’t very satisfactory, but since Bootsnap already ships with a C extension,
I thought I could just <a href="https://github.com/rails/bootsnap/pull/511">implement that API in Bootsnap itself</a>,
to get the performance improvement immediately.</p>

<p>Benchmarked on Intercom’s monolith (only the repo, not the dependencies) showed the same 2x improvement:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin25]
Warming up --------------------------------------
                orig     1.000 i/100ms
                 opt     1.000 i/100ms
Calculating -------------------------------------
                orig      1.988 (± 0.0%) i/s  (502.94 ms/i) -     10.000 in   5.031382s
                 opt      4.297 (± 0.0%) i/s  (232.70 ms/i) -     22.000 in   5.120236s

Comparison:
                orig:        2.0 i/s
                 opt:        4.3 i/s - 2.16x  faster
</code></pre></div></div>

<p>Bootsnap was now able to scan <code class="language-plaintext highlighter-rouge">~32k</code> files in <code class="language-plaintext highlighter-rouge">~10k</code> repositories in 230ms, while the previous implementation needed
500ms.</p>

<p>Later on, my Ruby feature request was discussed at the developer meeting, and a few concerns were raised, notably
it was considered that changing the signature of existing methods could cause backward compatibility issues.</p>

<p>Instead, after a few rounds of discussion, we settled on a new method: <code class="language-plaintext highlighter-rouge">Dir.scan</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Dir</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">type</span><span class="o">|</span>
  <span class="k">case</span> <span class="n">type</span>
  <span class="k">when</span> <span class="ss">:directory</span>
    <span class="c1"># ...</span>
  <span class="k">when</span> <span class="ss">:link</span>
    <span class="c1"># ...</span>
  <span class="k">when</span> <span class="ss">:file</span>
    <span class="c1"># ...</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This new feature will be available in Ruby 4.1.0.</p>

<h2 id="other-path-methods">Other Path Methods</h2>

<p>While this <code class="language-plaintext highlighter-rouge">N+1</code> issue was definitely the main hotspot, and this 2x win felt good, I tend to treat performance gains like
mushroom hunting.
When you find a mushroom, it usually means that it’s an area where they grow well, and that no other mushroom hunter has passed by recently.</p>

<p>Well, in my experience, unoptimized code is the same.
If you find a piece of code that is much slower than it could be, it suggests nobody ever needed it to be faster, hence it’s
likely the same for other pieces of code in the same area.</p>

<p>In this case, another Ruby method Bootsnap calls a lot was <code class="language-plaintext highlighter-rouge">File.join</code>, and while it wasn’t a major hotspot, it still was
visible on boot profile, so I figured it was worth looking into.</p>

<p>But how are you supposed to tell if some code is slower than it should be?</p>

<p>What commonly slows down a given method is its handling of corner cases, so a good comparison point is a naive implementation
that only considers the happy path.
In our case, the most common usage of <code class="language-plaintext highlighter-rouge">File.join</code> by far is basically just a concatenation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">file_join</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">child</span><span class="p">)</span>
  <span class="s2">"</span><span class="si">#{</span><span class="n">parent</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">child</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So if we benchmark this simplistic implementation against the real <code class="language-plaintext highlighter-rouge">File.join</code>, we should have a vague idea of how much
performance is left on the table:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>
<span class="nb">require</span> <span class="s2">"benchmark/ips"</span>

<span class="n">dir</span> <span class="o">=</span> <span class="s2">"/Users/byroot/src/github.com/byroot/ruby/build"</span>
<span class="n">entry</span> <span class="o">=</span> <span class="s2">"path/to/file.txt"</span>

<span class="no">Benchmark</span><span class="p">.</span><span class="nf">ips</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s2">"File.join"</span><span class="p">)</span> <span class="p">{</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">dir</span><span class="p">,</span> <span class="n">entry</span><span class="p">)</span> <span class="p">}</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s2">"interpolation"</span><span class="p">)</span> <span class="p">{</span> <span class="s2">"</span><span class="si">#{</span><span class="n">dir</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">entry</span><span class="si">}</span><span class="s2">"</span> <span class="p">}</span>
  <span class="n">x</span><span class="p">.</span><span class="nf">compare!</span><span class="p">(</span><span class="ss">order: :baseline</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ruby 4.0.2 (2026-03-17 revision d3da9fec82) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
           File.join   429.375k i/100ms
       interpolation     1.560M i/100ms
Calculating -------------------------------------
           File.join      4.336M (± 0.2%) i/s  (230.65 ns/i) -     21.898M in   5.050870s
       interpolation     17.501M (± 0.5%) i/s   (57.14 ns/i) -     88.905M in   5.079969s

Comparison:
           File.join:  4335527.0 i/s
       interpolation: 17501462.6 i/s - 4.04x  faster
</code></pre></div></div>

<p>Bingo!
A 4x difference really didn’t pass the smell test, so I immediately profiled <code class="language-plaintext highlighter-rouge">File.join</code> called 10 million times in a loop:</p>

<p><img src="/assets/articles/paths/file-join-profile.png" alt="Flame graph of Ruby's File.join" /></p>

<p><a href="https://share.firefox.dev/4cRhU3x">Full profile</a></p>

<p>What immediately surprised me was that <code class="language-plaintext highlighter-rouge">File.join</code> was spending over half its time in encoding-related functions (<code class="language-plaintext highlighter-rouge">rb_enc_*</code>),
most notably 33% in <code class="language-plaintext highlighter-rouge">rb_enc_mbclen</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Queries the number of bytes of the character at the passed pointer.
 *
 * @param[in]  p    Pointer to a character's first byte.
 * @param[in]  e    End of the string that has `p`.
 * @param[in]  enc  Encoding of the string.
 * @return     If the character at `p` does  not end until `e`, number of bytes
 *             between `p`  and `e`.   Otherwise the number  of bytes  that the
 *             character at `p` is encoded.
 *
 * @internal
 *
 * Strictly speaking there  are chances when `p`  points to a middle  byte of a
 * wide character.   This function  returns "the  number of  bytes from  `p` to
 * nearest of either `e` or the next character boundary", if you go strict.
 */</span>
<span class="kt">int</span> <span class="nf">rb_enc_mbclen</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">p</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">e</span><span class="p">,</span> <span class="n">rb_encoding</span> <span class="o">*</span><span class="n">enc</span><span class="p">);</span>
</code></pre></div></div>

<p>Without even looking at the code, this told me there was a large potential for an easy optimization, because <code class="language-plaintext highlighter-rouge">File.join</code>,
like all other Ruby methods handling paths, rejects paths encoded with non-ASCII compatible encodings:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"a"</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="no">Encoding</span><span class="o">::</span><span class="no">UTF_16LE</span><span class="p">),</span> <span class="s2">"b"</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="no">Encoding</span><span class="o">::</span><span class="no">UTF_16LE</span><span class="p">))</span>
<span class="c1"># =&gt; 'File.join': path name must be ASCII-compatible (UTF-16LE): "a" (Encoding::CompatibilityError)</span>
</code></pre></div></div>

<p>So I thought this could be some leftover from a long time ago that could be pruned, hence I started digging into the git history,
and found that this multi-byte encoding support was added by nobu in January 2012 (<a href="https://github.com/ruby/ruby/commit/ed469831e44f2b5a9384b18e660677b20a5ab664">commit <code class="language-plaintext highlighter-rouge">ed469831</code></a>),
whereas the code that rejects non-ASCII compatible encoding was only added in October 2012 (<a href="https://github.com/ruby/ruby/commit/ad54de2acac70ba2f889892df950508edbc972b7">commit <code class="language-plaintext highlighter-rouge">ad54de2a</code></a>), again by nobu.</p>

<p>Unfortunately, neither commit message was really explicit in its intent, nor linked to a bug ticket or anything like that.
Still, it did look like back in 2012, nobu tried to solve some issues with multi-byte paths, but it was ultimately decided to
only accept ASCII-compatible encodings and reject the others.</p>

<p>But a few years of working on Ruby taught me never to assume nobu made a mistake, so before jumping to that conclusion, I figured I’d ask him, just in case:</p>

<p><img src="/assets/articles/paths/nobu-file-join-question.png" alt="Aking nobu about the reason for multi-byte handling in path methods" /></p>

<p>And after a few hours, he answered me:</p>

<p><img src="/assets/articles/paths/nobu-file-join-answer.png" alt="nobu: The conflict of `0x5c` between the trailing byte in Shift_JIS family and the DOSISH path separator is a VERY well known issue in Japan." /></p>

<p>Indeed, it was no mistake, but some sort of corner case I didn’t know about involving the Japanese Shift JIS encoding.
This wasn’t the first time it happened to me, and probably won’t be the last.</p>

<p>Anyways, in such cases, there is a Wikipedia page that helped me multiple times: <a href="https://en.wikipedia.org/wiki/Japanese_language_and_computers">Japanese language and computers</a><sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.
But let me explain the problem here.</p>

<h3 id="ascii-compatibility">ASCII Compatibility</h3>

<p>Ruby supports over a hundred string encodings, and some of them are defined as “ASCII-compatible”, which isn’t a very well-defined concept.
According to Ruby, both UTF-8 and Shift JIS are ASCII-compatible:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">Encoding</span><span class="o">::</span><span class="no">UTF_8</span><span class="p">.</span><span class="nf">ascii_compatible?</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="o">&gt;&gt;</span> <span class="no">Encoding</span><span class="o">::</span><span class="no">Shift_JIS</span><span class="p">.</span><span class="nf">ascii_compatible?</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
</code></pre></div></div>

<p>Which in a way isn’t wrong, because both are ASCII superset, meaning valid ASCII is both valid UTF-8 and valid Shift JIS.</p>

<p>However, UTF-8’s killer feature is that it’s way more ASCII compatible than previous multi-byte encodings.
All UTF-8 multibyte characters only use codes outside the ASCII range (so higher than <code class="language-plaintext highlighter-rouge">127</code>).
Thanks to this, simple ASCII operations like searching for a specific character in the ASCII range can remain simple fixed-length operations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">backslash?</span><span class="p">(</span><span class="n">string</span><span class="p">)</span>
  <span class="n">string</span><span class="p">.</span><span class="nf">each_byte</span> <span class="k">do</span> <span class="o">|</span><span class="n">byte</span><span class="o">|</span>
    <span class="k">return</span> <span class="kp">true</span> <span class="k">if</span> <span class="n">byte</span> <span class="o">==</span> <span class="mh">0x5c</span> <span class="c1"># `\` is 0x5c in ASCII </span>
  <span class="k">end</span>
  
  <span class="kp">false</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In other words, with UTF-8, if you see a <code class="language-plaintext highlighter-rouge">0x5c</code> byte, you know for sure it’s a backslash character, whereas with Shift-JIS,
it may be a backslash character, or it may be a continuation byte of a multi-byte character.
For example, <code class="language-plaintext highlighter-rouge">構</code> is encoded as <code class="language-plaintext highlighter-rouge">0x8d 0x5c</code>.
Hence, you can’t efficiently treat Shift-JIS as ASCII, you must use a lookup table to check the width of every character,
which is what <code class="language-plaintext highlighter-rouge">rb_enc_mbclen</code> does (<code class="language-plaintext highlighter-rouge">mbclen</code> -&gt; multi-byte character length), and it’s a very costly operation compared to
just iterating over a stream of bytes.</p>

<p>But ultimately, it’s fair to assume the overwhelming majority of paths passed to <code class="language-plaintext highlighter-rouge">File.join</code> are encoded in UTF-8 or even pure-ASCII,
as such, I could implement a fast path for these encodings and keep the more complex algorithm for the others.</p>

<p>That’s something I already did a few years prior for various string methods, such that Ruby already had a helper to check
for such encodings:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kr">inline</span> <span class="n">bool</span>
<span class="nf">rb_str_encindex_fastpath</span><span class="p">(</span><span class="kt">int</span> <span class="n">encindex</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// The overwhelming majority of strings are in one of these 3 encodings,</span>
    <span class="c1">// which are all either ASCII or perfect ASCII supersets.</span>
    <span class="c1">// Hence you can use fast, single byte algorithms on them, such as `memchr` etc,</span>
    <span class="c1">// without all the overhead of fetching the rb_encoding and using functions such as</span>
    <span class="c1">// rb_enc_mbminlen etc.</span>
    <span class="c1">// Many other encodings could qualify, but they are expected to be rare occurrences,</span>
    <span class="c1">// so it's better to keep that list small.</span>
    <span class="k">switch</span> <span class="p">(</span><span class="n">encindex</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">case</span> <span class="n">ENCINDEX_ASCII_8BIT</span><span class="p">:</span>
      <span class="k">case</span> <span class="n">ENCINDEX_UTF_8</span><span class="p">:</span>
      <span class="k">case</span> <span class="n">ENCINDEX_US_ASCII</span><span class="p">:</span>
        <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
      <span class="nl">default:</span>
        <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Using this helper, <a href="https://github.com/ruby/ruby/commit/6cd4549060a608d8a7e5ee0dde2c4b69b08d7f6e">I implemented a fastpath for <code class="language-plaintext highlighter-rouge">File.join</code></a>,
using single byte comparisons, and that’s when I realized the multi-byte checks weren’t the only thing slowing down <code class="language-plaintext highlighter-rouge">File.join</code>
and several other path handling methods.</p>

<h3 id="reverse-search">Reverse Search</h3>

<p>After every path segment it concatenates, <code class="language-plaintext highlighter-rouge">File.join</code> would call <code class="language-plaintext highlighter-rouge">chompdirsep</code> to find whether
the segment had a trailing path separator.
That’s necessary because <code class="language-plaintext highlighter-rouge">File.join</code> avoids duplicate separators:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"foo/"</span><span class="p">,</span> <span class="s2">"/bar"</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="s2">"foo/bar"</span>
</code></pre></div></div>

<p>But there was something very wrong with its implementation:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">char</span> <span class="o">*</span>
<span class="nf">chompdirsep</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">path</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">end</span><span class="p">,</span> <span class="n">rb_encoding</span> <span class="o">*</span><span class="n">enc</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">path</span> <span class="o">&lt;</span> <span class="n">end</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">isdirsep</span><span class="p">(</span><span class="o">*</span><span class="n">path</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">last</span> <span class="o">=</span> <span class="n">path</span><span class="o">++</span><span class="p">;</span>
            <span class="k">while</span> <span class="p">(</span><span class="n">path</span> <span class="o">&lt;</span> <span class="n">end</span> <span class="o">&amp;&amp;</span> <span class="n">isdirsep</span><span class="p">(</span><span class="o">*</span><span class="n">path</span><span class="p">))</span> <span class="n">path</span><span class="o">++</span><span class="p">;</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">path</span> <span class="o">&gt;=</span> <span class="n">end</span><span class="p">)</span> <span class="k">return</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">last</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="p">{</span>
            <span class="n">Inc</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">end</span><span class="p">,</span> <span class="n">enc</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">path</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can see, the function receives the start and end pointers of the string, and is supposed to
return the position of the last meaningful separator, so that extra trailing separators are eliminated by <code class="language-plaintext highlighter-rouge">File.join</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"foo///"</span><span class="p">,</span> <span class="s2">"/bar"</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="s2">"foo/bar"</span>
</code></pre></div></div>

<p>The logical way to implement such a function would be to start looking from the back of the string, but here it
was scanning the entire string, meaning longer paths were disproportionately slower to join than shorter paths.</p>

<p>I’m not one hundred percent sure why it was implemented that way, probably because the multi-byte aware <code class="language-plaintext highlighter-rouge">Inc</code> macro
was readily available, and implementing a <code class="language-plaintext highlighter-rouge">Dec</code> macro would have been a bit trickier, but technically it should have been doable.</p>

<p>In my case, I only cared about optimizing the fast path, so I inlined a single-byte version of it, which searches for
duplicate separators from the end of the string:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">long</span> <span class="n">trailing_seps</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">while</span> <span class="p">(</span><span class="n">isdirsep</span><span class="p">(</span><span class="n">name</span><span class="p">[</span><span class="n">len</span> <span class="o">-</span> <span class="n">trailing_seps</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]))</span> <span class="p">{</span>
    <span class="n">trailing_seps</span><span class="o">++</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">rb_str_set_len</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">len</span> <span class="o">-</span> <span class="n">trailing_seps</span><span class="p">);</span>
</code></pre></div></div>

<p>And while I was in there, I kept looking for other opportunities.</p>

<h3 id="c-strings">C Strings</h3>

<p>The profile was showing <code class="language-plaintext highlighter-rouge">6.7%</code> of time spent in <code class="language-plaintext highlighter-rouge">rb_string_value_cstr</code>, which, after fixing the multi-byte encoding, was now a much bigger deal.</p>

<p>What that function does is that it ensures that a given Ruby string is also a valid “C string”, which implies two things:</p>

<ul>
  <li>The string is <code class="language-plaintext highlighter-rouge">NULL</code> terminated.</li>
  <li>The string does not contain any <code class="language-plaintext highlighter-rouge">NULL</code> bytes.</li>
</ul>

<p>Most Ruby methods dealing with path, do reject strings containing <code class="language-plaintext highlighter-rouge">NULL</code> bytes because that’s not valid for a file or directory name:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"foo</span><span class="se">\0</span><span class="s2">bar"</span><span class="p">,</span> <span class="s2">"baz"</span><span class="p">)</span>
<span class="p">(</span><span class="n">irb</span><span class="p">):</span><span class="mi">1</span><span class="ss">:in</span> <span class="s1">'File.join'</span><span class="p">:</span> <span class="n">string</span> <span class="n">contains</span> <span class="n">null</span> <span class="n">byte</span> <span class="p">(</span><span class="no">ArgumentError</span><span class="p">)</span>
</code></pre></div></div>

<p>However, we actually don’t really care here if the string is NULL-terminated or not, as all we’re doing is concatenating it,
we’re not passing it to any C-level API that expects a NULL-terminated string.
So <code class="language-plaintext highlighter-rouge">rb_string_value_cstr</code> wasn’t really the right function to call, hence I could replace it with <code class="language-plaintext highlighter-rouge">rb_str_null_check</code>, which only checks
the content of the string.</p>

<h3 id="variadic-arguments">Variadic Arguments</h3>

<p>Another hotpot from the profile was the 10% spent in <code class="language-plaintext highlighter-rouge">rb_ary_new_from_values</code>, which, as its name indicates, creates a new
array.</p>

<p>The reason is that <code class="language-plaintext highlighter-rouge">File.join</code> has some pretty flexible arguments:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"a"</span><span class="p">,</span> <span class="s2">"b"</span><span class="p">,</span> <span class="s2">"c"</span><span class="p">)</span> <span class="o">==</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"a"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"b"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"c"</span><span class="p">]])</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
</code></pre></div></div>

<p>So to simplify the implementation, <code class="language-plaintext highlighter-rouge">File.join</code> was defined to receive all its arguments in an <code class="language-plaintext highlighter-rouge">args</code> Array:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">static</span> <span class="no">VALUE</span>
<span class="n">rb_file_s_join</span><span class="p">(</span><span class="no">VALUE</span> <span class="n">klass</span><span class="p">,</span> <span class="no">VALUE</span> <span class="n">args</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">rb_file_join</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To avoid that extra allocation and copying, I changed it to not create the Array object, and instead receive a pointer
into the stack and the number of arguments:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">static</span> <span class="no">VALUE</span>
<span class="n">rb_file_s_join</span><span class="p">(</span><span class="n">int</span> <span class="n">argc</span><span class="p">,</span> <span class="no">VALUE</span> <span class="o">*</span><span class="n">argv</span><span class="p">,</span> <span class="no">VALUE</span> <span class="n">klass</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">rb_file_join</span><span class="p">(</span><span class="n">argc</span><span class="p">,</span> <span class="n">argv</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Allowing for not allocating that extra array in the simpler cases.</p>

<h3 id="result">Result</h3>

<p>All this combined made the common usages of <code class="language-plaintext highlighter-rouge">File.join</code> over 7 times faster:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>compare-ruby: ruby 4.1.0dev (2026-01-17T14:40:03Z master 00a3b71eaf) +PRISM [arm64-darwin25]
built-ruby: ruby 4.1.0dev (2026-01-18T12:55:15Z spedup-file-join 5948e92e03) +PRISM [arm64-darwin25]
warming up....

|              |compare-ruby|built-ruby|
|:-------------|-----------:|---------:|
|two_strings   |      2.477M|   19.317M|
|              |           -|     7.80x|
|many_strings  |    547.577k|   10.298M|
|              |           -|    18.81x|
|array         |    515.280k|  523.291k|
|              |           -|     1.02x|
|mixed         |    621.840k|  635.422k|
|              |           -|     1.02x|
</code></pre></div></div>

<p>And now, on Ruby <code class="language-plaintext highlighter-rouge">4.1.0dev</code>, using <code class="language-plaintext highlighter-rouge">File.join</code> for two simple paths is faster than using string interpolation:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ruby 4.1.0dev (2026-04-11T18:26:22Z compact-ar-table 06507da144) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
           File.join     1.944M i/100ms
       interpolation     1.716M i/100ms
Calculating -------------------------------------
           File.join     21.750M (± 0.4%) i/s   (45.98 ns/i) -    108.860M in   5.005112s
       interpolation     19.012M (± 0.6%) i/s   (52.60 ns/i) -     96.111M in   5.055419s

Comparison:
           File.join: 21750287.3 i/s
       interpolation: 19012105.0 i/s - 1.14x  slower
</code></pre></div></div>

<p>If you are curious, you can read <a href="https://github.com/ruby/ruby/pull/15898">the full pull request</a>.</p>

<h2 id="other-methods">Other Methods</h2>

<p>After finding such low-hanging fruits in <code class="language-plaintext highlighter-rouge">File.join</code>, I figured other path handling methods likely had similar issues,
and I applied similar optimizations to:</p>

<ul>
  <li><a href="https://github.com/ruby/ruby/pull/15919"><code class="language-plaintext highlighter-rouge">File.basename</code></a></li>
  <li><a href="https://github.com/ruby/ruby/pull/15907"><code class="language-plaintext highlighter-rouge">File.dirname</code></a></li>
  <li><a href="https://github.com/ruby/ruby/pull/15912"><code class="language-plaintext highlighter-rouge">File.extname</code></a></li>
  <li><a href="https://github.com/ruby/ruby/pull/16697"><code class="language-plaintext highlighter-rouge">File.expand_path</code></a></li>
</ul>

<p>Not that any of these were massive hotspots to my knowledge, but I saw no reason not to optimize them too.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Unless you use plugins such as <code class="language-plaintext highlighter-rouge">git-restore-mtime</code>, which have their own performance overhead. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>If I remember correctly. I’m writing this while traveling and can’t double-check the historical data. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>The mere existence of a Wikipedia page with such a title says a lot if you ask me. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="ruby" /><category term="performance" /><summary type="html"><![CDATA[Back in November last year, I started a new job at Intercom, and one of the first projects I got to work on was improving the Intercom monolith CI with some of my new colleagues.]]></summary></entry><entry><title type="html">Frozen String Literals: Past, Present, Future?</title><link href="https://byroot.github.io/ruby/performance/2025/10/28/string-literals.html" rel="alternate" type="text/html" title="Frozen String Literals: Past, Present, Future?" /><published>2025-10-28T08:03:51+00:00</published><updated>2025-10-28T08:03:51+00:00</updated><id>https://byroot.github.io/ruby/performance/2025/10/28/string-literals</id><content type="html" xml:base="https://byroot.github.io/ruby/performance/2025/10/28/string-literals.html"><![CDATA[<p>If you are a Rubyist, you’ve likely been writing <code class="language-plaintext highlighter-rouge"># frozen_string_literal: true</code> at the top of most of your Ruby
source code files, or at the very least, that you’ve seen it in some other projects.</p>

<p>Based on informal discussions at conferences and online, it seems that what this magic comment really is about is not always well understood,
so I figured it would be worth talking about why it’s there, what it does exactly, and what its future might look like.</p>

<h2 id="ruby-strings-are-mutable">Ruby Strings Are Mutable</h2>

<p>Before we can delve into what makes frozen string literals special, we first need to talk about the Ruby String type,
because it’s quite different from the equivalent type in other popular languages.</p>

<p>In the overwhelming majority of popular languages, strings are immutable.
That’s the case in Java, JavaScript, Python, Go, etc.</p>

<p>There are a few exceptions, though, like Perl, PHP<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, C/C++ (except for literals), and of course Ruby:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="n">str</span> <span class="o">=</span> <span class="no">String</span><span class="p">.</span><span class="nf">new</span>
<span class="o">=&gt;</span> <span class="s2">""</span>
<span class="o">&gt;&gt;</span> <span class="n">str</span><span class="p">.</span><span class="nf">object_id</span>
<span class="o">=&gt;</span> <span class="mi">24952</span>
<span class="o">&gt;&gt;</span> <span class="n">str</span> <span class="o">&lt;&lt;</span> <span class="s2">"foo"</span>
<span class="o">=&gt;</span> <span class="s2">"foo"</span>
<span class="o">&gt;&gt;</span> <span class="n">str</span>
<span class="o">=&gt;</span> <span class="s2">"foo"</span>
<span class="o">&gt;&gt;</span> <span class="n">str</span><span class="p">.</span><span class="nf">capitalize!</span>
<span class="o">=&gt;</span> <span class="s2">"Foo"</span>
<span class="o">&gt;&gt;</span> <span class="n">str</span><span class="p">.</span><span class="nf">upcase!</span>
<span class="o">=&gt;</span> <span class="s2">"FOO"</span>
<span class="o">&gt;&gt;</span> <span class="n">str</span>
<span class="o">=&gt;</span> <span class="s2">"FOO"</span>
<span class="o">&gt;&gt;</span> <span class="n">str</span><span class="p">.</span><span class="nf">object_id</span>
<span class="o">=&gt;</span> <span class="mi">24952</span>
</code></pre></div></div>

<p>Implementation-wise, they’re just an array of bytes, with an associated encoding to know how these bytes should be interpreted:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">String</span>
  <span class="nb">attr_reader</span> <span class="ss">:encoding</span>

  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@bytes</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="vi">@encoding</span> <span class="o">=</span> <span class="no">Encoding</span><span class="o">::</span><span class="no">UTF_8</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That too is quite unusual.</p>

<h2 id="string-encoding">String Encoding</h2>

<p>Most languages, especially the ones I listed above, instead have chosen a specific internal encoding, and all strings are encoded that way.
For instance, in Java and JavaScript, strings are encoded in UTF-16 because they were created somewhat at the same time as the first Unicode specification, and at that time, many people thought that surely 16 bits should be enough to encode all possible characters, but that later turned out to be wrong.
Most newer languages uses UTF-8, or a limited set of internal encodings.</p>

<p>For instance, in Python, strings can be encoded in either <code class="language-plaintext highlighter-rouge">ISO-8859-1</code> (AKA Latin 1), UTF-16 or UtF-32.
But from a user perspective, it’s an implementation detail, and you can’t really tell what encoding a particular string is using.
Semantically, strings are Unicode sequences, how that sequence is encoded in memory is abstracted away.</p>

<p>In these languages, whenever you have to handle text in another encoding, you start by re-encoding it into the internal representation.
In Ruby however, strings with different internal encodings can exist in the same program, and Ruby supports over a hundred different encodings:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">Encoding</span><span class="p">.</span><span class="nf">list</span><span class="p">.</span><span class="nf">size</span>
<span class="o">=&gt;</span> <span class="mi">103</span>
</code></pre></div></div>

<p>While I’m not 100% percent certain of why Ruby went that way, I highly suspect it is in big part due to Ruby’s Japanese origin.
In the early days of the Unicode specification, there was an attempt at unifying some of the “common” Chinese, Korean, and Japanese characters,
as what is now called the <a href="https://en.wikipedia.org/wiki/Han_unification">Han unification</a>.
Because of that character unification attempt, Unicode had lots of problems for Japanese text, hence the Japanese IT industry didn’t adopt Unicode as fast as the Western IT industry did, and for a very long time, Japanese-specific encoding such as <a href="https://en.wikipedia.org/wiki/Shift_JIS">Shift JIS</a> remained widespread.</p>

<p>As such, being able to work with Japanese text without going through a forced Unicode conversion was an important feature for a large part of Ruby’s core contributors.</p>

<p>But let’s go back to mutability.</p>

<h2 id="pros-and-cons">Pros And Cons</h2>

<p>Like most things in engineering, both immutable and mutable strings have pros and cons, so it’s not like one choice is inherently superior to the other.</p>

<p>One of the advantages of immutable strings is that you can more easily share them, for instance:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sliced_string</span> <span class="o">=</span> <span class="n">very_long_string</span><span class="p">[</span><span class="mi">1</span><span class="o">..-</span><span class="mi">1</span><span class="p">]</span>
</code></pre></div></div>

<p>In the above case, if strings are mutable, you need to copy all but one of the bytes of <code class="language-plaintext highlighter-rouge">very_long_string</code> into <code class="language-plaintext highlighter-rouge">sliced_string</code>, which can be costly.
But if strings are immutable, you can instead have <code class="language-plaintext highlighter-rouge">sliced_string</code> internally be pointing at the content of <code class="language-plaintext highlighter-rouge">very_long_string</code> with just an offset.
That is what some languages call String Views, or String slices.</p>

<p>Another advantage of immutable strings is that they allow for <a href="https://en.wikipedia.org/wiki/String_interning">interning</a>.
The idea is simple, if strings can’t be mutated, whenever you have multiple instances of strings with identical content, you can coalesce them into a single instance.
This deduplication can be done more or less aggressively, as it’s always a tradeoff in how much CPU time you want to spend searching for duplicates in the hope of saving some memory.</p>

<p>Some other advantages include not having to worry about mutation in multi-threaded code, as well as dictionary keys.
Strings are used a lot as dictionary keys.
If you mutate a string, you change its hash code, and that basically breaks hash tables.</p>

<p>On the other hand, mutable strings are very handy in some scenarios, like to iteratively build a final string:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">buffer</span> <span class="o">=</span> <span class="s2">""</span>
<span class="mi">10</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
  <span class="n">buffer</span> <span class="o">&lt;&lt;</span> <span class="s2">"hello"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Whereas in a language with immutable strings like Java, concatenating strings in a loop is known as a classic performance gotcha:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span> <span class="n">buffer</span> <span class="o">=</span> <span class="s">""</span><span class="o">;</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
  <span class="n">buffer</span> <span class="o">+=</span> <span class="s">"hello"</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>In the above example, on every loop, the <code class="language-plaintext highlighter-rouge">+=</code> operator causes a new string to be allocated, and the content to be copied, which gets exponentially more expensive as the string grows.
Instead, you are supposed to use a different object as a buffer: <code class="language-plaintext highlighter-rouge">StringBuilder</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">StringBuilder</span> <span class="n">buffer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">();</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
  <span class="n">buffer</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="s">"hello"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">buffer</span><span class="o">.</span><span class="na">toString</span><span class="o">();</span>
</code></pre></div></div>

<p>That’s the Java equivalent of appending strings to an array and then calling <code class="language-plaintext highlighter-rouge">array.join("")</code>.
It’s a common enough mistake that at some point the Java compiler gained the ability to <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.18.1">detect that pattern and automatically replace it with the equivalent code using <code class="language-plaintext highlighter-rouge">StringBuilder</code></a>.</p>

<p>While having to use a different buffer type isn’t the end of the world, I do very much like that it’s not necessary in Ruby.</p>

<p>But more generally, the advantage of mutable strings is that for some algorithms, being able to modify the string in place saves a lot of memory allocations and copying.</p>

<h2 id="ruby-actually-has-both">Ruby Actually Has Both</h2>

<p>Earlier in this post, I said Ruby had mutable strings, but it’s not quite true.
Ruby actually has both mutable and immutable strings, because in Ruby, every mutable object can be frozen, hence, Ruby has both mutable and immutable strings, and it takes advantage of this.</p>

<p>A fun way to poke at Ruby internals is through <a href="https://docs.ruby-lang.org/en/3.4/ObjectSpace.html#method-i-dump">the <code class="language-plaintext highlighter-rouge">ObjectSpace.dump</code> method</a>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"json"</span>
<span class="nb">require</span> <span class="s2">"objspace"</span>

<span class="k">def</span> <span class="nf">dump</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
  <span class="no">JSON</span><span class="p">.</span><span class="nf">pretty_generate</span><span class="p">(</span><span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">obj</span><span class="p">)))</span>
<span class="k">end</span>

<span class="n">str</span> <span class="o">=</span> <span class="s2">"Hello World"</span> <span class="o">*</span> <span class="mi">80</span>
<span class="nb">puts</span> <span class="n">dump</span><span class="p">(</span><span class="n">str</span><span class="p">)</span>
</code></pre></div></div>

<p>The above script will output something like:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x105068e10"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"STRING"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"slot_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">40</span><span class="p">,</span><span class="w">
  </span><span class="nl">"bytesize"</span><span class="p">:</span><span class="w"> </span><span class="mi">880</span><span class="p">,</span><span class="w">
  </span><span class="nl">"memsize"</span><span class="p">:</span><span class="w"> </span><span class="mi">921</span><span class="p">,</span><span class="w">
  </span><span class="err">...</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>It tells us the string content is <code class="language-plaintext highlighter-rouge">880B</code> (<code class="language-plaintext highlighter-rouge">bytesize</code>) and that Ruby allocated a <code class="language-plaintext highlighter-rouge">40B</code> wide slot (<code class="language-plaintext highlighter-rouge">slot_size</code>),
hence the string content is stored in an external buffer for a total of <code class="language-plaintext highlighter-rouge">921B</code> (<code class="language-plaintext highlighter-rouge">memsize</code>).</p>

<p>Now, look what happens if we slice that string:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"json"</span>
<span class="nb">require</span> <span class="s2">"objspace"</span>

<span class="k">def</span> <span class="nf">dump</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
  <span class="no">JSON</span><span class="p">.</span><span class="nf">pretty_generate</span><span class="p">(</span><span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">obj</span><span class="p">)))</span>
<span class="k">end</span>

<span class="n">str</span> <span class="o">=</span> <span class="s2">"Hello World"</span> <span class="o">*</span> <span class="mi">80</span>
<span class="nb">puts</span> <span class="s2">"initial str: </span><span class="si">#{</span><span class="n">dump</span><span class="p">(</span><span class="n">str</span><span class="p">)</span><span class="si">}</span><span class="se">\n</span><span class="s2">"</span>

<span class="n">slice</span> <span class="o">=</span> <span class="n">str</span><span class="p">[</span><span class="mi">40</span><span class="o">..-</span><span class="mi">1</span><span class="p">]</span>

<span class="nb">puts</span> <span class="s2">"str after:</span><span class="se">\n</span><span class="si">#{</span><span class="n">dump</span><span class="p">(</span><span class="n">str</span><span class="p">)</span><span class="si">}</span><span class="se">\n</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"slice:</span><span class="se">\n</span><span class="si">#{</span><span class="n">dump</span><span class="p">(</span><span class="n">slice</span><span class="p">)</span><span class="si">}</span><span class="se">\n</span><span class="s2">"</span>
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">str</span><span class="w"> </span><span class="err">after:</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x105178e18"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"STRING"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"slot_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">40</span><span class="p">,</span><span class="w">
  </span><span class="nl">"shared"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"references"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"0x1051786c0"</span><span class="w"> </span><span class="p">],</span><span class="w">
  </span><span class="nl">"memsize"</span><span class="p">:</span><span class="w"> </span><span class="mi">40</span><span class="p">,</span><span class="w">
  </span><span class="err">...</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">slice:</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x1051786e8"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"STRING"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"slot_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">40</span><span class="p">,</span><span class="w">
  </span><span class="nl">"shared"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"references"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"0x1051786c0"</span><span class="w"> </span><span class="p">],</span><span class="w">
  </span><span class="nl">"memsize"</span><span class="p">:</span><span class="w"> </span><span class="mi">40</span><span class="p">,</span><span class="w">
  </span><span class="err">...</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Now, both <code class="language-plaintext highlighter-rouge">str</code> and <code class="language-plaintext highlighter-rouge">slice</code> have the <code class="language-plaintext highlighter-rouge">shared: true</code> attribute, which indicates that they’re not actually owning their content, they are pointing inside another String object.
You can also see that both <code class="language-plaintext highlighter-rouge">str</code> and <code class="language-plaintext highlighter-rouge">slice</code> have a reference to the same object at address: <code class="language-plaintext highlighter-rouge">0x1051786c0</code>.
So even though it has mutable strings, Ruby is still able to optimize some operations using “string views” like languages with immutable strings.
However, since <code class="language-plaintext highlighter-rouge">str</code> is mutable, Ruby couldn’t directly create a string view that references <code class="language-plaintext highlighter-rouge">str</code>, it first had to transfer the buffer ownership to a third String object, and that one is immutable.
But if <code class="language-plaintext highlighter-rouge">str</code> was frozen, Ruby would have been able to directly create <code class="language-plaintext highlighter-rouge">slice</code> as a view inside <code class="language-plaintext highlighter-rouge">str</code>.</p>

<p>Similarly, when I was listing some of the pros and cons of mutable strings, I mentioned how mutable strings are a problem when used as hash table keys.
Perhaps you’ve never noticed it, but to avoid this problem, Ruby automatically freezes string keys in Hash:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="n">str</span> <span class="o">=</span> <span class="s2">"test"</span>
<span class="o">=&gt;</span> <span class="s2">"test"</span>
<span class="o">&gt;&gt;</span> <span class="n">str</span><span class="p">.</span><span class="nf">frozen?</span>
<span class="o">=&gt;</span> <span class="kp">false</span>
<span class="o">&gt;&gt;</span> <span class="nb">hash</span> <span class="o">=</span> <span class="p">{</span> <span class="n">str</span> <span class="o">=&gt;</span> <span class="mi">1</span> <span class="p">}</span>
<span class="o">=&gt;</span> <span class="p">{</span><span class="s2">"test"</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">}</span>
<span class="o">&gt;&gt;</span> <span class="nb">hash</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span>
<span class="o">=&gt;</span> <span class="s2">"test"</span>
<span class="o">&gt;&gt;</span> <span class="nb">hash</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span><span class="p">.</span><span class="nf">frozen?</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="o">&gt;&gt;</span> <span class="p">[</span><span class="n">str</span><span class="p">.</span><span class="nf">object_id</span><span class="p">,</span> <span class="nb">hash</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span><span class="p">.</span><span class="nf">object_id</span><span class="p">]</span>
<span class="o">=&gt;</span> <span class="p">[</span><span class="mi">16</span><span class="p">,</span> <span class="mi">24</span><span class="p">]</span>
</code></pre></div></div>

<p>As you can see, here Ruby couldn’t directly use the <code class="language-plaintext highlighter-rouge">str</code> string as a Hash key, it first had to make a frozen copy of it.
Here, too, if <code class="language-plaintext highlighter-rouge">str</code> was frozen, Ruby could have saved the extra work of duplicating this string.</p>

<p>I believe that illustrates the common tradeoffs at play with mutable strings.
On one hand, they can be much more efficient, allowing for in-place modifications, but on the other hand, they impose extra allocations and copying to protect yourself from mutations.</p>

<h2 id="the-history-of-frozen-string-literal">The History Of Frozen String Literal</h2>

<p>To avoid this extra copying overhead, it used to be a fairly common optimization technique to store string literals in constants.
For instance, you can see this idiom in <a href="https://github.com/rack/rack/commit/8b8690bcb7762cde729088c2abdacb610ebea1f7">a 17 years old patch to rack</a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Rack</span>
  <span class="k">class</span> <span class="nc">MethodOverride</span>
    <span class="no">METHOD_OVERRIDE_PARAM_KEY</span> <span class="o">=</span> <span class="s2">"_method"</span><span class="p">.</span><span class="nf">freeze</span>
    <span class="no">HTTP_METHOD_OVERRIDE_HEADER</span> <span class="o">=</span> <span class="s2">"HTTP_X_HTTP_METHOD_OVERRIDE"</span><span class="p">.</span><span class="nf">freeze</span>

    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="c1"># ...</span>
      <span class="nb">method</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="no">POST</span><span class="p">[</span><span class="no">METHOD_OVERRIDE_PARAM_KEY</span><span class="p">]</span> <span class="o">||</span>
        <span class="n">env</span><span class="p">[</span><span class="no">HTTP_METHOD_OVERRIDE_HEADER</span><span class="p">]</span>
      <span class="c1"># ...</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It’s this pattern that led <a href="https://github.com/haileys">Hailey Somerville</a> from GitHub to open <a href="https://bugs.ruby-lang.org/issues/8579">a feature request to propose a new syntax for frozen string literals</a>: <code class="language-plaintext highlighter-rouge">%f</code>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">req</span><span class="o">.</span><span class="no">POST</span><span class="p">[</span><span class="o">%</span><span class="n">f</span><span class="p">(</span><span class="n">_method</span><span class="p">)]</span> <span class="o">||</span> <span class="n">env</span><span class="p">[</span><span class="o">%</span><span class="n">f</span><span class="p">(</span><span class="no">HTTP_X_HTTP_METHOD_OVERRIDE</span><span class="p">)]</span>
</code></pre></div></div>

<p>This syntax wasn’t accepted, but as a counter proposal, <a href="https://github.com/mame">Yusuke Endoh (mame)</a> suggested an “f suffix”:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">req</span><span class="o">.</span><span class="no">POST</span><span class="p">[</span><span class="s2">"_method"</span><span class="n">f</span><span class="p">]</span> <span class="o">||</span> <span class="n">env</span><span class="p">[</span><span class="s2">"HTTP_X_HTTP_METHOD_OVERRIDE"</span><span class="n">f</span><span class="p">]</span>
</code></pre></div></div>

<p>This one was accepted and implemented in Ruby <code class="language-plaintext highlighter-rouge">2.1.0dev</code>.</p>

<p>However, many core developers didn’t like this new syntax, so even after its implementation, multiple counterproposals were made.
Notably, <a href="https://bugs.ruby-lang.org/issues/8976">Akira Tanaka (akr), proposed a file-based directive</a>: <code class="language-plaintext highlighter-rouge"># freeze_string: true</code>, but it didn’t catch on.</p>

<p>However before the final 2.1.0 release, <a href="https://github.com/headius">Charles Nutter</a> <a href="https://bugs.ruby-lang.org/issues/8992">opened another feature request</a>,
and suggested to instead implement a compiler optimization for <code class="language-plaintext highlighter-rouge">String#freeze</code>, so as to provide the same feature but without introducing a new syntax.</p>

<p>If you aren’t familiar with how the Ruby virtual machine works, or virtual machines in general, you may be surprised to hear that Ruby has a compiler, but it absolutely does.</p>

<p>Prior to Ruby 2.1, the program <code class="language-plaintext highlighter-rouge">"Hello World".freeze</code> would be compiled by Ruby into a sequence of two instructions:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">RubyVM</span><span class="o">::</span><span class="no">InstructionSequence</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="sx">%{"Hello World".freeze}</span><span class="p">).</span><span class="nf">disasm</span>
<span class="o">==</span> <span class="ss">disasm: </span><span class="c1">#&lt;ISeq:&lt;compiled&gt;@&lt;compiled&gt;:1 (1,0)-(1,19)&gt;</span>
<span class="mo">0000</span> <span class="n">putstring</span>                              <span class="s2">"Hello World"</span>             <span class="p">(</span>   <span class="mi">1</span><span class="p">)[</span><span class="no">Li</span><span class="p">]</span>
<span class="mo">0002</span> <span class="n">opt_send_without_block</span>                 <span class="o">&lt;</span><span class="n">calldata!mid</span><span class="ss">:freeze</span><span class="p">,</span> <span class="n">argc</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span> <span class="no">ARGS_SIMPLE</span><span class="o">&gt;</span>
<span class="mo">0004</span> <span class="n">leave</span>
</code></pre></div></div>

<p>First, a <code class="language-plaintext highlighter-rouge">putstring</code> instruction to put <code class="language-plaintext highlighter-rouge">"Hello World"</code> on the VM stack, followed by an <code class="language-plaintext highlighter-rouge">opt_send_without_block</code> to call the <code class="language-plaintext highlighter-rouge">#freeze</code> method on it.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">putstring</span><span class="p">(</span><span class="n">frozen_string</span><span class="p">)</span>
  <span class="vi">@stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">frozen_string</span><span class="p">.</span><span class="nf">dup</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>When invoked, the instruction receives a reference to a frozen String object that has been created by the Ruby compiler.
But since the semantics is that the string <code class="language-plaintext highlighter-rouge">#freeze</code> will be called on must be mutable, it has to duplicate it, and it’s the mutable copy that is put on the stack.</p>

<p>In my opinion, the <code class="language-plaintext highlighter-rouge">putstring</code> instruction isn’t correctly named, because its name suggests it just puts the frozen string directly on the stack.
This isn’t consistent with other <code class="language-plaintext highlighter-rouge">put*</code> instructions like <code class="language-plaintext highlighter-rouge">putobject</code>, which directly puts an object on the stack without duping it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">putobject</span><span class="p">(</span><span class="n">object</span><span class="p">)</span>
  <span class="vi">@stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">object</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>But also inconsistent with some other instructions like <code class="language-plaintext highlighter-rouge">duparray</code> and <code class="language-plaintext highlighter-rouge">duphash</code>, which actually behave like <code class="language-plaintext highlighter-rouge">putstring</code> does:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">duparray</span><span class="p">(</span><span class="n">array</span><span class="p">)</span>
  <span class="vi">@stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">array</span><span class="p">.</span><span class="nf">dup</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So it would be much clearer if it had been named <code class="language-plaintext highlighter-rouge">dupstring</code> instead of <code class="language-plaintext highlighter-rouge">putstring</code>.</p>

<p>But anyways, Charles’ suggestion was to have the compiler generate a different set of VM instructions when the <code class="language-plaintext highlighter-rouge">#freeze</code> method is called
on a string literal:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">RubyVM</span><span class="o">::</span><span class="no">InstructionSequence</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="sx">%{"Hello World".freeze}</span><span class="p">).</span><span class="nf">disasm</span>
<span class="o">==</span> <span class="ss">disasm: </span><span class="c1">#&lt;ISeq:&lt;compiled&gt;@&lt;compiled&gt;:1 (1,0)-(1,20)&gt;</span>
<span class="mo">0000</span> <span class="n">opt_str_freeze</span>                         <span class="s2">"Hello World"</span><span class="p">,</span> <span class="o">&lt;</span><span class="n">calldata!mid</span><span class="ss">:freeze</span><span class="p">,</span> <span class="n">argc</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span> <span class="no">ARGS_SIMPLE</span><span class="o">&gt;</span><span class="p">(</span>   <span class="mi">1</span><span class="p">)[</span><span class="no">Li</span><span class="p">]</span>
<span class="mo">0003</span> <span class="n">leave</span>
</code></pre></div></div>

<p>As you can see, on more recent rubies, the <code class="language-plaintext highlighter-rouge">putstring</code> and <code class="language-plaintext highlighter-rouge">opt_send_without_block</code> instructions have been replaced by a single <code class="language-plaintext highlighter-rouge">opt_str_freeze</code>.
Its implementation in pseudo-Ruby would be something like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">opt_str_freeze</span><span class="p">(</span><span class="n">frozen_string</span><span class="p">)</span>
  <span class="k">if</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">string_freeze_was_redefined?</span>
    <span class="vi">@stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">frozen_string</span><span class="p">.</span><span class="nf">dup</span><span class="p">.</span><span class="nf">freeze</span><span class="p">)</span>
  <span class="k">else</span>
    <span class="vi">@stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">frozen_string</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As you can see, to not break semantics, the instruction has to check that <code class="language-plaintext highlighter-rouge">String#freeze</code> hasn’t been redefined, but apart from that cheap precondition, the instruction does strictly less work than before.</p>

<p>This is the feature Ruby 2.1.0 ultimately shipped with in December 2013.</p>

<h2 id="further-optimizations">Further Optimizations</h2>

<p>To further reduce string allocations, in 2014, Aman Karmani (tmm1) and Hailey Somerville (haileys) from GitHub submitted <a href="https://bugs.ruby-lang.org/issues/9382">a patch to add two more optimized instructions, <code class="language-plaintext highlighter-rouge">opt_aref_with</code> and <code class="language-plaintext highlighter-rouge">opt_aset_with</code></a>.</p>

<p>Before their patch, accessing a hash with a string key would cause a string allocation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">RubyVM</span><span class="o">::</span><span class="no">InstructionSequence</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="sx">%{some_hash["str"]}</span><span class="p">).</span><span class="nf">disasm</span>
<span class="o">...</span>
<span class="mo">0003</span> <span class="n">putstring</span>                              <span class="s2">"str"</span>
<span class="mo">0005</span> <span class="n">opt_aref</span>                               <span class="o">&lt;</span><span class="n">calldata!mid</span><span class="ss">:[]</span><span class="p">,</span> <span class="n">argc</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span> <span class="no">ARGS_SIMPLE</span><span class="o">&gt;</span><span class="p">[</span><span class="no">CcCr</span><span class="p">]</span>
<span class="mo">0007</span> <span class="n">leave</span>
</code></pre></div></div>

<p>After the patch, these two instructions were replaced by a single <code class="language-plaintext highlighter-rouge">opt_aref_with</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">RubyVM</span><span class="o">::</span><span class="no">InstructionSequence</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="sx">%{some_hash["str"]}</span><span class="p">).</span><span class="nf">disasm</span>
<span class="o">...</span>
<span class="mo">0003</span> <span class="n">opt_aref_with</span>                          <span class="s2">"str"</span><span class="p">,</span> <span class="o">&lt;</span><span class="n">calldata!mid</span><span class="ss">:[]</span><span class="p">,</span> <span class="n">argc</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span> <span class="no">ARGS_SIMPLE</span><span class="o">&gt;</span>
<span class="mo">0006</span> <span class="n">leave</span>
</code></pre></div></div>

<p>Similar to <code class="language-plaintext highlighter-rouge">opt_str_freeze</code>, these instructions would check if the method is being called on a Hash, and if <code class="language-plaintext highlighter-rouge">Hash#[]</code> hadn’t been redefined.
When both conditions are true, the instruction would be able to look up in the hash without first copying the string.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">opt_aref_with</span><span class="p">(</span><span class="n">frozen_string</span><span class="p">)</span>
  <span class="k">if</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">hash_aref_was_redefined?</span> <span class="o">||</span> <span class="o">!</span><span class="vi">@stack</span><span class="p">.</span><span class="nf">last</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Hash</span><span class="p">)</span>
    <span class="c1"># fallback</span>
    <span class="vi">@stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">frozen_string</span><span class="p">.</span><span class="nf">dup</span><span class="p">)</span>
    <span class="n">value</span> <span class="o">=</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">call_method</span><span class="p">(</span><span class="ss">:[]</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
    <span class="vi">@stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
  <span class="k">else</span>
    <span class="c1"># fast path</span>
    <span class="nb">hash</span> <span class="o">=</span> <span class="vi">@stack</span><span class="p">.</span><span class="nf">pop</span>
    <span class="n">value</span> <span class="o">=</span> <span class="nb">hash</span><span class="p">[</span><span class="n">frozen_string</span><span class="p">]</span>
    <span class="vi">@stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>According to Aman Karmani, this reduced allocations in GitHub by 3%, which is quite massive for what is a relatively small patch.</p>

<p>As a sidenote, this optimized instruction has <a href="https://bugs.ruby-lang.org/issues/21553">just been removed by Aaron Paterson</a> on the Ruby trunk, because given most performance-sensitive code already uses the magic comment, this optimization no longer yields much benefit.</p>

<h2 id="ruby-30-and-frozen-string-literals">Ruby 3.0 And Frozen String Literals</h2>

<p>Perhaps in part because of that new feature, or perhaps because of other reasons.
The knowledge of the performance impact of all these useless string duplication in Ruby applications started to spread around 2014,
and some community members, notably Richard Scheenman, started to submit <a href="https://github.com/rails/rails/pull/21057">pull requests in Rails</a>,
<a href="https://github.com/rack/rack/pull/737">rack</a> and a bunch of other gems, with some pretty significant results, such as an 11.9% latency reduction on <a href="https://www.codetriage.com/">codetriage.com</a>.</p>

<p>These performance gains were generally too good to pass up, but regardless, many people felt that the resulting code was much more ugly.
So the question of freezing string by default came back regularly, but was always rejected.</p>

<p>Until <a href="https://github.com/ruby/dev-meeting-log/blob/master/2015/DevMeeting-2015-08-20.md#magic-comment-for-frozen-string-literal-by-default">Akira Matsuda (amatsuda) brought the issue again at the Ruby core developer meeting in August 2015</a>,
and there <a href="https://xcancel.com/yukihiro_matz/status/634386185507311616">Matz decided that Ruby string literals would be frozen in Ruby 3.0</a>.</p>

<p>A number of other features to ease the transition were also decided.
First, the <code class="language-plaintext highlighter-rouge"># frozen_string_literal: true</code> magic comment was introduced to help gems prepare for Ruby 3.0.</p>

<p>Then, to ensure that any code that wouldn’t have been made compatible with Ruby 3.0 would remain usable, two Ruby command line options were added: <code class="language-plaintext highlighter-rouge">--enable-frozen-string-literal</code> and <code class="language-plaintext highlighter-rouge">--disable-frozen-string-literal</code>.</p>

<p>This way, once Ruby 3.0 would be released, if your code or one of your dependencies wasn’t compatible yet, you could just set
<code class="language-plaintext highlighter-rouge">RUBYOPT="--disable-frozen-string-literal"</code> and keep going.</p>

<p>And also a <code class="language-plaintext highlighter-rouge">--debug-frozen-string-literal</code> command line option, to help developers.</p>

<p>All these new features were released with Ruby 2.3 in December 2015.</p>

<p>What happens when you run Ruby with <code class="language-plaintext highlighter-rouge">--enable-frozen-string-literal</code> or with the <code class="language-plaintext highlighter-rouge"># frozen_string_literal: true</code> magic comment is that the compiler generates a different bytecode:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">RubyVM</span><span class="o">::</span><span class="no">InstructionSequence</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="sx">%{# frozen_string_literal: true</span><span class="se">\n</span><span class="sx">"Hello World"}</span><span class="p">).</span><span class="nf">disasm</span>
<span class="o">==</span> <span class="ss">disasm: </span><span class="c1">#&lt;ISeq:&lt;compiled&gt;@&lt;compiled&gt;:2 (2,0)-(2,13)&gt;</span>
<span class="mo">0000</span> <span class="n">putobject</span>                              <span class="s2">"Hello World"</span>             <span class="p">(</span>   <span class="mi">2</span><span class="p">)[</span><span class="no">Li</span><span class="p">]</span>
<span class="mo">0002</span> <span class="n">leave</span>
</code></pre></div></div>

<p>Now, instead of the <code class="language-plaintext highlighter-rouge">putstring</code> instruction, the compiler generates a <code class="language-plaintext highlighter-rouge">putobject</code> instruction.
As I mentioned above, this instruction directly puts the frozen string that was created during compilation on the stack, with no extra duplication.</p>

<p>So it’s important to understand that frozen string literals are strictly less work for Ruby than mutable string literals.</p>

<h2 id="community-usage">Community Usage</h2>

<p>Following the release of Ruby 2.3, the Rubocop project added <a href="https://github.com/rubocop/rubocop/pull/2542/commits/425b7469f109f2eae0648b600aa3ad24e85f6e21">a new cop to enforce the use of the <code class="language-plaintext highlighter-rouge"># frozen_string_literal: true</code> comment</a>,
with the intent of helping projects be ready for Ruby 3.0 in the future.</p>

<p>Over the following years, many projects migrated to frozen string literals, <a href="https://github.com/rails/rails/pull/29506">including Rails</a> and <a href="https://github.com/ruby/rake/pull/209">rake</a> in 2017, <a href="https://github.com/rack/rack/pull/1250">Rack in 2018</a>,
and of course a long tail of other projects.</p>

<p>It’s always hard to say with certainty how much a feature is used, but I think it’s safe to say that, aside from a few projects that deliberately chose not to follow suit, a large majority of the actively developed gems did migrate to frozen string literals.
However, many of the more stable and less actively developed gems didn’t.</p>

<p>There was no indication of when Ruby 3.0 would be released, and the lack of compatibility with it wasn’t advertised by warnings or any other methods, hence, few people even knew whether any of their dependencies needed to be updated.</p>

<p>Over time, the magic comment slowly became an incantation most Rubyists follow, in big part because of rubocop, but as far as I know, basically no one was trying to run their application with <code class="language-plaintext highlighter-rouge">--enable-frozen-string-literal</code>, and few even knew about it.</p>

<h2 id="abandoned-plan">Abandoned Plan</h2>

<p>However, <a href="https://bugs.ruby-lang.org/issues/11473#note-53">in October 2019, just before the release of Ruby 2.7, Matz abandoned the plan to make frozen string literal the default for Ruby 3.0</a>.</p>

<blockquote>
  <p>I consider this for years. I REALLY like the idea but I am sure introducing this could cause HUGE compatibility issue, even bigger than Ruby 1.9.
So I officially abandon making frozen-string-literals default (for Ruby3).</p>

  <p>–
Matz</p>
</blockquote>

<p>I must say this decision did surprise me at the time.
I definitely understand not wanting to cause a Python 3 sort of moment, but I don’t think frozen string literals would have caused it,
because ultimately you could always have set <code class="language-plaintext highlighter-rouge">RUBYOPT="--disable-frozen-string-literal"</code> and kept running your applications unchanged if necessary.</p>

<p>I’m pretty sure if Python 3 had a way of running Python 2 code, the migration would have been much less of a big deal.</p>

<p>It was even more surprising to me because Ruby 2.7 also introduced new deprecation warnings in preparation for the keyword argument change in Ruby 3.0, and from my point of view, this breaking change was way bigger than frozen string literals would ever have been.
It caused so many deprecations that a <a href="https://www.ruby-lang.org/en/news/2020/10/02/ruby-2-7-2-released/">Ruby 2.7.2 was later released specifically to turn deprecation warnings off</a>.
And arguably, updating code to support the new keyword argument logic was way more involved than for frozen string literals.
If you have a look at <a href="https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/">the migration guide</a>, it’s fairly long and complex,
whereas frozen string literals only need a few strategically placed <code class="language-plaintext highlighter-rouge">.dup</code> there and there.</p>

<p>As a datapoint, I personally handled the migration of Shopify’s monolith and roughly 700 gem dependencies for both the Ruby 3.0 keyword arguments and for <code class="language-plaintext highlighter-rouge">--enable-frozen-string-literal</code>.
For keyword arguments, I had to send pull requests to almost a hundred gems, as well as change a lot of code in the monolith itself, and some of them were really non-trivial to fix.
For frozen string literals, I only had to send pull requests to 12 gems, and it was just a matter of adding a few <code class="language-plaintext highlighter-rouge">.dup</code> calls.</p>

<p>But anyway, by the time of the Ruby 3.0 release, it had been almost 5 years since the initial plan had been laid out, and most of the performance-sensitive code had migrated to use the magic comment, so this abandonment didn’t spark much discussion, and few people noticed.</p>

<h2 id="new-standards">New Standards</h2>

<p>Until four years later, in January 2024, I started hearing about <code class="language-plaintext highlighter-rouge">standardrb</code> and how <a href="https://github.com/standardrb/standard/pull/181">it doesn’t enforce the presence of the frozen string literal magic comment</a>.
I also saw a few projects starting to remove them, or new projects deliberately not adding them, because this extra comment at the top is seen as cruft.</p>

<p>And I must say I agree.
I hate that comment.</p>

<p>Back when I started with Ruby, in version 1.8, the default encoding of source files was ASCII, so we frequently had to add a magic comment
at the top of the file to tell Ruby they were encoded in UTF-8.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># encoding: utf-8</span>
</code></pre></div></div>

<p>I hated that comment back then, because what I always loved about Ruby is that the source code is almost entirely free of boilerplate.
So when Ruby 2.0 made UTF-8 the default encoding, and we could finally get rid of all this cruft, it made me extremely happy.</p>

<p>I would love to do the same with the frozen string literal comment, but once you are aware of all these useless allocations and copies, it’s really hard to unsee.
I’m now familiar enough with the VM that when I look at code without the magic comment, I pretty much visualize the implicit <code class="language-plaintext highlighter-rouge">dup</code> calls.</p>

<p>When I look at code like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">env</span><span class="p">[</span><span class="s2">"HTTPS"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"on"</span> <span class="p">?</span> <span class="s2">"https"</span> <span class="p">:</span> <span class="s2">"http"</span>
</code></pre></div></div>

<p>I can’t help but see this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">env</span><span class="p">[</span><span class="s2">"HTTPS"</span><span class="p">.</span><span class="nf">dup</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"on"</span><span class="p">.</span><span class="nf">dup</span> <span class="p">?</span> <span class="s2">"https"</span><span class="p">.</span><span class="nf">dup</span> <span class="p">:</span> <span class="s2">"http"</span><span class="p">.</span><span class="nf">dup</span>
</code></pre></div></div>

<p>Which drives me nuts.
And yes, these are small strings, and the GC got faster in the last few years, but still, string literals are everywhere, so these allocations add up and cause a death by a thousand cuts.</p>

<p>So seeing that the community was slowly unlearning this lesson pained me, and I decided I’d try to revive the initiative.</p>

<h2 id="chilled-string-literals">Chilled String Literals</h2>

<p>In my opinion, what the initial plan lacked was a proper deprecation path.
Many Ruby users had heard the default would change with Ruby 3.0, but Ruby itself never emitted any deprecation to warn users that code would need to be updated, so very little work happened to prepare for it.</p>

<p>Hence, if I wanted to convince Matz to try again, I needed to come up with a way to emit useful deprecation warnings whenever some code would mutate a literal string.
That’s where I came up with <a href="https://bugs.ruby-lang.org/issues/20205">the concept of <em>chilled strings</em></a>.</p>

<p>Starting from Ruby 3.4, when a source file has no <code class="language-plaintext highlighter-rouge">frozen_string_literal</code> comment (either <code class="language-plaintext highlighter-rouge">true</code> or <code class="language-plaintext highlighter-rouge">false</code>), instead of generating <code class="language-plaintext highlighter-rouge">putstring</code> instructions, the compiler now generates <code class="language-plaintext highlighter-rouge">putchilledstring</code> instructions:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">RubyVM</span><span class="o">::</span><span class="no">InstructionSequence</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="sx">%{puts "Hello World"}</span><span class="p">).</span><span class="nf">disasm</span>
<span class="o">==</span> <span class="ss">disasm: </span><span class="c1">#&lt;ISeq:&lt;compiled&gt;@&lt;compiled&gt;:1 (1,0)-(1,18)&gt;</span>
<span class="mo">0000</span> <span class="n">putself</span>                                                          <span class="p">(</span>   <span class="mi">1</span><span class="p">)[</span><span class="no">Li</span><span class="p">]</span>
<span class="mo">0001</span> <span class="n">putchilledstring</span>                       <span class="s2">"Hello World"</span>
<span class="mo">0003</span> <span class="n">opt_send_without_block</span>                 <span class="o">&lt;</span><span class="n">calldata!mid</span><span class="ss">:puts</span><span class="p">,</span> <span class="n">argc</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span> <span class="no">FCALL</span><span class="o">|</span><span class="no">ARGS_SIMPLE</span><span class="o">&gt;</span>
<span class="mo">0005</span> <span class="n">leave</span>
</code></pre></div></div>

<p>This new instruction is identical to <code class="language-plaintext highlighter-rouge">putstring</code>, except it additionally marks the newly allocated string with the <code class="language-plaintext highlighter-rouge">STR_CHILLED</code> flag.
Then I modified the <code class="language-plaintext highlighter-rouge">rb_check_frozen</code> function, which is responsible for raising <code class="language-plaintext highlighter-rouge">FrozenError</code> when a frozen object is mutated, to also check for that flag.
When a chilled string is mutated, a deprecation warning is emitted, and the flag is removed so that only the very first mutation emits a warning:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">Warning</span><span class="p">[</span><span class="ss">:deprecated</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="o">&gt;&gt;</span> <span class="s2">"test"</span> <span class="o">&lt;&lt;</span> <span class="s2">"a"</span> <span class="o">&lt;&lt;</span> <span class="s2">"b"</span>
<span class="p">(</span><span class="n">irb</span><span class="p">):</span><span class="mi">3</span><span class="p">:</span> <span class="ss">warning: </span><span class="n">literal</span> <span class="n">string</span> <span class="n">will</span> <span class="n">be</span> <span class="n">frozen</span> <span class="k">in</span> <span class="n">the</span> <span class="n">future</span> <span class="p">(</span><span class="n">run</span> <span class="n">with</span> <span class="o">--</span><span class="n">debug</span><span class="o">-</span><span class="n">frozen</span><span class="o">-</span><span class="n">string</span><span class="o">-</span><span class="n">literal</span> <span class="k">for</span> <span class="n">more</span> <span class="n">information</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="s2">"testab"</span>
</code></pre></div></div>

<p>The migration plan is that in a yet to be defined future version, these deprecation warnings would be visible by default, and then in a further version, frozen string literals would become the default.</p>

<h2 id="measuring-the-performance-impact">Measuring The Performance Impact</h2>

<p>Just like in the previous discussions back in 2014, <a href="https://github.com/mame">Yusuke Endoh (mame)</a> objected to the change, arguing that the performance benefits of frozen string literals were never properly measured because back in 2014, lots of code wasn’t compatible so it wasn’t possible to measure.</p>

<blockquote>
  <p>how much would the performance degrade if we removed <code class="language-plaintext highlighter-rouge"># frozen_string_literal: true</code> from all code used in yjit-bench?</p>
</blockquote>

<p>So I went ahead and built a modified Ruby interpreter on which the magic comment had no effect, and <a href="https://bugs.ruby-lang.org/issues/20205#note-34">benchmarked it against mainline Ruby</a>.</p>

<p>The results were that frozen string literals make Lobsters, an open source discussion board in Rails, 8-9% faster.
It also made <code class="language-plaintext highlighter-rouge">railsbench</code>, a synthetic Rails application, 4-6% faster, and <code class="language-plaintext highlighter-rouge">liquid-render</code> 11% faster.</p>

<p>And one thing to note is that the benchmarked codebase and its dependencies, like Rack, still contain lots of code that was hand-optimized from the pre-frozen string literal days to avoid allocations.
So the difference would be certainly larger if mutable string literals weren’t already worked around.</p>

<p>Similarly, back then I was surprised to only see a meager 1-2% gain on the <code class="language-plaintext highlighter-rouge">erubi-rails</code> benchmark, given it’s quite string-heavy.
But in retrospect, it’s very much expected because one of the biggest performance tricks of erubi is that it works around mutable string literals in its code generation by leveraging <code class="language-plaintext highlighter-rouge">opt_str_freeze</code> instructions:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">Erubi</span><span class="o">::</span><span class="no">Engine</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Hello &lt;% name%&gt;!"</span><span class="p">).</span><span class="nf">src</span>
<span class="n">_buf</span> <span class="o">=</span> <span class="o">::</span><span class="no">String</span><span class="p">.</span><span class="nf">new</span><span class="p">;</span> <span class="n">_buf</span> <span class="o">&lt;&lt;</span> <span class="s1">'Hello '</span><span class="p">.</span><span class="nf">freeze</span><span class="p">;</span> <span class="nb">name</span><span class="p">;</span> <span class="n">_buf</span> <span class="o">&lt;&lt;</span> <span class="s1">'!'</span><span class="p">.</span><span class="nf">freeze</span><span class="p">;</span>
<span class="n">_buf</span><span class="p">.</span><span class="nf">to_s</span>
</code></pre></div></div>

<p>All this makes it hard to come up with a clear measure of the performance benefits of freezing string literals.
At this point, making them the default is more to allow Rubyists to write nicer and less contrived code, not so much about improving performance.</p>

<p>After some more rounds of discussion, Matz <a href="https://bugs.ruby-lang.org/issues/20205#note-35">accepted the proposal</a> but without committing to any specific timeline,
and I implemented the feature with <a href="https://github.com/etiennebarrie">Étienne Barrié</a>, which shipped with Ruby 3.4.0.</p>

<h2 id="so-its-done">So It’s Done?</h2>

<p>So at this point, it may look like a done deal.
The deprecations are in place, it’s just a matter of deciding when to flip the switch.</p>

<p>But as we’ve seen in the past, that doesn’t mean much.
Matz may still change his mind at any point, and there are still a few Ruby core members actively campaigning against frozen string literals.</p>

<p>Personally, I’m quite tired of arguing about it.
It might be a personal bias, given the overwhelming majority of the code I interact with has been frozen string literal compatible for a decade, but it seems to me that the Ruby community very largely adopted frozen string literals, so for me it seems obvious to make it the default.</p>

<p>But not everyone in Ruby core has the same view of the community.
Some members like Mame are very involved in <a href="https://en.wikipedia.org/wiki/Quine_(computing)">quines</a> and other forms of <a href="https://github.com/tric/trick2025">artistic programming like TRICK</a>,
in which mutable string literals are used a lot.
So I understand that for him, switching the default means breaking a number of historical programs he cares about.</p>

<p>Ultimately, as always with Ruby’s direction, it will come down to what Matz decides.
For now, he has publicly accepted the migration plan, but not yet committed to any timeline, and I’m not sure Matz really has a vision of what the community at large desires on this topic.
With Ruby 4.0 being likely released this year, it’s very possible this migration stays in limbo for years and is ultimately abandoned again.</p>

<h2 id="alternatives">Alternatives</h2>

<p>At the end of the day, I don’t care so much about frozen string literals being the default.
I just want to be able to stop adding this ugly comment at the top of my files, without losing the performance benefit and without having to explicitly freeze my constants.</p>

<p>An alternative to changing the default could be to allow setting compiler options for entire directories.
This would allow Rubyists to enable frozen string literals in a single place, typically the <code class="language-plaintext highlighter-rouge">gemspec</code> or Rails config.</p>

<p>However, this would fragment Ruby more, because it means a given code snippet may or may not work based on where it is located.
This was already a concern with the magic comment, it would be an even bigger one with directory-based compiler options.
So I’m not sure Matz would be ok with that.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I can’t predict what the future of string literals in Ruby will be.
I do hope they’ll be frozen a few years from now, but I’m not holding my breath.</p>

<p>In the meantime I do encourage gem authors to tes<a href="https://github.com/asciidoctor/asciimath/pull/78">t their gems with <code class="language-plaintext highlighter-rouge">--enable-frozen-string-literal</code></a></p>

<p>What is certain, however, is that performance-wise, they only have upsides, as they’re strictly less work for the Ruby VM, but your performance-sensitive dependencies likely already use them, or at least work around mutable string literals in the hot paths.
Hence, you are unlikely to notice a big difference if you were to run your application with <code class="language-plaintext highlighter-rouge">RUBYOPT="--enable-frozen-string-literal"</code>.
However, if you do measure a negative performance impact, there is no doubt you are measuring incorrectly.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>A previous version of the post wrongly listed PHP as a language with immutable strings. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="ruby" /><category term="performance" /><summary type="html"><![CDATA[If you are a Rubyist, you’ve likely been writing # frozen_string_literal: true at the top of most of your Ruby source code files, or at the very least, that you’ve seen it in some other projects.]]></summary></entry><entry><title type="html">Dear Rubyists: Shopify Isn’t Your Enemy</title><link href="https://byroot.github.io/opensource/ruby/2025/10/09/dear-rubyists.html" rel="alternate" type="text/html" title="Dear Rubyists: Shopify Isn’t Your Enemy" /><published>2025-10-09T05:03:51+00:00</published><updated>2025-10-09T05:03:51+00:00</updated><id>https://byroot.github.io/opensource/ruby/2025/10/09/dear-rubyists</id><content type="html" xml:base="https://byroot.github.io/opensource/ruby/2025/10/09/dear-rubyists.html"><![CDATA[<p>I’ve been meaning to write a post about my perspective on Open Source and corporate entities.
I already got the rough outline of it; however, I’m suffering from writer’s block,
but more importantly, the whole post is a praise of how Shopify engages with Open Source communities.
Hence, given the current climate, I don’t think I could publish it without addressing the elephant in the room first anyway.</p>

<p>So here it is, I am deeply convinced that contrary to what has been alleged recently,
Shopify has nothing but good intentions toward Ruby and its community.</p>

<p>It is healthy to be skeptical toward corporations, I certainly am, but I believe Shopify is currently receiving undue distrust considering their track record of massive investment in the Ruby ecosystem.
And some of that may be due to a lack of understanding of how they engage with Open Source communities.</p>

<p>So I’ll try to explain what they do, how they do it, and why we need more companies like Shopify, not less.</p>

<h2 id="proper-disclaimer">Proper Disclaimer</h2>

<p>As is customary in this sort of situation, I first need to disclose the nature of my relationship with Shopify.</p>

<p>I could try to brush it off by just saying that I was employed by them from November 2013 to August 2025, but in my opinion, that would be a cop-out.
Knowing that someone has been previously employed by someone else doesn’t tell you anything about where they’re speaking from.
Worse, instead of enlightening you on which biases the author might have, it might let you think they have insider knowledge, hence are even more reliable.</p>

<p>What is important to disclose is how the relationship ended.</p>

<p>In my case, I left Shopify for several reasons, but mainly because of my constant friction with the CEO.
Ever since my first interaction with him twelve years ago, I knew he was someone I couldn’t see eye to eye with on almost every subject.
Even when I’d occasionally happen to agree on a specific topic, his overly maximalist position and lack of nuance would drive me away.
The only reason I managed to stick this long at the company is that I made sure to pick projects and teams so as to minimize my interactions with him.</p>

<p>And the reason why I ended up quitting is that it was no longer possible to avoid him.
Since I consider him directly responsible for my burnout last year, I couldn’t possibly stay any longer.</p>

<p>I could go on for hours about all the hard feelings, but this is not really the place, I only mean to share enough to explain where I’m speaking from.
What is important to know is that I have absolutely zero reasons to give a pass to Shopify over anything.</p>

<h2 id="people-are-multidimensional">People Are Multidimensional</h2>

<p>But despites my personal feelings and history, it has to be said that Shopify’s CEO is a Rubyist at heart, almost to a fault.</p>

<p>Contrary to what you might think, Ruby isn’t all that popular at Shopify.
Even when I started back in 2013, only a small fraction of new hires had any prior experience with Ruby,
and a decade later, there aren’t so many proud Rubyists in the Shopify ranks.
Most developers, and even many executives, would rather use something else.</p>

<p>Yet, Ruby and Rails remain the default stack at Shopify, and the only reason for that is the CEO.
Every Shopify employee knows that suggesting straying away from Ruby wouldn’t fly there.
And I’m convinced that if it were anyone else at the helm, Shopify would have joined the long list of companies that attempted to migrate to something else
and are now stuck with both a Ruby monolith and a ton of half-migrated micro-services in Java or Go.</p>

<p>Hence, it’s important to recognize that people are multidimensional.
Just because you can’t see eye to eye on some topic doesn’t mean you can’t be allies (even if only by circumstances) on another.</p>

<p>But Shopify isn’t only its CEO.</p>

<h2 id="the-ruby--rails-infrastructure-team-rri">The Ruby &amp; Rails Infrastructure Team (R&amp;RI)</h2>

<p>As Rubyists, the side of Shopify you are the most likely to interact with, or at least be familiar with, is the Ruby and Rails Infrastructure team (R&amp;RI).</p>

<p>It’s a team of 40ish people.
They’re the ones you see on countless GitHub issues and pull requests, maintaining countless projects, and speaking at conferences.
I know all of them very well, and I can attest that, barring a couple of rare exceptions, they’re all long-time proud Rubyists, not mercenaries nor zealous “company men”.</p>

<p>I believe, without the shadow of a doubt, that if Shopify ever started to have ill intentions toward the community, many people in the R&amp;RI team would either resign or call it out or both.
At the very least, they would confide in other members of the community, and that would inevitably be public rather quickly.</p>

<p>You may think I’m exaggerating, and surely with their cushy salaries, many of them would have second thoughts.
But I honestly don’t think so.
Shopify isn’t even paying that well (depending on the market).
Based on my discussions with the people who left the team over the years, the most common cause of voluntary departures, by far, was compensation.
And most of the team could find another job rather quickly anyway, even in this market.</p>

<p>What makes them stay at Shopify, and why it took me so long to finally decide to quit, is that right now, it is hands down the best place
in the world to contribute to the Ruby ecosystem.
Nowhere else comes close, and that’s all due to Shopify’s philosophy toward Open Source.</p>

<h2 id="your-dependencies-are-your-code-too">Your Dependencies Are Your Code Too</h2>

<p>Whether you realized it already or not, all the code you depend on, all the code that runs on your servers, is your code.
It doesn’t matter if it was written by someone you never met in Nebraska, or by a multi-billion-dollar corporation.</p>

<p>You run it, you own it.</p>

<p>If it has a bug, if it is missing a feature, or if it has any other needs, that’s on you to figure out the solution for yourself.
There’s no relying on the original author to get that responsibility off your plate.</p>

<p>To illustrate this, I remember back in 2014 or 2015, when MySQL servers started segfaulting in production at a regular interval.
IIRC, Shopify had a support contract with a MySQL consultancy, and they probably were notified of it.
But we didn’t sit there waiting for the “owners” or experts to figure it out.</p>

<p>It’s a colleague who went knee deep in core dumps to figure out this was caused by an <code class="language-plaintext highlighter-rouge">alloca</code> call in a non-leaf function,
causing a stack overflow, produced a patch, patched our MySQL servers, and then sent the patch upstream<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.</p>

<p>This philosophy is at the heart of Shopify’s Ruby &amp; Rails Infrastructure team.
It is determined not just to be a user of the open source ecosystem, but to proactively engage with it, contribute,
and make it better through engineering time and contributions.
Not by delegating the responsibility to a third party nor exploiting maintainers goodwill.</p>

<p>I also sometimes hear people saying that Shopify is snatching all the super senior Ruby developers, but I’d argue that’s mostly untrue.
The reality is that in most cases, Shopify is growing these developers internally.</p>

<p>Take Kevin Newton, for instance.
He started as a product developer at Shopify, but after a few years, he pitched his vision of a universal parser for Ruby,
managed to get transferred to the R&amp;RI team, worked on the project that became Prism, became a Ruby core committer, won the Ruby Prize award, etc.
Since then, he left Shopify to work on a Python JIT at Meta, yet he is still maintaining Prism, because he is a Rubyist at heart.
And Kevin is far from the only example of that; I am one as well, and so are dozens of my former teammates.
Some, like Peter Zhu, even started as interns.</p>

<p>The reason I’m explaining this is that I feel there is a part of the community that is naturally distrustful of Shopify or corporations in general.
I don’t blame them, there have been countless examples of nefarious behaviours from companies, so it’s logical and healthy to at least be skeptical.
But it’s also important to recognize and salute positive behavior when it happens.</p>

<p>In this specific case, I believe that recently, Shopify has been giving the community something that is priceless: a large number of proficient and deeply committed contributors to Ruby itself and the whole ecosystem.
And I’d argue that is way more valuable for the future and sustainability of Ruby than any amount of money.</p>

<h2 id="sustainability-isnt-just-about-money">Sustainability Isn’t Just About Money</h2>

<p>Usually, when the topic of Open Source sustainability comes up, it ends up revolving around how to make companies pay for developers’ time.
There is this idealized image of Open Source being an amalgamation of lone developers tirelessly maintaining projects for free, eating ramen while big bad companies make huge profits out of their work.
There is definitely some truth to it, it is far from uncommon, but it’s also a bit of a tired cliché.</p>

<p>The Open Source ecosystem is also a lot of projects that are contributed to by people on various companies’ payrolls.
Linux is the poster child of healthy corporate involvement, with the overwhelming majority of contributions coming from employees of companies with a vested interest in the kernel.
That’s just one example, but when you look at big and complex open source projects, most of the time you’ll see big companies involved in one way or another.
That’s how most of the sustainable open source happens today, way more than through donations.</p>

<p>Hence, I’d argue that if an open source community wants to be sustainable, it needs to be welcoming of corporate contributions.
I don’t mean trust them blindly, it’s important to keep them in check just in case, but you have to let them play ball.</p>

<p>Ruby has successfully done that.
Back in 2019, Rafael França and Matz met in Bristol.
Rafael asked Matz what he needed, and Matz answered: “I need people”.
That’s how the Ruby and Rails Infrastructure team started getting involved in Ruby development, that’s what ultimately led to YJIT, now ZJIT, numerous GC improvements like Variable Width Allocation, modular GC, Prism, tons of Ractors improvements, etc.
But more importantly, almost a dozen new Ruby core committers.</p>

<p>I would wager that if that day Matz had asked for money, we’d have much worse results to show for.</p>

<h2 id="money-can-create-perverse-incentives">Money Can Create Perverse Incentives</h2>

<p>And aside from worse results, I’d argue it would have created perverse incentives.</p>

<p>I have nothing but respect for people who try to find ways to fund open source development in alternative ways.
However, it’s important to look at it through the lens of structures and incentives.</p>

<p>Whenever you design a system that involves people, you need to consider how a person who tries to maximize their personal benefits is incentivized to behave.</p>

<p>A typical example is ticket inspectors on trains and buses.
You may be tempted to give them a cut on the fines they give to people, as to incentivise them to work harder, but by doing so,
you create a problem that they are incentivized to be inflexible with commuters, causing a lot of conflicts instead of resolving situations peacefully.
Some of them might even be incentivized to give bullshit fines to earn a little extra money<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>.</p>

<p>If a system requires all the people involved to be perfect and act selflessly, then I’d argue it’s a flawed system.</p>

<p>Now, if Shopify had instead poured millions in cash into the Ruby Association or Matz himself, how would you, I, or anyone
be able to trust that the project direction and decision are free of influence?
How to trust that a given feature was accepted solely on its own merit and not just because it came from a big sponsor?
Inversely, when a feature is declined, how do you trust it wasn’t because it didn’t come from a sponsor?</p>

<p>That’s the thing with money, once you have it, it’s very difficult to do without.
When a big sponsor pulls out, you have to lay off staff, stop some initiatives, etc.
So even if you publicly declare that there’s no strings attached, even if you never explicitly say anything about it,
entities and people who receive funding are naturally incentivized to keep the donor happy so that the funding keeps coming.</p>

<p>Whereas with corporate contributors, sure, their employer may decide to assign them to another project, but there are no hard consequences, and most of them will stick around regardless.
Most will even remain contributors if they quit or are laid off.</p>

<p>You can actually witness that dynamic between Shopify and Ruby publicly, for instance, in how Prism is now the default parser, but isn’t yet the only official parser.
I can tell you that this has ruffled quite a few feathers at Shopify, but that’s the thing, Matz and Ruby don’t feel indebted to Shopify, they feel entirely free to say no.
And I think that’s how it should be.</p>

<p>To be clear, I’m not saying open source should be free of any monetary exchanges, just that it’s crucial to do it in a way that doesn’t let these sorts of suspicions arise.</p>

<h2 id="not-every-project-is-equally-forkable">Not Every Project Is Equally Forkable</h2>

<p>I know some people will object to the above, arguing that this is all open source, so if you are not happy with the direction of the project, you can always fork, ergo: shut up!
And while this is true in most cases, in practice, there are some projects that aren’t as easily forked because of their position.</p>

<p>For instance, if you look at Sidekiq, it’s making loads of money with its Pro and Enterprise offerings, and quite openly declines some features in the open source project so as not to cannibalize sales.
As far as I am aware, pretty much everyone is fine with it.
Sure, you’ll find a few people complaining about it, but that’s just background noise.</p>

<p>This is because Sidekiq isn’t on any critical path, there are plenty of alternatives you can go for if you aren’t satisfied with it, and if you wish to fork it and add such a feature for yourself, it’s pretty trivial, you don’t need to convince anyone.
Hence, everyone sees it as fair.</p>

<p>However, some projects have a moat.
A dominant position granted by another project.
Imagine if, instead of allowing you to use any job processor you want through Active Job, Rails had instead decided to make Sidekiq the only option.
In such a world, then I believe a whole lot more people would be upset or suspicious, because the bar to clear to use an alternative would be way higher.
A lot of Rails users would feel captive.</p>

<p>Well, I would argue that rubygems is in such a situation.
It is distributed with Ruby, required early during the Ruby boot process, is coupled with all distributed gems via the <code class="language-plaintext highlighter-rouge">gemspec</code> format, etc.
Because of this, it has a massive moat.
Forking it to build and use your own alternative to it is hardly viable, even for a big team like Shopify’s Ruby and Rails Infrastructure team.</p>

<p>As such, while it’s still nothing but commendable to try to fund its maintenance work, you have to be careful to avoid any perverse incentives and conflicts of interest.
Otherwise, even if you are exceptionally selfless and well-intentioned, you will inevitably spur suspicion whenever you refuse contributions or ask for sponsorship on a GitHub issue.</p>

<p>Unfortunately, it did happen.</p>

<p>Over the past decade, people in the community, not just Shopify employees, started to conclude that rubygems and bundler were being monetized by some key maintainers.
To be clear, I’m not trying to convince anyone that this was actually the case.
Some of that dirty laundry that has been an open secret among the Ruby maintainers’ community for a long time has recently been aired out, and I suspect there’s more to come.
You are free to form your own opinion on the topic if you so wish.</p>

<p>But my point is that it doesn’t actually matter whether rubygems was actually being unduly taken advantage of or not.
Ultimately, it’s down to who and what you consider legitimate.</p>

<p>My point is that the economic model chosen to fund rubygems’ maintenance, combined with its critical position in the ecosystem, has allowed for these suspicions to exist and persist, creating tensions and driving potential sources of funding away.</p>

<p>Again, I believe the problem is with structures and incentives, as well as optics, not specific people being imperfect or ill-intentioned.</p>

<h2 id="shopify-and-rubygems-rocky-relationship">Shopify and Rubygems Rocky Relationship</h2>

<p>Because of this, the relationship between Shopify and the various entities overseeing rubygems development has been quite rocky for a long time.</p>

<p>As you are probably aware, supply chain security has been a hot topic in the corporate world, hence, around 2021, Shopify started trying to contribute more to rubygems, and an entire team of developers was assembled with the goal of helping the upstream projects.</p>

<p>I no longer have access to all the history, and some details are now blurry.
But from what I recall, there were various goals, such as requiring multi-factor authentication to publish the most popular packages, making code signing easier, and a few other topics.</p>

<p>However, that initiative didn’t exactly receive a warm welcome from upstream.
It’s not that these features weren’t desired, but the understanding on Shopify’s side was that maintainers preferred to be paid to do it, rather than just accept contributions.</p>

<p>This is what ultimately led to Shopify funding Ruby Central directly (other than being a recurring major sponsor at their conferences for years).
The deal was for <a href="https://rubycentral.org/news/ruby-shield/">one million dollars over 4 years, under the name Ruby Shield</a>.</p>

<p>But even after that, the feeling on the Shopify side was that upstream was still uncooperative, until ultimately they decided to cut their losses and re-assigned engineers elsewhere.
The 4-year funding deal remained, but not much was expected of it.</p>

<p>Shopify could have threatened to pull funding at that time to try to coerce Ruby Central, yet they didn’t.</p>

<h2 id="shopify-never-threatened-to-pull-funding">Shopify Never Threatened To Pull Funding</h2>

<p>As I said earlier, ever since this controversy started, I’ve been unconvinced by the theory that all this would have been orchestrated by Shopify or through Shopify.
That simply would have required involving too many people, and I absolutely can’t imagine that none of them would have objected in one way or another.</p>

<p>But anyway, since then, I did contact two former coworkers, and they both assured me that Shopify never threatened to pull Ruby Central’s funding, nor threatened not to renew it.</p>

<p>Now, as I tried to explain earlier, even if you loudly claim money comes with no strings attached, people and entities are naturally incentivized to do what they think is necessary to keep it coming.
As such, it’s entirely possible that despite the absence of threats, Ruby Central’s moves may have been motivated by the need to secure the existing funding and/or find additional sources of funding.</p>

<p>My former coworkers also told me their side of the story, and it’s absolutely nothing like what has been alleged so far.
I deeply trust these two people, and I can’t possibly imagine they’d be lying to me, but I’d understand if you don’t want to take my word for it.</p>

<p>I don’t know when their side of the story will come out, nor if it will come out at all, but I do hope it comes out soon and with receipts.
Seeing so many good-natured and well-intentioned people get demonized like they have been over the last few weeks is depressing.</p>

<p>It is undeniable that, regardless of what Ruby Central’s intentions were, the communication and execution have been abysmal.
It is also true that there is a deep disagreement about what they rightfully or legitimately owned that won’t easily be resolved.
However, I can’t believe the entire organisation was ill-intentioned, here again, that would involve too many people to be conceivable.</p>

<p>Similarly, the claim that Aaron sending patches to rubygems is a clue that there was a conspiracy at play drives me nuts.
I’ve seen these pull requests being made with my own eyes, and I can tell you that the reason is way more mundane than that.
We were at Rails World, someone mentioned <code class="language-plaintext highlighter-rouge">rv</code>, the question of why you’d need to write something in Rust to speed up gem installation was raised, and Aaron and a few others started to profile Bundler to see if it could be made faster.</p>

<p>That’s it, that’s all there is.
Aaron got nerd sniped into making Bundler faster, and now he’s being called out for supposedly being part of a hostile takeover?
Give me a break.</p>

<h2 id="we-need-more-shopifies-not-less">We Need More Shopifies, Not Less</h2>

<p>I think it’s healthy to be wary of Shopify’s huge footprint on the ecosystem.
Companies are fickle beings, and even if I’m not particularly concerned about them ever having ill intent toward the Ruby ecosystem,
it’s not impossible that in the future they may decide to invest less.</p>

<p>But the response shouldn’t be to try to cast Shopify and its employees aside.
It would be silly to punish them for helping too much.
What we need is more companies doing their part.
Both to reduce Shopify’s relative influence, but also to have more diverse perspectives, use cases, and priorities.</p>

<p>I’m not saying every company should have a team as big as Shopify’s R&amp;RI, but there are numerous Ruby-based companies with valuations in billions and several hundred developers on their payroll, yet they contribute very little upstream.
If you work at one of such companies, you should really consider how you could do more.</p>

<p>That’s what I intend to do at my next job, to get one more Ruby company to pull its weight.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>For the annecdote, MySQL refused the patch arguing that this error couldn’t realistically happen. LOL. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>This is not a made up example by the way. It has been a big issue in France for over a decade. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="opensource" /><category term="ruby" /><summary type="html"><![CDATA[I’ve been meaning to write a post about my perspective on Open Source and corporate entities. I already got the rough outline of it; however, I’m suffering from writer’s block, but more importantly, the whole post is a praise of how Shopify engages with Open Source communities. Hence, given the current climate, I don’t think I could publish it without addressing the elephant in the room first anyway.]]></summary></entry><entry><title type="html">Unlocking Ractors: generic instance variables</title><link href="https://byroot.github.io/ruby/performance/2025/08/11/unlocking-ractors-generic-variables.html" rel="alternate" type="text/html" title="Unlocking Ractors: generic instance variables" /><published>2025-08-11T09:03:51+00:00</published><updated>2025-08-11T09:03:51+00:00</updated><id>https://byroot.github.io/ruby/performance/2025/08/11/unlocking-ractors-generic-variables</id><content type="html" xml:base="https://byroot.github.io/ruby/performance/2025/08/11/unlocking-ractors-generic-variables.html"><![CDATA[<p>In two previous posts, I explained that one of the big blockers for Ractors’ viability is that while they’re supposed
to run fully in parallel, in many cases, they’d perform worse than a single thread because there were numerous codepaths
in the Ruby virtual machine and runtime that were still protected by the global VM lock.</p>

<p>I also explained how I removed two of these contention points, <a href="/ruby/performance/2025/04/26/unlocking-ractors-object-id.html">the <code class="language-plaintext highlighter-rouge">object_id</code> method</a>,
and <a href="/ruby/performance/2025/05/24/unlocking-ractors-class-variables.html">class instance variables</a>.</p>

<p>Since then, the situation has improved quite drastically, as numerous other contentious points have been either eliminated or reduced by me and my former teammates.
I’m not going to make a post for each of them, as in most cases it boils down to the same <a href="https://en.wikipedia.org/wiki/Read-copy-update">RCU technique</a>
I explained in the post about class instance variables.</p>

<p>But there’s one such contention point I find interesting and that I’d like to write about: the generic instance variables table.</p>

<h2 id="how-instance-variables-work">How Instance Variables Work</h2>

<p>As a Ruby user, you are likely familiar with the idea that everything is an object, and that is somewhat true, but that doesn’t mean all objects are equal.
I already touched on that subject in some of my previous posts, so I’ll do it quickly.</p>

<p>In the context of instance variables, in the Ruby VM you essentially have 3 or 4 types of objects, depending on how you count.</p>

<p>First, you have the “immediates”, small integers (<code class="language-plaintext highlighter-rouge">1</code>), booleans (<code class="language-plaintext highlighter-rouge">true</code>, <code class="language-plaintext highlighter-rouge">false</code>), static symbols (<code class="language-plaintext highlighter-rouge">:foo</code>, but not dynamic symbols like <code class="language-plaintext highlighter-rouge">"bar".to_sym</code>), etc.
These are called immediates because they don’t actually exist in memory; they don’t have an allocated object slot on the heap.  Their reference <em>is</em> their value.
In other words, they’re just <a href="https://en.wikipedia.org/wiki/Tagged_pointer">tagged pointers</a>.</p>

<p>Hence, they can’t have instance variables, and Ruby will treat them as if they were frozen to maintain the illusion of parity with other objects:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="mi">42</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="ss">:@test</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">(</span><span class="n">irb</span><span class="p">):</span><span class="mi">2</span><span class="ss">:in</span> <span class="s1">'Kernel#instance_variable_set'</span><span class="p">:</span> <span class="n">can</span><span class="err">'</span><span class="n">t</span> <span class="n">modify</span> <span class="n">frozen</span> <span class="no">Integer</span><span class="p">:</span> <span class="mi">42</span> <span class="p">(</span><span class="no">FrozenError</span><span class="p">)</span>
</code></pre></div></div>

<p>Then you have the more regular <code class="language-plaintext highlighter-rouge">T_OBJECT</code>, for your user-defined classes.
In the case of <code class="language-plaintext highlighter-rouge">T_OBJECT</code>, instance variables are stored inside the object’s slot like an array.
Consider the following object with 3 instance variables:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Foo</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@a</span> <span class="o">=</span> <span class="mi">1</span>
    <span class="vi">@b</span> <span class="o">=</span> <span class="mi">2</span>
    <span class="vi">@c</span> <span class="o">=</span> <span class="mi">3</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It will fit in the base <code class="language-plaintext highlighter-rouge">40B</code> object slot.
<code class="language-plaintext highlighter-rouge">16B</code> is being used for the object’s flags and a pointer to its class, and the remaining <code class="language-plaintext highlighter-rouge">24B</code> is used for the three instance variable references:</p>

<table>
  <thead>
    <tr>
      <th>flags</th>
      <th>klass</th>
      <th>@a</th>
      <th>@b</th>
      <th>@c</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>T_OBJECT</td>
      <td>0xffeff</td>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
  </tbody>
</table>

<p>In some cases, if an instance variable is added later and the slot is full, the Ruby VM may have to allocate a separate memory
region and “spill” the instance variables there, but this is actually fairly rare. The VM keeps track of how many variables
the instances of each class have, so if Ruby ever has to spill, every future instance of that class will be allocated in a larger slot.</p>

<p>The third type of objects are <code class="language-plaintext highlighter-rouge">T_CLASS</code> and <code class="language-plaintext highlighter-rouge">T_MODULE</code>. Since that was the topic of my previous post, I’ll be quick.
Class instance variables are laid out like for <code class="language-plaintext highlighter-rouge">T_OBJECT</code> except they’re in a “companion” slot.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Foo</span>
  <span class="vi">@a</span> <span class="o">=</span> <span class="mi">1</span>
  <span class="vi">@b</span> <span class="o">=</span> <span class="mi">2</span>
  <span class="vi">@c</span> <span class="o">=</span> <span class="mi">3</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The layout of the class itself stores a reference to that “companion” slot:</p>

<table>
  <thead>
    <tr>
      <th>flags</th>
      <th>klass</th>
      <th>obj_fields</th>
      <th>…</th>
      <th>…</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>T_CLASS</td>
      <td>0xffeaa</td>
      <td>0xffdddd</td>
      <td> </td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>And that other slot is laid out exactly like a <code class="language-plaintext highlighter-rouge">T_OBJECT</code>, except its type is <code class="language-plaintext highlighter-rouge">T_IMEMO</code> for “Internal Memory”:</p>

<table>
  <thead>
    <tr>
      <th>flags</th>
      <th>klass</th>
      <th>@a</th>
      <th>@b</th>
      <th>@c</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>T_IMEMO/fields</td>
      <td>0xffeaa</td>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
  </tbody>
</table>

<p>That’s a type of object that, as a Ruby user, you can’t directly interact with, nor even get a reference to; they’re basically invisible.
But they are used internally by the VM to store various data in memory managed by the GC instead of using manual memory management with <code class="language-plaintext highlighter-rouge">malloc</code> and <code class="language-plaintext highlighter-rouge">free</code>.</p>

<p>And then you have all the other objects. <code class="language-plaintext highlighter-rouge">Hash</code>, <code class="language-plaintext highlighter-rouge">Array</code>, <code class="language-plaintext highlighter-rouge">String</code>, etc.
For these, the space inside the object slot is already used.
For example, a <code class="language-plaintext highlighter-rouge">String</code> slot is used to store the string <code class="language-plaintext highlighter-rouge">length</code>, <code class="language-plaintext highlighter-rouge">capacity</code>, and if it’s small enough, the bytes that compose the string itself, otherwise a pointer to a manually allocated buffer.</p>

<p>Yet, Ruby allows you to define any instance variables you want on a string:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="n">s</span> <span class="o">=</span> <span class="s2">"test"</span>
<span class="o">&gt;&gt;</span> <span class="n">s</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="ss">:@test</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="o">&gt;&gt;</span> <span class="n">s</span><span class="p">.</span><span class="nf">instance_variable_get</span><span class="p">(</span><span class="ss">:@test</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="mi">1</span>
</code></pre></div></div>

<p>To allow this, the VM has an internal hash table, which used to be called the <code class="language-plaintext highlighter-rouge">genivar_tbl</code>, for Generic Instance Variables Hash-Table, and that I renamed into <code class="language-plaintext highlighter-rouge">generic_fields_tbl_</code> as part of my work on <code class="language-plaintext highlighter-rouge">object_id</code>.</p>

<p>I previously explained how this works in <a href="/ruby/performance/2025/04/26/unlocking-ractors-object-id.html#generic-instance-variables">my post about the <code class="language-plaintext highlighter-rouge">object_id</code></a>
method, but I’ll reexplain here with a bit more detail, as it’s really the core topic.</p>

<p>Once again, I’ll use Ruby pseudo-code to make it easier:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">GenericIvarObject</span>
  <span class="no">GENERIC_FIELDS_TBL</span> <span class="o">=</span> <span class="no">Hash</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">compare_by_identity</span>

  <span class="k">def</span> <span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">ivar_shape</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">shape</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
      <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
        <span class="k">if</span> <span class="n">buffer</span> <span class="o">=</span> <span class="no">GENERIC_FIELDS_TBL</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span>
          <span class="n">buffer</span><span class="p">[</span><span class="n">ivar_shape</span><span class="p">.</span><span class="nf">index</span><span class="p">]</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In that global hash, the keys are the reference to the objects, and the values are pointers to manually allocated buffers.
Inside the buffer, there is an array of references just like in a <code class="language-plaintext highlighter-rouge">T_OBJECT</code> or a <code class="language-plaintext highlighter-rouge">T_IMEMO/fields</code>.</p>

<p>This isn’t ideal for multiple reasons.</p>

<p>First, having to do a hash-lookup is way more expensive than reading at an offset like we do for <code class="language-plaintext highlighter-rouge">T_OBJECT</code>, or even chasing a reference
like we do for <code class="language-plaintext highlighter-rouge">T_CLASS</code> and <code class="language-plaintext highlighter-rouge">T_MODULE</code>.</p>

<p>But worse, if we’re in a multi-ractor scenario, we have to acquire the VM lock for the whole operation.
First, because that hash-table is global and not thread-safe, then because we must ensure that another Ractor can’t free that manually allocated buffer while we’re reading it<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.</p>

<p>So now, you probably understand the problem.
Any code that reads or writes an instance variable in an object that isn’t a direct descendant of <code class="language-plaintext highlighter-rouge">Object</code> (actually <code class="language-plaintext highlighter-rouge">BasicObject</code>) nor <code class="language-plaintext highlighter-rouge">Module</code> is a contention point for Ractors.</p>

<h2 id="surely-that-isnt-common">Surely That Isn’t Common?</h2>

<p>Before I dig into what can be changed, you may wonder if it even matters.</p>

<p>And it’s a very fair question. Developer time isn’t unlimited, hence the question of whether it is worth removing a contention
points boil down to how hot a code path it is, and how hard it is to fix it.</p>

<p>When I started looking at this, it was from the angle of <code class="language-plaintext highlighter-rouge">T_STRUCT</code>.
I wanted the instance variable of <code class="language-plaintext highlighter-rouge">Struct</code> and <code class="language-plaintext highlighter-rouge">Data</code> objects
not to be contention points, e.g., it’s not that rare to see <code class="language-plaintext highlighter-rouge">Struct</code> being used as some sort of code generator:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Address</span> <span class="o">=</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:street</span><span class="p">,</span> <span class="ss">:city</span><span class="p">)</span> <span class="k">do</span>
  <span class="k">def</span> <span class="nf">something_else</span>
    <span class="vi">@something_else</span> <span class="o">||=</span> <span class="n">compute_something</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Because <code class="language-plaintext highlighter-rouge">Struct.new</code> and <code class="language-plaintext highlighter-rouge">Data.define</code> don’t create <code class="language-plaintext highlighter-rouge">T_OBJECT</code> but <code class="language-plaintext highlighter-rouge">T_STRUCT</code> objects.
In these, the space inside the slot is used for the declared fields, not for the ivars.</p>

<p>Another pattern I expected was C extensions. When a Ruby C extension needs to expose an API, it uses the <code class="language-plaintext highlighter-rouge">TypedData</code> API, which allows to create <code class="language-plaintext highlighter-rouge">T_DATA</code> objects.
But it’s not rare for extensions to do as little as possible in C, and to extend that C class with some Ruby.</p>

<p>An example of that is the <code class="language-plaintext highlighter-rouge">trilogy</code> gem, which <a href="https://github.com/trilogy-libraries/trilogy/blob/16667c95e8c2716a16e69e8325d6b0cb615591e2/contrib/ruby/ext/trilogy-ruby/cext.c#L1141-L1153">defines a bunch of C methods</a></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">RUBY_FUNC_EXPORTED</span> <span class="kt">void</span> <span class="nf">Init_cext</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">VALUE</span> <span class="n">Trilogy</span> <span class="o">=</span> <span class="n">rb_const_get</span><span class="p">(</span><span class="n">rb_cObject</span><span class="p">,</span> <span class="n">rb_intern</span><span class="p">(</span><span class="s">"Trilogy"</span><span class="p">));</span>
    <span class="n">rb_define_alloc_func</span><span class="p">(</span><span class="n">Trilogy</span><span class="p">,</span> <span class="n">allocate_trilogy</span><span class="p">);</span>

    <span class="n">rb_define_private_method</span><span class="p">(</span><span class="n">Trilogy</span><span class="p">,</span> <span class="s">"_connect"</span><span class="p">,</span> <span class="n">rb_trilogy_connect</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>
    <span class="n">rb_define_method</span><span class="p">(</span><span class="n">Trilogy</span><span class="p">,</span> <span class="s">"change_db"</span><span class="p">,</span> <span class="n">rb_trilogy_change_db</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
    <span class="n">rb_define_alias</span><span class="p">(</span><span class="n">Trilogy</span><span class="p">,</span> <span class="s">"select_db"</span><span class="p">,</span> <span class="s">"change_db"</span><span class="p">);</span>
    <span class="n">rb_define_method</span><span class="p">(</span><span class="n">Trilogy</span><span class="p">,</span> <span class="s">"query"</span><span class="p">,</span> <span class="n">rb_trilogy_query</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
    <span class="c1">//...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>But <a href="https://github.com/trilogy-libraries/trilogy/blob/16667c95e8c2716a16e69e8325d6b0cb615591e2/contrib/ruby/lib/trilogy.rb">then augment that C class with Ruby code</a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Trilogy</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">options</span> <span class="o">=</span> <span class="p">{})</span>
    <span class="n">options</span><span class="p">[</span><span class="ss">:port</span><span class="p">]</span> <span class="o">=</span> <span class="n">options</span><span class="p">[</span><span class="ss">:port</span><span class="p">].</span><span class="nf">to_i</span> <span class="k">if</span> <span class="n">options</span><span class="p">[</span><span class="ss">:port</span><span class="p">]</span>
    <span class="n">mysql_encoding</span> <span class="o">=</span> <span class="n">options</span><span class="p">[</span><span class="ss">:encoding</span><span class="p">]</span> <span class="o">||</span> <span class="s2">"utf8mb4"</span>
    <span class="n">encoding</span> <span class="o">=</span> <span class="no">Trilogy</span><span class="o">::</span><span class="no">Encoding</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">mysql_encoding</span><span class="p">)</span>
    <span class="n">charset</span> <span class="o">=</span> <span class="no">Trilogy</span><span class="o">::</span><span class="no">Encoding</span><span class="p">.</span><span class="nf">charset</span><span class="p">(</span><span class="n">mysql_encoding</span><span class="p">)</span>
    <span class="vi">@connection_options</span> <span class="o">=</span> <span class="n">options</span>
    <span class="vi">@connected_host</span> <span class="o">=</span> <span class="kp">nil</span>

    <span class="n">_connect</span><span class="p">(</span><span class="n">encoding</span><span class="p">,</span> <span class="n">charset</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That’s a pattern I really like, as it allows to write less C and more Ruby, so I would have hated having to complexify some C extensions so that they’d perform better under ractors.</p>

<p>Then you have a few classics, <a href="https://github.com/rails/rails/blob/3235827585d87661942c91bc81f64f56d710f0b2/activesupport/lib/active_support/core_ext/string/output_safety.rb#L19-L73">like <code class="language-plaintext highlighter-rouge">ActiveSupport::SafeBuffer</code></a>,
which is a subclass of <code class="language-plaintext highlighter-rouge">String</code> with a <code class="language-plaintext highlighter-rouge">@html_safe</code> instance variable:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">ActiveSupport</span>
  <span class="k">class</span> <span class="nc">SafeBuffer</span> <span class="o">&lt;</span> <span class="no">String</span>
    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">str</span> <span class="o">=</span> <span class="s2">""</span><span class="p">)</span>
      <span class="vi">@html_safe</span> <span class="o">=</span> <span class="kp">true</span>
      <span class="k">super</span>
    <span class="k">end</span>

    <span class="c1"># ...snip</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So it’s not that rare for code to inherit from core types, and it can end up in hot spots.
Even though I would recommend avoiding it as much as possible, for reasons other than performance, sometimes it’s the pragmatic thing to do, so users do it.</p>

<h2 id="some-data-points">Some Data Points</h2>

<p>Regardless, I was quite convinced that improving this code path would be useful and started working on it.
But later on, I was asked to provide some data, so while I’m breaking the chronology here, let me share it with you.</p>

<p>I started by doing my favorite hack in the VM, a good old print gated by an environment variable:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">if</span> <span class="p">(</span><span class="n">getenv</span><span class="p">(</span><span class="s">"DEB"</span><span class="p">))</span> <span class="p">{</span>
      <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">rb_obj_info</span><span class="p">(</span><span class="n">obj</span><span class="p">));</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>Then I modified the <a href="https://github.com/Shopify/yjit-bench/"><code class="language-plaintext highlighter-rouge">yjit-bench</code> suite</a> to set <code class="language-plaintext highlighter-rouge">ENV["DEB"] = "1"</code> at the start
of the benchmarks loops, as I’m more interested in runtime codepaths than in boottime ones.</p>

<p>I then ran the <a href="https://github.com/Shopify/shipit-engine"><code class="language-plaintext highlighter-rouge">shipit</code></a> benchmark while redirecting STDERR to a file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bundle <span class="nb">exec </span>ruby benchmark.rb 2&gt; /tmp/ivar-stats.txt
</code></pre></div></div>

<p>And did some quick number crunching with <code class="language-plaintext highlighter-rouge">irb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">File</span><span class="p">.</span><span class="nf">readlines</span><span class="p">(</span><span class="s2">"/tmp/ivar-stats.txt"</span><span class="p">,</span> <span class="ss">chomp: </span><span class="kp">true</span><span class="p">).</span><span class="nf">tally</span><span class="p">.</span><span class="nf">sort_by</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:last</span><span class="p">).</span><span class="nf">reverse</span>
</code></pre></div></div>

<p>Here are some results. It’s a very vanilla Rails 8 application, nothing fancy:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span>
 <span class="p">[</span><span class="s2">"VM/thread"</span><span class="p">,</span> <span class="mi">4886969</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"T_HASH"</span><span class="p">,</span> <span class="mi">229501</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"SQLite3::Backup"</span><span class="p">,</span> <span class="mi">122531</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"T_STRING"</span><span class="p">,</span> <span class="mi">70597</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"xmlDoc"</span><span class="p">,</span> <span class="mi">23625</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"T_ARRAY"</span><span class="p">,</span> <span class="mi">9039</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"OpenSSL/Cipher"</span><span class="p">,</span> <span class="mi">2800</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"xmlNode"</span><span class="p">,</span> <span class="mi">2025</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"encoding"</span><span class="p">,</span> <span class="mi">358</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"time"</span><span class="p">,</span> <span class="mi">199</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"proc"</span><span class="p">,</span> <span class="mi">68</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"T_STRUCT"</span><span class="p">,</span> <span class="mi">38</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"OpenSSL/X509/STORE"</span><span class="p">,</span> <span class="mi">3</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"Psych/parser"</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span>
 <span class="p">[</span><span class="s2">"set"</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span>
<span class="p">]</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">T_STRUCT</code> was there as I expected, but entirely dwarfed by other types.
For the ones that aren’t obvious:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">"VM/Thread"</code> is literally <code class="language-plaintext highlighter-rouge">Thread</code> instances.</li>
  <li><code class="language-plaintext highlighter-rouge">xmlNode</code> and <code class="language-plaintext highlighter-rouge">xmlDoc</code> are <code class="language-plaintext highlighter-rouge">nokogiri</code> objects.</li>
  <li>Anything that doesn’t start with <code class="language-plaintext highlighter-rouge">T_</code>, is a <code class="language-plaintext highlighter-rouge">T_DATA</code>.</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">T_HASH</code> I definitely didn’t expect, and it wasn’t clear where it was coming from. So I did another hack:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">getenv</span><span class="p">(</span><span class="s">"DEB"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">TYPE_P</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">T_HASH</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">rb_bug</span><span class="p">(</span><span class="s">"here"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">rb_bug</code> function causes the RubyVM to abort and print its crash report, which does contain the Ruby level-backtrace.
With that, I figured these were <a href="https://github.com/rack/rack/blob/9163ac3f5fac795179f9935e2ba6533a0ca1cf82/lib/rack/utils.rb#L436-L449"><code class="language-plaintext highlighter-rouge">Rack::Utils::HeaderHash</code></a> instances.</p>

<p>As for the <code class="language-plaintext highlighter-rouge">T_ARRAY</code>, it seems like it was mostly from <a href="https://github.com/rails/rails/blob/bb3ddbf032c3a24c2c94f911c8c5ca9f6939c6d9/activesupport/lib/active_support/inflector/inflections.rb#L33-L37"><code class="language-plaintext highlighter-rouge">ActiveSupport::Inflector::Inflections::Uncountables</code></a></p>

<p>And for <code class="language-plaintext highlighter-rouge">"VM/Thread"</code> it comes from <a href="https://github.com/rails/rails/blob/bb3ddbf032c3a24c2c94f911c8c5ca9f6939c6d9/activesupport/lib/active_support/isolated_execution_state.rb#L7-L8"><code class="language-plaintext highlighter-rouge">ActiveSupport::IsolatedExecutionState</code></a>.</p>

<p>All the rest was various <code class="language-plaintext highlighter-rouge">T_DATA</code> defined by C extensions, like the <code class="language-plaintext highlighter-rouge">trilogy</code> example I shared.</p>

<p>I ran a few other benchmarks from the <code class="language-plaintext highlighter-rouge">yjit-bench</code> repo, and often found similar generic instance variable usages.</p>

<p>So to answer the question, while it’s not that big of a hotspot, I believe it’s used enough to be worth optimizing, especially for <code class="language-plaintext highlighter-rouge">T_DATA</code>,
and not just because of Ractors.</p>

<h2 id="shaped-structs">Shaped Structs</h2>

<p>But as I said, before I got all that data, my sight was set on <code class="language-plaintext highlighter-rouge">T_STRUCT</code>.
Struct objects are laid out very similarly to <code class="language-plaintext highlighter-rouge">T_OBJECT</code> except that the space is used for “members” instead of instance variables.</p>

<p>For instance, the following struct:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">struct</span> <span class="o">=</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:field_1</span><span class="p">,</span> <span class="ss">:field_2</span><span class="p">).</span><span class="nf">new</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</code></pre></div></div>

<p>Would be laid out as is:</p>

<table>
  <thead>
    <tr>
      <th>flags</th>
      <th>klass</th>
      <th>field_1</th>
      <th>field_2</th>
      <th>-</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>T_STRUCT</td>
      <td>0xbbeaa</td>
      <td>1</td>
      <td>2</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>Hence, my initial idea was that if we were to encode the struct’s layout using shapes like we do for instance variables, we’d
be able to collocate members and variables together so that:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">MyStruct</span> <span class="o">=</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:field_1</span><span class="p">,</span> <span class="ss">:field_2</span><span class="p">)</span> <span class="k">do</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
    <span class="k">super</span>
    <span class="vi">@c</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Could be laid out as:</p>

<table>
  <thead>
    <tr>
      <th>flags</th>
      <th>klass</th>
      <th>field_1</th>
      <th>field_2</th>
      <th>@c</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>T_STRUCT</td>
      <td>0xffeaa</td>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
  </tbody>
</table>

<p>Which would be perfect. Everything would be embedded in the object slot, so we’d have minimal memory usage and access times.</p>

<p>Unfortunately, after putting some more thought into it, I realized that was a major problem with it: complex shapes.
I <a href="https://railsatscale.com/2023-10-24-memoization-pattern-and-object-shapes/#shape_too_complex">previously wrote at length on what complex shapes are</a>, so very quickly,
in the Ruby VM, shapes aren’t garbage collected, so if some code generates a lot of different shapes, Ruby will deoptimize the object and use a hash table to store
its instance variables. It also does the same if the program uses all the possible shape slots.</p>

<p>So if <code class="language-plaintext highlighter-rouge">Struct</code> members were encoded with shapes, we’d need to have many fallback code paths to handle complex structs,
and for some of the struct APIs, that is straight out impossible, because Struct objects can be treated like arrays:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:a</span><span class="p">,</span> <span class="ss">:b</span><span class="p">).</span><span class="nf">new</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
<span class="o">=&gt;</span> <span class="mi">2</span>
</code></pre></div></div>

<p>In such a case, all we have is the member offset, so if the struct was deoptimized into a hash, we wouldn’t be able to look up members by index anymore, short of keeping a reverse index, but that’s really a lot of extra complexity.
So I abandoned this idea.</p>

<h2 id="shape-offset">Shape Offset</h2>

<p>A few days later, I was brainstorming with Étienne Barrié, and we thought of a simpler solution.
Instead of encoding struct members in shapes, we could introduce a new type of shape to encode at which offset the instance variables start.</p>

<p>As often mentioned, shapes are a tree, so an object with variables <code class="language-plaintext highlighter-rouge">@a -&gt; @b -&gt; @c -&gt; @d</code>, the shape tree would look like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ROOT_SHAPE</span>
  <span class="p">\</span><span class="o">-</span> <span class="no">Ivar</span><span class="p">(</span><span class="ss">name: :@a</span><span class="p">,</span> <span class="ss">index: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">3</span><span class="p">)</span>
    <span class="p">\</span><span class="o">-</span> <span class="no">Ivar</span><span class="p">(</span><span class="ss">name: :@b</span><span class="p">,</span> <span class="ss">index: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">3</span><span class="p">)</span>
      <span class="p">\</span><span class="o">-</span> <span class="no">Ivar</span><span class="p">(</span><span class="ss">name: :@c</span><span class="p">,</span> <span class="ss">index: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">3</span><span class="p">)</span>
        <span class="p">\</span><span class="o">-</span> <span class="no">Ivar</span><span class="p">(</span><span class="ss">name: :@d</span><span class="p">,</span> <span class="ss">index: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">8</span><span class="p">)</span>
</code></pre></div></div>

<p>With offset shapes, the same instance variable list, but for a struct with two members, would look like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ROOT_SHAPE</span>
  <span class="p">\</span><span class="o">-</span> <span class="no">Offset</span><span class="p">(</span><span class="ss">index: index: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">3</span><span class="p">)</span>
    <span class="p">\</span><span class="o">-</span> <span class="no">Ivar</span><span class="p">(</span><span class="ss">name: :@a</span><span class="p">,</span> <span class="ss">index: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">3</span><span class="p">)</span>
      <span class="p">\</span><span class="o">-</span> <span class="no">Ivar</span><span class="p">(</span><span class="ss">name: :@b</span><span class="p">,</span> <span class="ss">index: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">8</span><span class="p">)</span>
        <span class="p">\</span><span class="o">-</span> <span class="no">Ivar</span><span class="p">(</span><span class="ss">name: :@c</span><span class="p">,</span> <span class="ss">index: </span><span class="mi">4</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">8</span><span class="p">)</span>
          <span class="p">\</span><span class="o">-</span> <span class="no">Ivar</span><span class="p">(</span><span class="ss">name: :@d</span><span class="p">,</span> <span class="ss">index: </span><span class="mi">5</span><span class="p">,</span> <span class="ss">capacity: </span><span class="mi">8</span><span class="p">)</span>
</code></pre></div></div>

<p>Here again, we’d need to handle the case where the Ruby VM ran out of shapes, but at least only the instance variables
would be deoptimized into a hash table, the struct members would still be laid out like an array, saving a ton of complexity.</p>

<p>That being said, while I still think this is a good idea, it’s a fairly big project with some uncertainties.
So when I evoked this solution with Peter Zhu, he suggested something much simpler.</p>

<h2 id="direct-references">Direct References</h2>

<p>The annoying thing with generic instance variables isn’t so much that they aren’t embedded inside the object’s slot, but that to find the companion slot, you need to go through that global hash table.</p>

<p>Of course, if they were embedded, it would mean better data locality, which is good for performance, but that really isn’t much compared to the hash-lookup, so a single pointer chase would already be a major win.</p>

<p>Hence, Peter’s suggestion was to just use empty space in struct slots to keep a direct reference to the buffer that holds
the instance variables, and since structs are basically fixed-size arrays, we can store that reference right after the
last struct member.</p>

<p>In pseudo-code, it would be more or less:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Struct</span>
  <span class="k">def</span> <span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">ivar</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">__slot_capacity__</span> <span class="o">&gt;</span> <span class="n">size</span>
      <span class="nb">self</span><span class="p">[</span><span class="n">size</span><span class="p">].</span><span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">ivar</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="c1"># use the generic instance variables table</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That’s essentially the same strategy as with classes and modules.</p>

<p>At least on paper, that was quite easy because <a href="https://github.com/ruby/ruby/pull/13626">a few weeks prior, I had refactored the generic instance variables to use the same underlying managed object as classes</a>: <code class="language-plaintext highlighter-rouge">T_IMEMO/fields</code>.</p>

<p>Once again, <a href="https://github.com/ruby/ruby/pull/14095">I paired with Étienne Barrié to implement that idea</a>, but the resulting PR was way larger and more complex than I had hoped for, because of a lack of encapsulation.</p>

<p>In many places across the VM, when dealing with instance variables, you have a similar big <code class="language-plaintext highlighter-rouge">switch/case</code> statement with
a branch for each of the 3 or 4 possible types of object layouts.
So making <code class="language-plaintext highlighter-rouge">T_STRUCT</code> different would mean adding one more code path in all these places, which would leave me with a bad taste in my mouth.</p>

<p>That’s why I backtracked a bit and decided to start by <a href="https://github.com/ruby/ruby/pull/14107">refactoring the generic instance variables table, so that all accesses go through a very small number of functions</a>.
After that, all reads and writes to the table went through mostly just two functions, making it the perfect place to specialize the behavior for struct objects.</p>

<p>As a bit of a sidenote, the more I work on the Ruby VM, the more I realize the challenging part isn’t to come up with a brilliant idea,
or a clever algorithm, but the sheer effort required to refactor code without breaking everything.
The C language doesn’t have a lot of features for abstractions and encapsulation, so coupling is absolutely everywhere.</p>

<p>Anyways, with that refactoring done, I was able to re-implement <a href="https://github.com/ruby/ruby/pull/14129">the same pull request we did with Étienne, but half the size</a>, most of it being just tests, documentation, and benchmarking code.</p>

<p>Now the generic instance variable lookup function looks like this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">VALUE</span>
<span class="nf">rb_obj_fields</span><span class="p">(</span><span class="n">VALUE</span> <span class="n">obj</span><span class="p">,</span> <span class="n">ID</span> <span class="n">field_name</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">RUBY_ASSERT</span><span class="p">(</span><span class="o">!</span><span class="n">RB_TYPE_P</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">T_IMEMO</span><span class="p">));</span>
    <span class="n">ivar_ractor_check</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">field_name</span><span class="p">);</span>

    <span class="n">VALUE</span> <span class="n">fields_obj</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">rb_shape_obj_has_fields</span><span class="p">(</span><span class="n">obj</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">switch</span> <span class="p">(</span><span class="n">BUILTIN_TYPE</span><span class="p">(</span><span class="n">obj</span><span class="p">))</span> <span class="p">{</span>
          <span class="k">case</span> <span class="n">T_STRUCT</span><span class="p">:</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">LIKELY</span><span class="p">(</span><span class="o">!</span><span class="n">FL_TEST_RAW</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">RSTRUCT_GEN_FIELDS</span><span class="p">)))</span> <span class="p">{</span>
                <span class="n">fields_obj</span> <span class="o">=</span> <span class="n">RSTRUCT_FIELDS_OBJ</span><span class="p">(</span><span class="n">obj</span><span class="p">);</span>
                <span class="k">break</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="c1">// fall through</span>
          <span class="nl">default:</span>
            <span class="n">RB_VM_LOCKING</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">st_lookup</span><span class="p">(</span><span class="n">generic_fields_tbl_</span><span class="p">,</span> <span class="p">(</span><span class="n">st_data_t</span><span class="p">)</span><span class="n">obj</span><span class="p">,</span> <span class="p">(</span><span class="n">st_data_t</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">fields_obj</span><span class="p">))</span> <span class="p">{</span>
                    <span class="n">rb_bug</span><span class="p">(</span><span class="s">"Object is missing entry in generic_fields_tbl"</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">fields_obj</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When dealing with a <code class="language-plaintext highlighter-rouge">T_STRUCT</code> and if there’s some unused space in the slot, we entirely bypass the <code class="language-plaintext highlighter-rouge">generic_fields_tbl</code> and <code class="language-plaintext highlighter-rouge">RB_VM_LOCKING</code>.</p>

<p>And to ensure we don’t fall in the fallback path too much, we modified the Struct allocator to allocate a large enough slots for structs
that have instance variables:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">VALUE</span>
<span class="nf">struct_alloc</span><span class="p">(</span><span class="n">VALUE</span> <span class="n">klass</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">long</span> <span class="n">n</span> <span class="o">=</span> <span class="n">num_members</span><span class="p">(</span><span class="n">klass</span><span class="p">);</span>
    <span class="kt">size_t</span> <span class="n">embedded_size</span> <span class="o">=</span> <span class="n">offsetof</span><span class="p">(</span><span class="k">struct</span> <span class="n">RStruct</span><span class="p">,</span> <span class="n">as</span><span class="p">.</span><span class="n">ary</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="n">VALUE</span><span class="p">)</span> <span class="o">*</span> <span class="n">n</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">RCLASS_MAX_IV_COUNT</span><span class="p">(</span><span class="n">klass</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">embedded_size</span> <span class="o">+=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">VALUE</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// snip...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As a result, instance variable accesses in structs are now noticeably faster, even when no ractor is involved:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>compare-ruby: ruby 3.5.0dev (2025-08-06T12:50:36Z struct-ivar-fields-2 9a30d141a1) +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-08-06T12:57:59Z struct-ivar-fields-2 2ff3ec237f) +PRISM [arm64-darwin24]
warming up.....

|                      |compare-ruby|built-ruby|
|:---------------------|-----------:|---------:|
|member_reader         |    590.317k|  579.246k|
|                      |       1.02x|         -|
|member_writer         |    543.963k|  527.104k|
|                      |       1.03x|         -|
|member_reader_method  |    213.540k|  213.004k|
|                      |       1.00x|         -|
|member_writer_method  |    192.657k|  191.491k|
|                      |       1.01x|         -|
|ivar_reader           |    403.993k|  569.915k|
|                      |           -|     1.41x|
</code></pre></div></div>

<p>That was a satisfying change.</p>

<h2 id="generalizing-to-other-types">Generalizing to Other Types</h2>

<p>Now that we had a working pattern, the question was where else could we apply it.</p>

<p>I definitely knew instance variables on <code class="language-plaintext highlighter-rouge">T_STRING</code> are rather common, given I’m very familiar with <code class="language-plaintext highlighter-rouge">ActiveSupport::SafeBuffer</code>, so I thought about pulling a similar trick for them.</p>

<p>Unfortunately, what made this possible with <code class="language-plaintext highlighter-rouge">T_STRUCT</code> is that they are essentially fixed-size arrays.
Which means we know that whatever free space is left in the slot won’t ever be needed in the future.</p>

<p>Whereas other types like <code class="language-plaintext highlighter-rouge">T_STRING</code> and <code class="language-plaintext highlighter-rouge">T_ARRAY</code> are variable size.
If you start storing a reference in free space at the end of the slot, you then need to be very careful that if the user appends
to the string or array, it won’t overwrite that reference. That’s much harder to do and probably not worth the extra complexity.</p>

<p>But one of my favorite things with Ruby and Rails is to be able to optimize from both ends.
If some pattern Rails uses isn’t very performant, I can try to optimize Ruby, but I can also just change what Rails does.</p>

<p>In the case of <code class="language-plaintext highlighter-rouge">ActiveSupport::SafeBuffer</code>, all we’re storing is just a boolean: <code class="language-plaintext highlighter-rouge">@html_safe = true</code>, and eventually, if something is appended to the buffer, the flag will be flipped.
But appends into safe buffers are very rare.</p>

<p>Most of the time, <code class="language-plaintext highlighter-rouge">String#html_safe</code> is only used as a way to tag the string, to indicate that it doesn’t need to be escaped when it’s later appended into another buffer. In other words, the overwhelming majority of instances never flip that flag.</p>

<p>Based on that knowledge, <a href="https://github.com/rails/rails/pull/55352">I changed that variable to be a negative</a>.
Instead of starting with <code class="language-plaintext highlighter-rouge">@html_safe = true</code>, we can start with <code class="language-plaintext highlighter-rouge">@html_unsafe = false</code>, and since referencing an instance
variable that doesn’t exist evaluates to <code class="language-plaintext highlighter-rouge">nil</code>, which is also falsy, we can simply not set the variable at all.</p>

<p>The result made <code class="language-plaintext highlighter-rouge">String#html_safe</code> twice as fast, even when no Ractor is started:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ruby 3.5.0dev (2025-07-17T14:01:57Z master a46309d19a) +YJIT +PRISM [arm64-darwin24]
Calculating -------------------------------------
    String#html_safe (old)     6.421M (± 1.6%) i/s  (155.75 ns/i) -     32.241M in   5.022802s
    String#html_safe          12.470M (± 0.8%) i/s   (80.19 ns/i) -     63.140M in   5.063698s
</code></pre></div></div>

<p>I guess this is a good example of <a href="https://en.wiktionary.org/wiki/mechanical_sympathy">mechanical sympathy</a><sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>, the more you know about how the tools you are using work, the more effectively you can use them.</p>

<p>And now that I learned about <code class="language-plaintext highlighter-rouge">ActiveSupport::Inflector::Inflections::Uncountables</code>, I should probably change it in a similar way.</p>

<p>But the one other type that I thought was worth attention to was <code class="language-plaintext highlighter-rouge">T_DATA</code>.</p>

<h2 id="typeddata">TypedData</h2>

<p>Until just a few months ago, <code class="language-plaintext highlighter-rouge">T_DATA</code> slots were fully used; here’s the <code class="language-plaintext highlighter-rouge">RTypedData</code> C struct in Ruby 3.4,
I added some annotations with the size of each field:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">RTypedData</span> <span class="p">{</span>
    <span class="cm">/** The part that all ruby objects have in common. */</span>
    <span class="k">struct</span> <span class="n">RBasic</span> <span class="n">basic</span><span class="p">;</span> <span class="c1">// 16B</span>

    <span class="cm">/**
     * This field  stores various  information about how  Ruby should  handle a
     * data.   This roughly  resembles a  Ruby level  class (apart  from method
     * definition etc.)
     */</span>
    <span class="k">const</span> <span class="n">rb_data_type_t</span> <span class="o">*</span><span class="k">const</span> <span class="n">type</span><span class="p">;</span> <span class="c1">// 8B</span>

    <span class="cm">/**
     * This has to be always 1.
     *
     * @internal
     */</span>
    <span class="k">const</span> <span class="n">VALUE</span> <span class="n">typed_flag</span><span class="p">;</span> <span class="c1">// 8B</span>

    <span class="cm">/** Pointer to the actual C level struct that you want to wrap. */</span>
    <span class="kt">void</span> <span class="o">*</span><span class="n">data</span><span class="p">;</span> <span class="c1">// 8B</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Just quickly, the first <code class="language-plaintext highlighter-rouge">16B</code> was used for the common header all Ruby objects share, <code class="language-plaintext highlighter-rouge">8B</code> was used to store a pointer
to another struct that gives information to Ruby on what to do with this object, for instance, how to garbage collect it.</p>

<p>And then two other <code class="language-plaintext highlighter-rouge">8B</code> values, one pointing to arbitrary memory a C extension might have allocated, and then <code class="language-plaintext highlighter-rouge">typed_flag</code>.
If you read the comment associated with <code class="language-plaintext highlighter-rouge">typed_flag</code>, you may wonder what purpose it can possibly serve.</p>

<p>It’s there because <code class="language-plaintext highlighter-rouge">RTypedData</code> is the newer API for C extensions that was introduced in 2009 by Koichi Sasada.
Historically, when you needed to wrap a piece of native memory in a Ruby object, you’d use the <code class="language-plaintext highlighter-rouge">RData</code> API, and you had to
supply:</p>

<ul>
  <li>A pointer to the memory region.</li>
  <li>A marking function for the GC.</li>
  <li>A free function for the GC.</li>
</ul>

<p>That older, deprecated API is still there today, and you can see the struct that backs it up:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * @deprecated
 *
 * Old  "untyped"  user  data.   It  has  roughly  the  same  usage  as  struct
 * ::RTypedData, but lacked several features such as support for compaction GC.
 * Use of this struct is not recommended  any longer.  If it is dead necessary,
 * please inform the core devs about your usage.
 *
 * @internal
 *
 * @shyouhei tried to add RBIMPL_ATTR_DEPRECATED for this type but that yielded
 * too many warnings  in the core.  Maybe  we want to retry  later...  Just add
 * deprecated document for now.
 */</span>
<span class="k">struct</span> <span class="n">RData</span> <span class="p">{</span>

    <span class="cm">/** Basic part, including flags and class. */</span>
    <span class="k">struct</span> <span class="n">RBasic</span> <span class="n">basic</span><span class="p">;</span>

    <span class="cm">/**
     * This function is called when the object is experiencing GC marks.  If it
     * contains references to  other Ruby objects, you need to  mark them also.
     * Otherwise GC will smash your data.
     *
     * @see      rb_gc_mark()
     * @warning  This  is  called  during  GC  runs.   Object  allocations  are
     *           impossible at that moment (that is why GC runs).
     */</span>
    <span class="n">RUBY_DATA_FUNC</span> <span class="n">dmark</span><span class="p">;</span>

    <span class="cm">/**
     * This function is called when the object  is no longer used.  You need to
     * do whatever necessary to avoid memory leaks.
     *
     * @warning  This  is  called  during  GC  runs.   Object  allocations  are
     *           impossible at that moment (that is why GC runs).
     */</span>
    <span class="n">RUBY_DATA_FUNC</span> <span class="n">dfree</span><span class="p">;</span>

    <span class="cm">/** Pointer to the actual C level struct that you want to wrap. */</span>
    <span class="kt">void</span> <span class="o">*</span><span class="n">data</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>So in various places in the Ruby VM, when you interact with a <code class="language-plaintext highlighter-rouge">T_DATA</code> object, you need to know if it’s a <code class="language-plaintext highlighter-rouge">RTypedData</code> or a <code class="language-plaintext highlighter-rouge">RData</code>
before you can do much of anything with it.</p>

<p>That’s where <code class="language-plaintext highlighter-rouge">typed_flag</code> comes in. It’s at the same offset in the <code class="language-plaintext highlighter-rouge">RTypedData</code>struct as the <code class="language-plaintext highlighter-rouge">dfree</code> pointer in the <code class="language-plaintext highlighter-rouge">RData</code> struct, and for various reasons, it’s impossible for a legitimate C function pointer to be strictly equal to <code class="language-plaintext highlighter-rouge">1</code>.</p>

<p>That’s why <code class="language-plaintext highlighter-rouge">typed_flag</code> is always <code class="language-plaintext highlighter-rouge">1</code>, it allows us to check if a <code class="language-plaintext highlighter-rouge">T_DATA</code> is typed by checking <code class="language-plaintext highlighter-rouge">rdata-&gt;dfree == 1</code>.</p>

<p>Now you might wonder why I’m telling you all of this.
Well, it’s because that <code class="language-plaintext highlighter-rouge">typed_flag</code> field is using <code class="language-plaintext highlighter-rouge">8B</code> of space to store exactly <code class="language-plaintext highlighter-rouge">1bit</code> of information, and that has bugged me for several years.</p>

<p>Even though truth be told, the comment is outdated, and the field can also sometimes be <code class="language-plaintext highlighter-rouge">3</code> as <a href="https://railsatscale.com/2025-06-03-implementing-embedded-typeddata-objects/">we piggy-backed on it with Peter Zhu last year to implement embedded TypedData objects</a>.
But that’s still 32 times more than needed, so if someone could think of a better place to store these two bits, that would free and entire <code class="language-plaintext highlighter-rouge">8B</code> to store a direct reference to the <code class="language-plaintext highlighter-rouge">T_IMEMO/fields</code>.</p>

<h2 id="enter-set-man">Enter Set Man</h2>

<p>Well, it turns out that someone did earlier this year.</p>

<p>Just before the RubyKaigi developer meeting, Jeremy Evans <a href="https://bugs.ruby-lang.org/issues/21216">proposed to turn <code class="language-plaintext highlighter-rouge">Set</code> into a core class, and to reimplement it in C</a>, and that was accepted.
Later during the conference, he asked me to <a href="https://github.com/ruby/ruby/pull/13074">review his usage of the RTypedData API</a>, and I suggested a bunch of improvements to make <code class="language-plaintext highlighter-rouge">Set</code>
objects smaller and reduce pointer chasing by leveraging embedded RTypedData objects.</p>

<p>But turns out that there was a bit of an annoying tradeoff here. The <code class="language-plaintext highlighter-rouge">RTypedData</code> struct is <code class="language-plaintext highlighter-rouge">40B</code> large, but when used embedded, we recycle the <code class="language-plaintext highlighter-rouge">data</code> pointer, so it’s only <code class="language-plaintext highlighter-rouge">32B</code> large,
and the <code class="language-plaintext highlighter-rouge">set_table</code> struct Jememy needed to store is <code class="language-plaintext highlighter-rouge">56B</code>, for a total of <code class="language-plaintext highlighter-rouge">88B</code>, which is a particularly annoying number.</p>

<p>Not because of the meaning some distasteful people attribute to it, but because it is just <code class="language-plaintext highlighter-rouge">8B</code> too large to fit in a standard <code class="language-plaintext highlighter-rouge">80B</code> GC slot, hence if we marked it as embeded, the footprint would grow from <code class="language-plaintext highlighter-rouge">40 + 56 = 96B</code> to <code class="language-plaintext highlighter-rouge">160B</code> with lots of wasted space.</p>

<p>In all honesty, it wasn’t a massive problem unless your application is using a massive amount of sets, but it seems that it really bothered Jeremy.</p>

<p>What he came up with a couple of weeks later was that <a href="https://github.com/ruby/ruby/pull/13190">he moved these two bits of memory into the low bits of <code class="language-plaintext highlighter-rouge">RTypedData.type</code> and <code class="language-plaintext highlighter-rouge">RData.dmark</code></a>,
freeing <code class="language-plaintext highlighter-rouge">8B</code> per embedded TypedData object and allowing <code class="language-plaintext highlighter-rouge">Set</code> objects to fit in 80B.</p>

<p>Here again, the assumption was that because of alignment rules, the three lower bits of pointers can’t ever be set, so we can store our own information in there.</p>

<p>But now, I think <a href="https://github.com/ruby/ruby/pull/14134">this space could be put to better use to store a reference to a companion <code class="language-plaintext highlighter-rouge">T_IMEMO/fields</code></a>, so we could skip the global instance variables table.
The problem is that here again it’s a matter of tradeoff. We can waste some memory to save some CPU cycles, which is better is really just a judgment call.</p>

<p>Just like this issue bothered Jeremy a few months back, it now bothered me, and I went searching for a way to save another <code class="language-plaintext highlighter-rouge">8B</code> in <code class="language-plaintext highlighter-rouge">Set</code> objects.</p>

<h2 id="shrinking-set">Shrinking Set</h2>

<p>Hence, I started to stare at the <code class="language-plaintext highlighter-rouge">struct set_table</code> while frowning my eyebrows in the hope of spotting some redundant or superfluous member I could eliminate:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">set_table</span> <span class="p">{</span>
    <span class="cm">/* Cached features of the table -- see st.c for more details.  */</span>
    <span class="kt">unsigned</span> <span class="kt">char</span> <span class="n">entry_power</span><span class="p">,</span> <span class="n">bin_power</span><span class="p">,</span> <span class="n">size_ind</span><span class="p">;</span>
    <span class="cm">/* How many times the table was rebuilt.  */</span>
    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">rebuilds_num</span><span class="p">;</span>
    <span class="k">const</span> <span class="k">struct</span> <span class="n">st_hash_type</span> <span class="o">*</span><span class="n">type</span><span class="p">;</span>
    <span class="cm">/* Number of entries currently in the table.  */</span>
    <span class="n">st_index_t</span> <span class="n">num_entries</span><span class="p">;</span>
    <span class="cm">/* Array of bins used for access by keys.  */</span>
    <span class="n">st_index_t</span> <span class="o">*</span><span class="n">bins</span><span class="p">;</span>
    <span class="cm">/* Start and bound index of entries in array entries.
       entries_starts and entries_bound are in interval
       [0,allocated_entries].  */</span>
    <span class="n">st_index_t</span> <span class="n">entries_start</span><span class="p">,</span> <span class="n">entries_bound</span><span class="p">;</span>
    <span class="cm">/* Array of size 2^entry_power.  */</span>
    <span class="n">set_table_entry</span> <span class="o">*</span><span class="n">entries</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>I was first attracted to the trio of <code class="language-plaintext highlighter-rouge">num_entries</code>, <code class="language-plaintext highlighter-rouge">entries_start</code>, and <code class="language-plaintext highlighter-rouge">entries_bound</code>. All of these are <code class="language-plaintext highlighter-rouge">8B</code> integers, so if I could eliminate just one of them, I’d be set<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p>

<p>Without being really intimate with the set implementation, I guessed that surely, if you know how many entries you have, you don’t need both the offset of the start and end of the entries list.
So in theory, I could just replace every reference to <code class="language-plaintext highlighter-rouge">entries_bound</code> by <code class="language-plaintext highlighter-rouge">entries_start + num_entries</code>.</p>

<p>What I do when I experiment with code I’m not fully familiar with, is that I try to prove my assumptions.
Here I wrote a small helper function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kr">inline</span> <span class="n">st_index_t</span>
<span class="nf">set_entries_bound</span><span class="p">(</span><span class="k">const</span> <span class="k">struct</span> <span class="n">set_table</span> <span class="o">*</span><span class="n">set</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">RUBY_ASSERT</span><span class="p">(</span><span class="n">set</span><span class="o">-&gt;</span><span class="n">entries_start</span> <span class="o">+</span> <span class="n">set</span><span class="o">-&gt;</span><span class="n">num_entries</span> <span class="o">==</span> <span class="n">set</span><span class="o">-&gt;</span><span class="n">entries_bound</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">set</span><span class="o">-&gt;</span><span class="n">entries_bound</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then went over the code to replace all the direct accesses to <code class="language-plaintext highlighter-rouge">set-&gt;entries_bound</code> by my helper, and tried to run the test suite to see if that <code class="language-plaintext highlighter-rouge">RUBY_ASSERT</code> would trip or not.</p>

<p>Well, turns out it wasn’t that simple… After seeing the test suite light up like a Christmas tree, I dug into the code
helped by the backtraces in the crash reports, and realized the <code class="language-plaintext highlighter-rouge">entries_bound</code> doesn’t always match the entries’ size,
There is even a comment about it in the code:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="cm">/* Do not update entries_bound here.  Otherwise, we can fill all
       bins by deleted entry value before rebuilding the table.  */</span>
</code></pre></div></div>

<p>So that was a bust, and I went back to the drawing board.</p>

<p>After some more staring and eyebrow frowning, I got another idea.</p>

<p>Ruby’s hash-tables (Ruby sets are hash-sets) are ordered.
Hence, you can see them as the combination of a regular unordered hash table and a classic array. The hash-table values are just offset into that array.</p>

<p>Here, the hash-table part is the <code class="language-plaintext highlighter-rouge">st_index_t *bins</code>, and the array part is <code class="language-plaintext highlighter-rouge">set_table_entry *entries</code>.</p>

<p>Both of these are memory regions allocated with <code class="language-plaintext highlighter-rouge">malloc</code>, and they are grown and shrunk at the same time when you add or remove elements from the set.</p>

<p>Hence, if we can know how large one of them is, we could allocate both with a single <code class="language-plaintext highlighter-rouge">malloc</code>, and then access the other by simply skipping over the first one.</p>

<p>In this case, the size of <code class="language-plaintext highlighter-rouge">set_table.bins</code> is indicated by <code class="language-plaintext highlighter-rouge">set_table.bin_power</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Return size of the allocated bins of table TAB.  */</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="n">st_index_t</span>
<span class="nf">set_bins_size</span><span class="p">(</span><span class="k">const</span> <span class="n">set_table</span> <span class="o">*</span><span class="n">tab</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">features</span><span class="p">[</span><span class="n">tab</span><span class="o">-&gt;</span><span class="n">entry_power</span><span class="p">].</span><span class="n">bins_words</span> <span class="o">*</span> <span class="k">sizeof</span> <span class="p">(</span><span class="n">st_index_t</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That’s how <a href="https://github.com/ruby/ruby/commit/9250ece276bae357a6ac42cb832c67bbfab0eb01">with a relatively small patch, I was able to save 8B from <code class="language-plaintext highlighter-rouge">struct set_table</code></a>,
which could allow us to keep <code class="language-plaintext highlighter-rouge">Set</code> objects in <code class="language-plaintext highlighter-rouge">80B</code> slots even if we make embedded <code class="language-plaintext highlighter-rouge">RTypedData</code> <code class="language-plaintext highlighter-rouge">32B</code> again.</p>

<p>However, I still need to run some benchmarks to make sure this patch wouldn’t degrade set performance significantly.</p>

<h2 id="lookup-cache">Lookup Cache</h2>

<p>For some remaining types like <code class="language-plaintext highlighter-rouge">T_STRING</code>, <code class="language-plaintext highlighter-rouge">T_ARRAY</code>, or <code class="language-plaintext highlighter-rouge">T_HASH</code>, it’s unlikely we’ll ever find spaces in their slots for an extra reference.
So I had another idea to speed up accesses and reduce contention.</p>

<p>The core of the assumption is that whenever we look up the instance variables of an object, there is a high chance that the next lookup will be for the same object.</p>

<p>So what if we kept a cache of the last object we looked up, and its associated <code class="language-plaintext highlighter-rouge">T_IMEMO/fields</code>?</p>

<p>In pseudo-ruby:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">GenericIvarObject</span>
  <span class="no">GENERIC_FIELDS_TBL</span> <span class="o">=</span> <span class="no">Hash</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">compare_by_identity</span>

  <span class="k">def</span> <span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">ivar_shape</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">shape</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
      <span class="n">fields_obj</span> <span class="o">=</span> <span class="k">if</span> <span class="no">Fiber</span><span class="p">[</span><span class="ss">:__last_obj__</span><span class="p">]</span> <span class="o">==</span> <span class="nb">self</span>
        <span class="no">Fiber</span><span class="p">[</span><span class="ss">:__last_fields__</span><span class="p">]</span>
      <span class="k">else</span>
        <span class="no">Fiber</span><span class="p">[</span><span class="ss">:__last_obj__</span><span class="p">]</span> <span class="o">=</span> <span class="nb">self</span>
        <span class="no">Fiber</span><span class="p">[</span><span class="ss">:__last_obj__</span><span class="p">]</span> <span class="o">=</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
          <span class="no">GENERIC_FIELDS_TBL</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span>
        <span class="k">end</span>
      <span class="k">end</span>

      <span class="n">fields_obj</span><span class="p">.</span><span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Given that the cache is in fiber local storage, we don’t need to protect it with a lock.</p>

<p>I have <a href="https://github.com/ruby/ruby/pull/14132">a draft patch for that idea</a> that I need to polish and benchmark, but I like that it’s quite simple.</p>

<h2 id="future-work">Future Work</h2>

<p>Ultimately, for the remaining cases, it would be good if the Ruby VM had a proper concurrent-map implementation to allow lock-free lookups into the generic instance variables table.
However, concurrent maps are <em>hard</em>, so it might not happen any time soon.</p>

<p>In the meantime, for the more important types like <code class="language-plaintext highlighter-rouge">T_STRUCT</code> and <code class="language-plaintext highlighter-rouge">T_DATA</code>, we now have solutions, either already merged or potentially soon to be, and for others, we have a way to reduce how often we look up the table.
And all that improves performance for both single-threaded and multi-ractor applications, so it’s a win-win.</p>

<p>My biggest concern with Ractors is that at some point we’d significantly impact single-threaded performance for the benefit of Ractors, so when we find optimizations that improve both use-cases, I’m particularly happy.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>You might think that since an object can’t be visible by more than one ractor unless it is frozen, then this isn’t a concern. But actually, since <code class="language-plaintext highlighter-rouge">object_id</code> is now essentially a memoized instance variable, it can happen. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>There was <a href="https://www.youtube.com/watch?v=wCOuJB6MEQo">a pretty good talk on that subject</a> at Euruko 2024. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>Pun intended. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="ruby" /><category term="performance" /><summary type="html"><![CDATA[In two previous posts, I explained that one of the big blockers for Ractors’ viability is that while they’re supposed to run fully in parallel, in many cases, they’d perform worse than a single thread because there were numerous codepaths in the Ruby virtual machine and runtime that were still protected by the global VM lock.]]></summary></entry><entry><title type="html">What’s wrong with the JSON gem API?</title><link href="https://byroot.github.io/ruby/json/2025/08/02/whats-wrong-with-the-json-gem-api.html" rel="alternate" type="text/html" title="What’s wrong with the JSON gem API?" /><published>2025-08-02T09:03:51+00:00</published><updated>2025-08-02T09:03:51+00:00</updated><id>https://byroot.github.io/ruby/json/2025/08/02/whats-wrong-with-the-json-gem-api</id><content type="html" xml:base="https://byroot.github.io/ruby/json/2025/08/02/whats-wrong-with-the-json-gem-api.html"><![CDATA[<p>As I mentioned at the start of my <a href="/ruby/json/2024/12/15/optimizing-ruby-json-part-1.html">Optimizing Ruby’s JSON</a> series of posts,
performance isn’t why I candidated to be the new gem’s maintainer.</p>

<p>The actual reason is that the gem has many APIs that I think aren’t very good, and some that are outright dangerous.</p>

<p>As a gem user, it’s easy to be annoyed at deprecations and breaking changes.
It’s noisy and creates extra work, so I entirely understand that people may suffer from deprecation fatigue.
But while it occasionally happens to run into mostly cosmetic deprecations that aren’t really worth the churn they cause (and that annoys me a lot too),
most of the time there’s a good reason for them, it just is very rarely conveyed to the users, and even more rarely discussed,
so let’s do that for once.</p>

<p>So I’d like to go over some of the API changes and deprecations I already implemented or will likely implement soon,
given it’s a good occasion to explain why the change is valuable, and to talk about API design more broadly.</p>

<h2 id="dealing-with-deprecations-in-ruby">Dealing With Deprecations in Ruby</h2>

<p>But before I delve into deprecated API, I’d like to mention how to effectively deal with deprecations in modern Ruby.</p>

<p>Since Ruby 2.7, warning messages emitted with <code class="language-plaintext highlighter-rouge">Kernel#warn</code> are categorized, and one of the available categories is <code class="language-plaintext highlighter-rouge">:deprecated</code>.
By default, deprecation warnings are silenced; to display them, you must enable the <code class="language-plaintext highlighter-rouge">:deprecated</code> category like so:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Warning</span><span class="p">[</span><span class="ss">:deprecated</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>
</code></pre></div></div>

<p>It is very highly recommended to do so in your test suite, so much so that Rails and Minitest will do it by default.</p>

<p>However, if you are using RSpec, you’ll have to do it yourself in your <code class="language-plaintext highlighter-rouge">spec_helper.rb</code> file, because we’ve tried to get
<a href="https://github.com/rspec/rspec/issues/37">RSpec to do it too for over four years now, but without success</a>.
But I’m still hopeful <a href="https://github.com/rspec/rspec/pull/161">it will eventually happen</a>.</p>

<p>Another useful thing to know about Ruby’s <code class="language-plaintext highlighter-rouge">Kernel#warn</code> method is that under the hood, it calls the <code class="language-plaintext highlighter-rouge">Warning.warn</code> method,
allowing you to redefine it and customize its behavior.</p>

<p>For instance, you could turn warnings into errors like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Warning</span>
  <span class="k">def</span> <span class="nf">warn</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="o">...</span><span class="p">)</span>
    <span class="k">raise</span> <span class="n">message</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Doing so both ensures warnings aren’t missed, and helps tracking them down as you’ll get an exception with a full backtrace
rather than a warning that points at a single call-site that may not necessarily help you find the problem.</p>

<p>This is a pattern I use in most of my own projects, and that <a href="https://github.com/rails/rails/blob/add5a73b26e78d6b13945525874749ae40af21c7/tools/strict_warnings.rb">I also included into Rails’ own test suite</a>.
For larger projects, where being deprecation-free all the time may be complicated, there’s also the more sophisticated <a href="https://github.com/Shopify/deprecation_toolkit"><code class="language-plaintext highlighter-rouge">deprecation_toolkit</code> gem</a>.</p>

<h2 id="the-create_additions-option">The create_additions Option</h2>

<p>Now, let’s start with the API that convinced me to request maintainership.</p>

<p>Do you know the difference between <code class="language-plaintext highlighter-rouge">JSON.load</code> and <code class="language-plaintext highlighter-rouge">JSON.parse</code>?</p>

<p>There’s more than one, but the main difference is that it has a different set of options enabled by default, and notably
one that is a massive footgun: <code class="language-plaintext highlighter-rouge">create_additions: true</code>.</p>

<p>This option is so bad that <a href="https://github.com/rubocop/rubocop/pull/3448">Rubocop’s default set of rules bans <code class="language-plaintext highlighter-rouge">JSON.load</code> outright for security reasons</a>,
and it has been involved in more than one <a href="https://discuss.rubyonrails.org/t/cve-2023-27531-possible-deserialization-of-untrusted-data-vulnerability-in-kredis-json/82467">security vulnerabilities</a>.</p>

<p>Let’s dig into what it does:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"json"</span>

<span class="k">class</span> <span class="nc">Point</span>
  <span class="k">class</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span>
    <span class="k">def</span> <span class="nf">json_create</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
      <span class="n">new</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">"x"</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s2">"y"</span><span class="p">])</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
    <span class="vi">@x</span> <span class="o">=</span> <span class="n">x</span>
    <span class="vi">@y</span> <span class="o">=</span> <span class="n">y</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="n">document</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">'JSON'</span><span class="sh">
  {
    "json_class": "Point",
    "x": 123.456,
    "y": 789.321
  }
</span><span class="no">JSON</span>

<span class="nb">p</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
<span class="c1"># =&gt; {"json_class" =&gt; "Point", "x" =&gt; 123.456, "y" =&gt; 789.321}</span>

<span class="nb">p</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
<span class="c1"># =&gt; #&lt;Point:0x00000001007f6d08 @x=123.456, @y=789.321&gt;</span>
</code></pre></div></div>

<p>So what the <code class="language-plaintext highlighter-rouge">create_additions: true</code> parsing option does is that when it notices an object with the special key <code class="language-plaintext highlighter-rouge">"json_class"</code>,
It resolves the constant and calls <code class="language-plaintext highlighter-rouge">#json_create</code> on it with the object.</p>

<p>By itself, this isn’t really a security vulnerability, as only classes with a <code class="language-plaintext highlighter-rouge">.json_create</code> method can be instantiated this way.
But if you’ve been using Ruby for a long time, this may remind you of similar issues with gems like <code class="language-plaintext highlighter-rouge">YAML</code> where similar capabilities
were exploited.</p>

<p>That’s the problem with these sorts of duck-typed APIs: they are way too global.</p>

<p>You can have a piece of code using <code class="language-plaintext highlighter-rouge">JSON.load</code> that is perfectly safe on its own, but then if it’s embedded in an application
that also loads some other piece of code that defines some <code class="language-plaintext highlighter-rouge">.json_create</code> methods you weren’t expecting, you may end up with
an unforeseen vulnerability.</p>

<p>But even if you don’t define any <code class="language-plaintext highlighter-rouge">json_create</code> methods, the gem will always define one on <code class="language-plaintext highlighter-rouge">String</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">require</span> <span class="s2">"json"</span>
<span class="o">&gt;&gt;</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="s1">'{"json_class": "String", "raw": [112, 119, 110, 101, 100]}'</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="s2">"pwned"</span>
</code></pre></div></div>

<p>Here again, you probably need to find some specific circumstances to exploit that, but you can probably see how this
trick can be used to bypass a validation check of some sort.</p>

<p>So what do I plan to do about it? Several things.</p>

<p>First, I deprecated the implicit <code class="language-plaintext highlighter-rouge">create_additions: true</code> option. If you use <code class="language-plaintext highlighter-rouge">JSON.load</code> for that feature, a deprecation
warning will be emitted, asking to use <code class="language-plaintext highlighter-rouge">JSON.unsafe_load</code> instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"json"</span>
<span class="no">Warning</span><span class="p">[</span><span class="ss">:deprecated</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>
<span class="no">JSON</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="s1">'{"json_class": "String", "raw": [112, 119, 110, 101, 100]}'</span><span class="p">)</span>
<span class="c1"># /tmp/j.rb:3: warning: JSON.load implicit support for `create_additions: true`</span>
<span class="c1"># is deprecated and will be removed in 3.0,</span>
<span class="c1"># use JSON.unsafe_load or explicitly pass `create_additions: true`</span>
</code></pre></div></div>

<p>That being said, considering how wonky this feature is, I’m also considering extracting it into another gem.</p>

<p>This used to be impossible, as it was baked deep into the both the C and the Java parsers,
but <a href="https://github.com/ruby/json/pull/774">I recently refactored it to be pure Ruby code using a callback exposed by the parsers</a>.</p>

<p>Now you can provide a <code class="language-plaintext highlighter-rouge">Proc</code> to <code class="language-plaintext highlighter-rouge">JSON.load</code>, the parser will invoke it for every parsed value, allowing you to substitute
a value by another:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cb</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span> <span class="k">do</span>
  <span class="k">case</span> <span class="n">obj</span>
  <span class="k">when</span> <span class="no">String</span>
    <span class="n">obj</span><span class="p">.</span><span class="nf">upcase</span>
  <span class="k">else</span>
    <span class="n">obj</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="nb">p</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="s1">'["a", {"b": 1}]'</span><span class="p">,</span> <span class="n">cb</span><span class="p">)</span>
<span class="c1"># =&gt; ["A", {"B" =&gt; 1}]</span>
</code></pre></div></div>

<p>Prior to that change, <code class="language-plaintext highlighter-rouge">JSON.load</code> already accepted a Proc, but its return value was ignored.</p>

<p>The nice thing is that this callback also now serves as a much safer and flexible way to handle the serialization of rich objects.
For instance, you could implement something like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">types</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s2">"range"</span> <span class="o">=&gt;</span> <span class="no">MyRangeType</span>
<span class="p">}</span>
<span class="n">cb</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span> <span class="k">do</span>
  <span class="k">case</span> <span class="n">obj</span>
  <span class="k">when</span> <span class="no">Hash</span>
    <span class="k">if</span> <span class="n">type</span> <span class="o">=</span> <span class="n">types</span><span class="p">[</span><span class="n">obj</span><span class="p">[</span><span class="s2">"__type"</span><span class="p">]]</span>
      <span class="n">type</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="n">obj</span>
    <span class="k">end</span>
  <span class="k">else</span>
    <span class="n">obj</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>While this requires more code from the user, it gives much tighter control over the deserialization,
but more importantly, it isn’t global anymore.
If a library uses this feature to deserialize trusted data, its callback is never going to be invoked by another library
like it’s the case with the old <code class="language-plaintext highlighter-rouge">Class#json_create</code> API.</p>

<p>The obvious solution would have been to follow the same route as <code class="language-plaintext highlighter-rouge">YAML</code>, with its <code class="language-plaintext highlighter-rouge">permitted_classes</code> argument, but
in my opinion, it wouldn’t have addressed the root of the problem, and it makes for a very unpleasant API to use.</p>

<p>Instead, I believe this Proc interface provides the same functionality as before, but in a way that is both more
flexible and safer.</p>

<p>I think this is a clear case for deprecation, given it is very rarely needed, has security implications, and surprises users.</p>

<h2 id="parsing-of-duplicate-keys">Parsing of Duplicate Keys</h2>

<p>Another behavior of the parser I recently deprecated is the treatment of duplicate keys.
Consider the following code:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">p</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="s1">'{"a": 1, "a": 2}'</span><span class="p">)[</span><span class="s2">"a"</span><span class="p">]</span>
</code></pre></div></div>

<p>What do you think it should return? You could argue that the first key or the last key should win, or that this should
result in a parse error.</p>

<p>Unfortunately, JSON is a bit of a “post-specified” format, as in it started as <a href="https://www.json.org/json-en.html">an extremely simple document</a>.
All it says about “objects” is:</p>

<blockquote>
  <p>An object is an unordered set of name/value pairs.
An object begins with <code class="language-plaintext highlighter-rouge">{</code> and ends with <code class="language-plaintext highlighter-rouge">}</code>.
Each name is followed by <code class="language-plaintext highlighter-rouge">:</code> and the name/value pairs are separated by <code class="language-plaintext highlighter-rouge">,</code>.</p>
</blockquote>

<p>That’s it, that’s the extent of the specification, as you can see, there is no mention of what a parser should do if it encounters a duplicate key.</p>

<p>Later on, various standardisation bodies tried to specify JSON based on the implementations out there.</p>

<p>Hence, we now have IETF’s STD 90, also known as <a href="https://datatracker.ietf.org/doc/html/rfc8259">RFC 8259</a>, which states:</p>

<blockquote>
  <p>Many implementations report the last name/value pair only.
Other implementations report an error or fail to parse the object,
and some implementations report all of the name/value pairs, including duplicates.</p>
</blockquote>

<p>In other words, it acknowledges most implementations return the last seen pair, but doesn’t prescribe any particular behavior.</p>

<p>There’s also the <a href="https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf">ECMA-404 standard</a></p>

<blockquote>
  <p>The JSON syntax does not impose any restrictions on the strings used as names,
does not require that name strings be unique, and does not assign any significance to the ordering of name/value pairs.
These are all semantic considerations that may be defined by
JSON processors or in specifications defining specific uses of JSON for data interchange.</p>
</blockquote>

<p>Which is pretty much the specification language equivalent of: 🤷‍♂️.</p>

<p>The problem with under-specified formats is that they can sometimes be exploited, the classic example being
<a href="https://en.wikipedia.org/wiki/HTTP_request_smuggling">HTTP request smuggling</a>.</p>

<p>And while it wasn’t an exploitation per se, <a href="https://hackerone.com/reports/3000510#activity-32819479">a security issue happened to Hacker One</a>,
in part because of that behavior.
Technically, the bug was on the JSON generation side, but if the JSON’s gem parser didn’t silently accept duplicated keys,
they would have caught it early in development.</p>

<p>That’s why starting from version <code class="language-plaintext highlighter-rouge">2.13.0</code>, <code class="language-plaintext highlighter-rouge">JSON.parse</code> <a href="https://github.com/ruby/json/pull/818">now accepts a new <code class="language-plaintext highlighter-rouge">allow_duplicate_key:</code> keyword argument</a>,
and if not explicitly allowed, a deprecation warning is emitted if a duplicate key is encountered:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"json"</span>
<span class="no">Warning</span><span class="p">[</span><span class="ss">:deprecated</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>

<span class="nb">p</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="s1">'{"a": 1, "a": 2}'</span><span class="p">)</span>
<span class="c1"># =&gt; {"a" =&gt; 2}</span>

<span class="c1"># /tmp/j.rb:4: warning: detected duplicate key "a" in JSON object.</span>
<span class="c1"># This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`</span>
<span class="c1">#at line 1 column 1</span>
</code></pre></div></div>

<p>As mentioned in the warning message, I plan to change the default behavior to be an error in the next major version, but of course
it will always be possible to explicitly allow for duplicate keys, for the rare cases where it’s needed.</p>

<p>Here again, I think this deprecation is justified because duplicated keys are rare, but also almost always a mistake,
hence I expect few people to need to change anything, and the ones who do will likely learn about a previously unnoticed
mistake in their application.</p>

<h2 id="the-to_json-and-to_s-methods">The to_json And to_s Methods</h2>

<p>Before you gasp in horror, don’t worry, I don’t plan on deprecating the <code class="language-plaintext highlighter-rouge">Object#to_json</code> method, ever.
It is way too widespread for this to ever be acceptable.</p>

<p>But that doesn’t mean this API is good, nor that nothing should be done about it.</p>

<p>At the center of the <code class="language-plaintext highlighter-rouge">json</code> gem API, there’s the notion that objects can define themselves how they should be
serialized into JSON by responding to the <code class="language-plaintext highlighter-rouge">to_json</code> method.</p>

<p>At first sight, it seems like a perfectly fine API, it’s an interface that objects can implement, fairly classic object-oriented design.</p>

<p>Here’s an example that changes how <code class="language-plaintext highlighter-rouge">Time</code> objects are serialized.</p>

<p>By default, <code class="language-plaintext highlighter-rouge">json</code> will call <code class="language-plaintext highlighter-rouge">#to_s</code> on objects it doesn’t know how to handle:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">generate</span><span class="p">({</span> <span class="ss">created_at: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span> <span class="p">})</span>
<span class="p">{</span><span class="s2">"created_at"</span><span class="ss">:"2025-08-02 13:03:32 +0200"</span><span class="p">}</span>
</code></pre></div></div>

<p>But we can instruct it to instead serialize <code class="language-plaintext highlighter-rouge">Time</code> using the ISO8601 / <a href="https://datatracker.ietf.org/doc/html/rfc3339">RFC 3339</a>
format:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Time</span>
  <span class="k">def</span> <span class="nf">to_json</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
    <span class="n">iso8601</span><span class="p">(</span><span class="mi">3</span><span class="p">).</span><span class="nf">to_json</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">generate</span><span class="p">({</span> <span class="ss">created_at: </span><span class="no">Time</span><span class="p">.</span><span class="nf">now</span> <span class="p">})</span>
<span class="p">{</span><span class="s2">"created_at"</span><span class="ss">:"2025-08-02T13:05:04.160+02:00"</span><span class="p">}</span>
</code></pre></div></div>

<p>This seems all well and good, but the problem, like for the <code class="language-plaintext highlighter-rouge">.json_create</code> method, is that this is a global behavior.
An application may very well need to serialize dates in different ways in different contexts.</p>

<p>Worse, in the context of a library, say an API client that needs to serialize <code class="language-plaintext highlighter-rouge">Time</code> in a specific way, it’s not really
possible to use this API, you can’t assume it’s acceptable to change such a global behavior, given you know nothing about the application in which you’ll run.</p>

<p>So to me, there are two problems here. First, using <code class="language-plaintext highlighter-rouge">#to_s</code> as a fallback works for a few types, like date, but it is really not helpful
for the overwhelming majority of other objects:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="no">Object</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
<span class="s2">"#&lt;Object:0x000000011ce214a0&gt;"</span>
</code></pre></div></div>

<p>I really can’t think of a situation in which this is the behavior that you want. If <code class="language-plaintext highlighter-rouge">JSON.generate</code> ends up calling <code class="language-plaintext highlighter-rouge">to_s</code> on an object, I’m willing to bet that in 99% of the time, the developer didn’t intend for that object to be serialized, or forgot to implement a <code class="language-plaintext highlighter-rouge">#to_json</code> on it.</p>

<p>Either way, it would be way more useful to raise an error, and requires that an explicit method to serialize that unknown object be provided.</p>

<p>The second is that it should be possible to customize a given type serialization locally, instead of globally.</p>

<p>In addition, returning a String as a JSON fragment is also not great, because it means recursively calling generators, and
allows to generate invalid documents:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Broken</span>
  <span class="k">def</span> <span class="nf">to_json</span>
    <span class="nb">to_s</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="o">&gt;&gt;</span> <span class="no">Broken</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">to_json</span>
<span class="o">=&gt;</span> <span class="s2">"#&lt;Broken:0x0000000123054050&gt;"</span>
<span class="o">&gt;&gt;</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="no">Broken</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">to_json</span><span class="p">)</span>
<span class="c1">#&gt; JSON::ParserError: unexpected character: '#&lt;Broken:0x000000011c9377a0&gt;'</span>
<span class="c1"># &gt; at line 1 column 1 </span>
</code></pre></div></div>

<p>That’s the problems the new <code class="language-plaintext highlighter-rouge">JSON::Coder</code> API is meant to solve.</p>

<p>By default, <code class="language-plaintext highlighter-rouge">JSON::Coder</code> only accepts to serialize types that have a direct JSON equivalent, so <code class="language-plaintext highlighter-rouge">Hash</code>, <code class="language-plaintext highlighter-rouge">Array</code>, <code class="language-plaintext highlighter-rouge">String</code> / <code class="language-plaintext highlighter-rouge">Symbol</code>,
<code class="language-plaintext highlighter-rouge">Integer</code>, <code class="language-plaintext highlighter-rouge">Float</code>, <code class="language-plaintext highlighter-rouge">true</code>, <code class="language-plaintext highlighter-rouge">false</code> and <code class="language-plaintext highlighter-rouge">nil</code>. Any type that doesn’t have a direct JSON equivalent produces an error:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">MY_JSON</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">::</span><span class="no">Coder</span><span class="p">.</span><span class="nf">new</span>
<span class="o">&gt;&gt;</span> <span class="no">MY_JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">({</span><span class="ss">a: </span><span class="mi">1</span><span class="p">})</span>
<span class="o">=&gt;</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">a</span><span class="se">\"</span><span class="s2">:1}"</span>
<span class="o">&gt;&gt;</span> <span class="no">MY_JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">({</span><span class="ss">a: </span><span class="no">Time</span><span class="p">.</span><span class="nf">new</span><span class="p">})</span>
<span class="c1">#&gt; JSON::GeneratorError: Time not allowed in JSON</span>
</code></pre></div></div>

<p>But it does allow you to provide a <code class="language-plaintext highlighter-rouge">Proc</code> to define the serialization of all other types:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">MY_JSON</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">::</span><span class="no">Coder</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">obj</span><span class="o">|</span>
  <span class="k">case</span> <span class="n">obj</span>
  <span class="k">when</span> <span class="no">Time</span>
    <span class="n">obj</span><span class="p">.</span><span class="nf">iso8601</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
  <span class="k">else</span>
    <span class="n">obj</span> <span class="c1"># return `obj` to fail serialization</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="o">&gt;&gt;</span> <span class="no">MY_JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">({</span><span class="ss">a: </span><span class="no">Time</span><span class="p">.</span><span class="nf">new</span><span class="p">})</span>
<span class="o">=&gt;</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">a</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">2025-08-02T14:03:15.091+02:00</span><span class="se">\"</span><span class="s2">}"</span>
</code></pre></div></div>

<p>Contrary to the <code class="language-plaintext highlighter-rouge">#to_json</code> method, here the Proc is expected to return a JSON primitive object, so you don’t have to
concern yourself with JSON escaping rules and such, which is much safer.</p>

<p>But if for some reason you do need to, you still can using <code class="language-plaintext highlighter-rouge">JSON::Fragment</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">MY_JSON</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">::</span><span class="no">Coder</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">obj</span><span class="o">|</span>
  <span class="k">case</span> <span class="n">obj</span>
  <span class="k">when</span> <span class="no">SomeRecord</span>
    <span class="no">JSON</span><span class="o">::</span><span class="no">Fragment</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">obj</span><span class="p">.</span><span class="nf">json_blob</span><span class="p">)</span>
  <span class="k">else</span>
    <span class="n">obj</span> <span class="c1"># return `obj` to fail serialization</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>With this new API, it’s now much easier for a gem to customize JSON generation in a local way.</p>

<p>Now, as I said before, I absolutely don’t plan to deprecate <code class="language-plaintext highlighter-rouge">#to_json</code>, nor even the behavior that calls <code class="language-plaintext highlighter-rouge">#to_s</code> on unknown objects.
Even though I think it’s a bad API, and that its replacement is way superior, the <code class="language-plaintext highlighter-rouge">#to_json</code> method has been at the center of the <code class="language-plaintext highlighter-rouge">json</code>
gem from the beginning and would require a massive amount of work from the community to migrate out of.</p>

<p>The decision to deprecate an API should always weigh the benefits against the costs.
Here, the cost is so massive that it is unimaginable for me to even consider it.</p>

<h2 id="load_default_options--dump_default_options">load_default_options / dump_default_options</h2>

<p>Another set of APIs I’ve marked as deprecated are the various <code class="language-plaintext highlighter-rouge">_default_options</code> accessors.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="s2">"http://example.com"</span><span class="p">)</span>
<span class="s2">"http://example.com"</span>
<span class="o">&gt;&gt;</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump_default_options</span><span class="p">[</span><span class="ss">:script_safe</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>
<span class="o">&gt;&gt;</span> <span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="s2">"http://example.com"</span><span class="p">)</span>
<span class="s2">"http:</span><span class="se">\/\/</span><span class="s2">example.com"</span>
</code></pre></div></div>

<p>The concept is simple: you can globally change the default options received by certain methods.</p>

<p>At first sight, this might seem like a convenience, it allows you to set some option without having to pass it around
at potentially dozens of different call sites.</p>

<p>But just like <code class="language-plaintext highlighter-rouge">#to_json</code> and other APIs, this change applies to the entire application, including some dependencies that may
not expect standard JSON methods to behave differently.</p>

<p>And that’s not a hypothetical, I personally ran into a gem that was using JSON to fingerprint some object graphs, e.g.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">fingerprint</span>
  <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span><span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">some_object_graph</span><span class="p">))</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That fingerprinting method was well tested in the gem, and was working well in a few dozen applications until one
day someone reported a bug in the gem. After some investigation, I figured the host application in question
had modified <code class="language-plaintext highlighter-rouge">JSON.dump_default_options</code>, causing the fingerprints to be different.</p>

<p>If you think about it, these sorts of global settings aren’t very different from monkey patching:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">JSON</span><span class="p">.</span><span class="nf">singleton_class</span><span class="p">.</span><span class="nf">prepend</span><span class="p">(</span><span class="no">Module</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span>
  <span class="k">def</span> <span class="nf">dump</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="nb">proc</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">,</span> <span class="n">opts</span> <span class="o">=</span> <span class="p">{})</span>
    <span class="n">opts</span> <span class="o">=</span> <span class="n">opts</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="ss">script_safe: </span><span class="kp">true</span><span class="p">)</span>
    <span class="k">super</span>
  <span class="k">end</span>
<span class="p">})</span>
</code></pre></div></div>

<p>The overwhelming majority of Rubyists are very aware of the potential pitfalls of monkey patching, and some absolutely loathe it,
yet, these sorts of global configuration APIs don’t get frowned upon as much for some reason.</p>

<p>In some cases, they make sense. e.g. if the configuration is for an application, or a framework (a framework essentially being an application skeleton),
there’s not really a need for local configuration, and a global one is simpler and easier to reason about.
But in a library, that may in turn be used by multiple other libraries with different configuration needs, they’re a problem.</p>

<p>Amusingly, <a href="https://bugs.ruby-lang.org/issues/21311#Avoiding-unexpected-globally-shared-modulesobjects">this sort of API was one of the justifications for the currently experimental namespace feature in Ruby 3.5.0dev</a>,
which shows the <code class="language-plaintext highlighter-rouge">json</code> gem is not the only one with this problem.</p>

<p>Here again, a better solution is the <code class="language-plaintext highlighter-rouge">JSON::Coder</code> API, if you want to centralize your JSON generation configuration across
your codebase, you can allocate a singleton with your desired options:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">MyLibrary</span>
  <span class="no">JSON_CODER</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">::</span><span class="no">Coder</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">script_safe: </span><span class="kp">true</span><span class="p">)</span>

  <span class="k">def</span> <span class="nf">do_things</span>
    <span class="no">JSON_CODER</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As a library author, you can even allow your users to substitute the configuration for one of their choosing:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">MyLibrary</span>
  <span class="k">class</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span>
    <span class="nb">attr_accessor</span> <span class="ss">:json_coder</span>
  <span class="k">end</span>
  <span class="vi">@json_coder</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">::</span><span class="no">Coder</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">script_safe: </span><span class="kp">true</span><span class="p">)</span>

  <span class="k">def</span> <span class="nf">do_things</span>
    <span class="no">MyLibrary</span><span class="p">.</span><span class="nf">json_coder</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Thankfully, from what I can see of the gem’s usage, these API were very rarely used, so while they’re not a major hindrance,
I figured the cost vs benefit is positive. And if someone really needs to set an option globally, they can monkey-patch JSON,
the effect is the same, and at least it’s more honest.</p>

<h2 id="conclusion">Conclusion</h2>

<p>As mentioned previously, the decision to deprecate shouldn’t be taken lightly.
It’s important to have empathy for the users who will have to deal with the fallout,
and there are a few things more annoying than cosmetic deprecations.</p>

<p>Yet it is also important to recognize when an API is error-prone or even outright dangerous,
and deprecations are sometimes a necessary evil to correct course.</p>

<p>Also, as you probably noticed, a common theme in most of the APIs I don’t like in the <code class="language-plaintext highlighter-rouge">json</code> gem, is global behavior and configuration.
I’m not certain why that is. A part of it might be that as Rubyists we value simplicity and conciseness, and that historically
the community has built its ethos as a reaction against overly verbose and ceremonial enterprise Java APIs, with their dependency injection frameworks and whatnot.</p>

<p>A bit of global state or behavior can sometimes bring a lot of simplicity, but it’s a very sharp tool that needs to be handled with extreme care.</p>]]></content><author><name></name></author><category term="ruby" /><category term="json" /><summary type="html"><![CDATA[As I mentioned at the start of my Optimizing Ruby’s JSON series of posts, performance isn’t why I candidated to be the new gem’s maintainer.]]></summary></entry><entry><title type="html">Unlocking Ractors: class instance variables</title><link href="https://byroot.github.io/ruby/performance/2025/05/24/unlocking-ractors-class-variables.html" rel="alternate" type="text/html" title="Unlocking Ractors: class instance variables" /><published>2025-05-24T09:03:51+00:00</published><updated>2025-05-24T09:03:51+00:00</updated><id>https://byroot.github.io/ruby/performance/2025/05/24/unlocking-ractors-class-variables</id><content type="html" xml:base="https://byroot.github.io/ruby/performance/2025/05/24/unlocking-ractors-class-variables.html"><![CDATA[<p>In <a href="/ruby/performance/2025/02/27/whats-the-deal-with-ractors.html">a previous post about ractors</a>, I explained why
I think it’s really unlikely you’d ever be able to run an entire application inside a ractor, but that they could
still be situationally very useful to move CPU-bound work out of the main thread, and to unlock some parallel algorithm.</p>

<p>But as I mentioned, this is unfortunately not yet viable because there are many known implementation bugs that can lead
to interpreter crashes, and that while they are supposed to execute in parallel, the Ruby VM still has one true global
lock that Ractors need to acquire to perform certain operations, making them often perform worse than the equivalent
single-threaded code.</p>

<p>One of these remaining contention points is class instance variables and class variables, and given it’s quite frequent
for code to check a class or module instance variable as some sort of configuration, this contention point can have a very
sizeable impact on Ractor performance, let me show you with a simple benchmark:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Mod</span>
  <span class="vi">@a</span> <span class="o">=</span> <span class="vi">@b</span> <span class="o">=</span> <span class="vi">@c</span> <span class="o">=</span> <span class="mi">1</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">compute</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
    <span class="n">count</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
      <span class="vi">@a</span> <span class="o">+</span> <span class="vi">@b</span> <span class="o">+</span> <span class="vi">@c</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">ITERATIONS</span> <span class="o">=</span> <span class="mi">1_000_000</span>
<span class="no">PARALLELISM</span> <span class="o">=</span> <span class="mi">8</span>

<span class="k">if</span> <span class="no">ARGV</span><span class="p">.</span><span class="nf">first</span> <span class="o">==</span> <span class="s2">"ractor"</span>
  <span class="n">ractors</span> <span class="o">=</span> <span class="no">PARALLELISM</span><span class="p">.</span><span class="nf">times</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span>
    <span class="no">Ractor</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
      <span class="no">Mod</span><span class="p">.</span><span class="nf">compute</span><span class="p">(</span><span class="no">ITERATIONS</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
  <span class="n">ractors</span><span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:take</span><span class="p">)</span>
<span class="k">else</span>
  <span class="no">Mod</span><span class="p">.</span><span class="nf">compute</span><span class="p">(</span><span class="no">ITERATIONS</span> <span class="o">*</span> <span class="no">PARALLELISM</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This simplistic micro-benchmark just add three module instance variables together repeatedly.
In one mode it does it serialy in the main thread, and if the <code class="language-plaintext highlighter-rouge">ractor</code> argument is passed, it does as many loop, but with 8
parallel ractors.
Hence in a perfect world, using the Ractors branch should be close to 8 times faster.</p>

<p>However, if you run this benchmark on Ruby’s master branch, this isn’t the result you’ll get:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>hyperfine <span class="nt">-w</span> 1 <span class="s1">'./miniruby --yjit ../test.rb'</span> <span class="s1">'./miniruby --yjit ../test.rb ractor'</span>
Benchmark 1: ./miniruby <span class="nt">--yjit</span> <span class="nt">--disable-all</span> ../test.rb
  Time <span class="o">(</span>mean ± σ<span class="o">)</span>:     252.4 ms ±   1.2 ms    <span class="o">[</span>User: 250.2 ms, System: 1.6 ms]
  Range <span class="o">(</span>min … max<span class="o">)</span>:   249.9 ms … 253.8 ms    11 runs

Benchmark 2: ./miniruby <span class="nt">--yjit</span> <span class="nt">--disable-all</span> ../test.rb ractor
  Time <span class="o">(</span>mean ± σ<span class="o">)</span>:      2.005 s ±  0.013 s    <span class="o">[</span>User: 2.098 s, System: 6.963 s]
  Range <span class="o">(</span>min … max<span class="o">)</span>:    1.992 s …  2.027 s    10 runs

Summary
  ./miniruby <span class="nt">--yjit</span> ../test.rb ran
    7.94 ± 0.06 <span class="nb">times </span>faster than ./miniruby <span class="nt">--yjit</span> ../test.rb ractor
</code></pre></div></div>

<p>That’s right, instead of being 8 times faster, the branch that uses Ractors ended up being 8 times slower.
This is because to read a module or class instance variables, secondary ractors have to acquire the VM lock,
which is a costly operation in itself, and worse, they end up waiting a lot to obtain the lock.</p>

<p>So what can we do about it?</p>

<h2 id="language-semantic">Language Semantic</h2>

<p>Before we delves into how this lock could be removed or reduced, let’s review how class instance variables behave with ractors.</p>

<p>Given that classes are global, their instance variables are too, hence they are essentially global.
Because of this, Ractors can’t let you do everything with them, otherwise, it would be a way to work around Ractors isolation.</p>

<p>The first rule is that only the main Ractor is allowed to set class instance variables:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Test</span>
  <span class="k">class</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span>
    <span class="nb">attr_accessor</span> <span class="ss">:var</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">Test</span><span class="p">.</span><span class="nf">var</span> <span class="o">=</span> <span class="mi">1</span> <span class="c1"># works</span>

<span class="no">Ractor</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
  <span class="c1"># works</span>
  <span class="nb">p</span> <span class="no">Test</span><span class="p">.</span><span class="nf">var</span>

  <span class="c1"># raises Ractor::IsolationError: can not set instance variables</span>
  <span class="c1"># of classes/modules by non-main Ractors</span>
  <span class="no">Test</span><span class="p">.</span><span class="nf">var</span> <span class="o">=</span> <span class="mi">2</span>
<span class="k">end</span><span class="p">.</span><span class="nf">take</span>
</code></pre></div></div>

<p>So secondary ractors can read instance variables on classes and modules, but can’t write them.</p>

<p>The second rule is that they can only read instance variables on classes if the object stored in that variable is shareable:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Test</span>
  <span class="k">class</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span>
    <span class="nb">attr_accessor</span> <span class="ss">:var1</span><span class="p">,</span> <span class="ss">:var2</span>
  <span class="k">end</span>

  <span class="vi">@var1</span> <span class="o">=</span> <span class="p">{}.</span><span class="nf">freeze</span>
  <span class="vi">@var2</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">end</span>

<span class="no">Ractor</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
  <span class="c1"># works:</span>
  <span class="nb">p</span> <span class="no">Test</span><span class="p">.</span><span class="nf">var1</span>

  <span class="c1"># raises Ractor::IsolationError: can not get unshareable values from</span>
  <span class="c1"># instance variables of classes/modules from non-main Ractors</span>
  <span class="nb">p</span> <span class="no">Test</span><span class="p">.</span><span class="nf">var2</span>
<span class="k">end</span><span class="p">.</span><span class="nf">take</span>
</code></pre></div></div>

<h2 id="reducing-contention">Reducing Contention</h2>

<p>Usually when dealing with lock contention issues, the first solution is to turn one big lock into multiple finer-grained locks.
In our simplistic benchmark, all ractors are accessing variables on the same module, so that wouldn’t help, but we could
assume that in more realistic scenarios, they’d access the variables of many different modules and, hence wouldn’t fight as much
for the same one.</p>

<p>But the way I envision Ractors being used in real-world cases, at least initially, is for running small pieces of
code in parallel, with an API approaching futures:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">futures</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">futures</span> <span class="o">&lt;&lt;</span> <span class="no">Ractor</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="n">fetch_and_compute_prices</span> <span class="p">}</span>
<span class="n">futures</span> <span class="o">&lt;&lt;</span> <span class="no">Ractor</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="n">fetch_and_compute_order_history</span> <span class="p">}</span>
<span class="o">...</span>
<span class="n">futures</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:take</span><span class="p">)</span>
</code></pre></div></div>

<p>As such I actually expect Ractors to commonly access the same module or class variables over and over, so introducing more finely grained locks isn’t very enticing.</p>

<p>Another possibility would be to use a <a href="https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock">read-write lock</a>,
given only the main ractor can “write” variables, all secondary ractors could acquire the read lock concurrently.
But from previous experience, while read-write locks do allow concurrent read threads not to stall, they’re still quite
costly when contented because all threads have to atomically increment and decrement the same value and that isn’t good
for the CPU cache.
It’s a fine solution when the operation you are protecting is a relatively slow one, but in our case, reading an instance
variable is extremely cheap, so any kind of lock, even an uncontended one, will be disproportionally costly and ruin performance.</p>

<p>That’s why the only reasonable solution is to find a way to not use a lock at all.</p>

<h2 id="how-do-instance-variables-work">How do Instance Variables Work</h2>

<p>To understand how we could make instance variables lock-free, we must first understand how they work.
As is now tradition, I’ll try to explain it using Ruby pseudo code, starting with instance variable reads:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Module</span>
  <span class="k">def</span> <span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
    <span class="k">if</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">main_ractor?</span>
      <span class="c1"># The main ractor is the only one allowed to write instance variables</span>
      <span class="c1"># hence it doesn't need to lock because we know no one else could be</span>
      <span class="c1"># concurrently modifying `@shape` or `@fields`</span>
      <span class="k">if</span> <span class="n">field_index</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">field_index_for</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
        <span class="vi">@fields</span><span class="p">[</span><span class="n">field_index</span><span class="p">]</span>
      <span class="k">end</span>
    <span class="k">else</span>
      <span class="c1"># Secondary ractors must lock the VM even for reads because the main Ractor</span>
      <span class="c1"># could be modifying `@shape` or `@fields` concurrently.</span>
      <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
        <span class="k">if</span> <span class="n">field_index</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">field_index_for</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
          <span class="n">value</span> <span class="o">=</span> <span class="vi">@fields</span><span class="p">[</span><span class="n">field_index</span><span class="p">]</span>
          <span class="k">raise</span> <span class="no">Ractor</span><span class="o">::</span><span class="no">IsolationError</span> <span class="k">unless</span> <span class="no">Ractor</span><span class="p">.</span><span class="nf">shareable?</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
          <span class="n">value</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I’m not going to explain how shapes work here, as I already explained it in multiple previous posts.
The only thing you really need to know is that instance variables are stored in a continuous array, and shapes
keep track of the offset at which each variable is stored. They also are immutable, so you can query them concurrently.</p>

<p>As a result, reading an instance variable only amount of querying the shape tree to figure out if that particular variable exists,
and if it does, what its index is. After that, we read the variable at the specified offset in the <code class="language-plaintext highlighter-rouge">@fields</code> array of the
object.</p>

<p>However, on secondary Ractors, we additionally need to lock the VM to ensure the shape and the fields are consistent,
but that will be clearer once I explain how writing instance variables works.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Module</span>
  <span class="k">def</span> <span class="nf">instance_variable_set</span><span class="p">(</span><span class="n">variable_name</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
    <span class="k">raise</span> <span class="no">FrozenError</span> <span class="k">if</span> <span class="nb">frozen?</span>
    <span class="c1"># The main ractor is the only one allowed to write instance variables</span>
    <span class="k">raise</span> <span class="no">Ractor</span><span class="o">::</span><span class="no">IsolationError</span> <span class="k">unless</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">main_ractor?</span>

    <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
      <span class="k">if</span> <span class="n">field_index</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">field_index_for</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
        <span class="c1"># The variable already exists, we replace its value</span>
        <span class="vi">@fields</span><span class="p">[</span><span class="n">field_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
      <span class="k">else</span>
        <span class="c1"># The variable doesn't exist, we have to make a shape transition</span>
        <span class="n">next_shape</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">add_instance_variable</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">next_shape</span><span class="p">.</span><span class="nf">capacity</span> <span class="o">&gt;</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">capacity</span>
          <span class="c1"># @fields is full, we need to allocate a larger one</span>
          <span class="n">new_fields</span> <span class="o">=</span> <span class="no">Memory</span><span class="p">.</span><span class="nf">allocate</span><span class="p">(</span><span class="ss">size: </span><span class="n">next_shape</span><span class="p">.</span><span class="nf">capacity</span><span class="p">)</span>
          <span class="n">new_fields</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="vi">@fields</span><span class="p">)</span> <span class="c1"># copy content</span>
          <span class="vi">@fields</span><span class="p">,</span> <span class="n">old_fields</span> <span class="o">=</span> <span class="n">new_fields</span><span class="p">,</span> <span class="vi">@fields</span>

          <span class="c1"># The fields array is manually managed memory, so it needs to be freed explicitly</span>
          <span class="no">Memory</span><span class="p">.</span><span class="nf">free</span><span class="p">(</span><span class="n">old_fields</span><span class="p">)</span>
        <span class="k">end</span>

        <span class="vi">@fields</span><span class="p">[</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">field_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
        <span class="vi">@shape</span> <span class="o">=</span> <span class="n">next_shape</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As you can see, the fields array has a given size, if we’re adding a new instance variable, we may need to allocate
a larger one and swap the two, as well as change the object’s shape.</p>

<p>That is why we need to lock the VM, we can’t let another ractor read an instance variable while we’re doing this because
it would run into all sorts of race conditions:</p>

<ul>
  <li>It could be reading inside <code class="language-plaintext highlighter-rouge">old_fields</code> while we’re freeing it, causing a use-after-free bug.</li>
  <li>It could be reading inside <code class="language-plaintext highlighter-rouge">old_fields</code> using the new shape, causing an out-of-bounds read.</li>
  <li>It could be reading inside <code class="language-plaintext highlighter-rouge">new_fields</code> using the new shape, but before we’ve written the new value, causing an uninitialized memory read.</li>
</ul>

<p>Now, if you are not familiar with C, or another low-level programming language, you might be thinking that I’m exaggerating.
After all, updating the shape is the last operation, so surely cases 2 and 3 aren’t possible.</p>

<p>Well, I got some bad news…</p>

<h2 id="memory-model">Memory Model</h2>

<p>Multithreaded programming is tricky, but even more so when allowing multiple threads to read and write the same memory,
because processors have all sorts of caches, hence a variable doesn’t only reside in one place in your RAM.</p>

<p>It can also be copied in the CPU L1/L2/etc caches, or even in the CPU registers.
When one thread writes into a variable, it’s not immediately visible to all other threads, the write will take a while
to propagate back to the RAM.
Worse, if you write into multiple variables in a specific order, it’s not even guaranteed other threads will witness these changes
in the same order.</p>

<p>Let’s consider a simple multi-threaded program:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Point</span> <span class="o">=</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:x</span><span class="p">,</span> <span class="ss">:y</span><span class="p">)</span>

<span class="n">treasure</span> <span class="o">=</span> <span class="kp">nil</span>

<span class="n">thread</span> <span class="o">=</span> <span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
  <span class="k">while</span> <span class="kp">true</span>
    <span class="k">if</span> <span class="n">treasure</span>
      <span class="nb">puts</span> <span class="s2">"Treasure is at </span><span class="si">#{</span><span class="n">treasure</span><span class="p">.</span><span class="nf">x</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2"> / </span><span class="si">#{</span><span class="n">treasure</span><span class="p">.</span><span class="nf">y</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
      <span class="k">break</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="n">point</span> <span class="o">=</span> <span class="no">Point</span><span class="p">.</span><span class="nf">new</span>
<span class="n">point</span><span class="p">.</span><span class="nf">x</span> <span class="o">=</span> <span class="mi">12</span>
<span class="n">point</span><span class="p">.</span><span class="nf">y</span> <span class="o">=</span> <span class="mi">24</span>
<span class="n">treasure</span> <span class="o">=</span> <span class="n">point</span>

<span class="n">thread</span><span class="p">.</span><span class="nf">join</span>
</code></pre></div></div>

<p>As a Ruby programmer, you likely expect this program to print <code class="language-plaintext highlighter-rouge">Treasure is at 12 / 24</code>, and you’d be correct.
After all, we fully initialize the <code class="language-plaintext highlighter-rouge">Point</code> instance before updating the <code class="language-plaintext highlighter-rouge">treasure</code> global variable to point to it.</p>

<p>But if we were to write a similar program in C, the output could be any of:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Treasure is at 12 / 24</code></li>
  <li><code class="language-plaintext highlighter-rouge">Treasure is at nil / 24</code></li>
  <li><code class="language-plaintext highlighter-rouge">Treasure is at 12 / nil</code></li>
  <li><code class="language-plaintext highlighter-rouge">Treasure is at nil / nil</code></li>
</ul>

<p>Why? Well, this has to do with <a href="https://en.wikipedia.org/wiki/Memory_model_(programming)">memory models</a>.
In order to optimize your code, compilers sometimes may have to change the order of memory reads and writes.
So for programmers to be able to write correct programs, they need to know what the compiler can and cannot do, and that’s
what a language memory model defines. In the case of C, the memory model is very lax, and compilers are allowed to reorder
reads and writes very extensively.</p>

<p>And it’s not only about the compilers. CPUs too can reorder read and write operations.
The <code class="language-plaintext highlighter-rouge">x86</code> (AKA Intel) memory model is quite strict, so it doesn’t reorder much, but the <code class="language-plaintext highlighter-rouge">arm64</code> memory model is much more lax,
so even if your compiler generated the native code in the same order, your CPU could execute them out of order,
giving you unpredictable results.</p>

<p>To work around this problem, C compilers and CPUs provide <a href="https://en.wikipedia.org/wiki/Barrier_(computer_science)">“barriers”</a>.
You can insert them in your code to enforce that reads and write can’t be reordered across such barriers, allowing
you to ensure that all threads will observe memory in a consistent way.</p>

<h2 id="atomic-write">Atomic Write</h2>

<p>From a programmer’s perspective, it’s generally exposed as “atomic” read and write operations, and it’s understood by the
compiler and CPU that memory operations cannot be reordered across atomic operations.</p>

<p>So going back to our <code class="language-plaintext highlighter-rouge">instance_variable_set</code> implementation, we can fix two of the three race conditions by using an atomic
write:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Module</span>
  <span class="k">def</span> <span class="nf">instance_variable_set</span><span class="p">(</span><span class="n">variable_name</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
    <span class="k">raise</span> <span class="no">FrozenError</span> <span class="k">if</span> <span class="nb">frozen?</span>
    <span class="c1"># The main ractor is the only one allowed to write instance variables</span>
    <span class="k">raise</span> <span class="no">Ractor</span><span class="o">::</span><span class="no">IsolationError</span> <span class="k">unless</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">main_ractor?</span>

    <span class="k">if</span> <span class="n">field_index</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">field_index_for</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
      <span class="c1"># The variable already exists, we replace its value</span>
      <span class="vi">@fields</span><span class="p">[</span><span class="n">field_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
    <span class="k">else</span>
      <span class="c1"># The variable doesn't exist, we have to make a shape transition</span>
      <span class="n">next_shape</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">add_instance_variable</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>

      <span class="k">if</span> <span class="n">next_shape</span><span class="p">.</span><span class="nf">capacity</span> <span class="o">&gt;</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">capacity</span>
        <span class="c1"># @fields is full, we need to allocate a larger one</span>
        <span class="n">new_fields</span> <span class="o">=</span> <span class="no">Memory</span><span class="p">.</span><span class="nf">allocate</span><span class="p">(</span><span class="ss">size: </span><span class="n">next_shape</span><span class="p">.</span><span class="nf">capacity</span><span class="p">)</span>
        <span class="n">new_fields</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="vi">@fields</span><span class="p">)</span> <span class="c1"># copy content</span>
        <span class="n">old_fields</span> <span class="o">=</span> <span class="vi">@fields</span>
        <span class="c1"># Ensure `@fields` isn't updated before its content has been filled</span>
        <span class="no">Atomic</span><span class="p">.</span><span class="nf">write</span> <span class="p">{</span> <span class="vi">@fields</span> <span class="o">=</span> <span class="n">new_fields</span> <span class="p">}</span>

        <span class="c1"># The fields array is manually managed memory, so it needs to be freed explicitly</span>
        <span class="no">Memory</span><span class="p">.</span><span class="nf">free</span><span class="p">(</span><span class="n">old_fields</span><span class="p">)</span>
      <span class="k">end</span>

      <span class="vi">@fields</span><span class="p">[</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">field_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
      <span class="vi">@shape</span> <span class="o">=</span> <span class="n">next_shape</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>With this simple change, we now guarantee that the new <code class="language-plaintext highlighter-rouge">@fields</code> will be visible to other threads before the new <code class="language-plaintext highlighter-rouge">@shape</code> is.</p>

<p>They may still see the old <code class="language-plaintext highlighter-rouge">@shape</code> with the new <code class="language-plaintext highlighter-rouge">@fields</code>, but that’s acceptable because all the offsets <code class="language-plaintext highlighter-rouge">@shape</code> may point to
contain the same values. Pretty neat. Now we only need to find a solution for the use-after-free problem.</p>

<h2 id="our-friend-the-garbage-collector">Our Friend The Garbage Collector</h2>

<p>So our problem is that after we swap the old <code class="language-plaintext highlighter-rouge">@fields</code> array for the new one, we must free the old array to not leak memory.
But if there is no synchronization, we can’t guarantee that another thread doesn’t have a reference to the old array in its
registers or caches, so it may try to read from it after it was freed, and that might lead to a segmentation fault.</p>

<p>Hence, we must wait until there’s no longer any reference to the old array before freeing it, and if you think about it
that’s exactly what a garbage collector does, and lucky for us, Ruby already has one.</p>

<p>So the solution to avoid use-after-free is to use an actual Ruby <code class="language-plaintext highlighter-rouge">Array</code> instead of manually allocated memory,
this way we no longer have to free it explicitly, the garbage collected will take care of it later:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Module</span>
  <span class="k">def</span> <span class="nf">instance_variable_set</span><span class="p">(</span><span class="n">variable_name</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
    <span class="k">raise</span> <span class="no">FrozenError</span> <span class="k">if</span> <span class="nb">frozen?</span>
    <span class="c1"># The main ractor is the only one allowed to write instance variables</span>
    <span class="k">raise</span> <span class="no">Ractor</span><span class="o">::</span><span class="no">IsolationError</span> <span class="k">unless</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">main_ractor?</span>

    <span class="k">if</span> <span class="n">field_index</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">field_index_for</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
      <span class="c1"># The variable already exists, we replace its value</span>
      <span class="vi">@fields</span><span class="p">[</span><span class="n">field_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
    <span class="k">else</span>
      <span class="c1"># The variable doesn't exist, we have to make a shape transition</span>
      <span class="n">next_shape</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">add_instance_variable</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>

      <span class="k">if</span> <span class="n">next_shape</span><span class="p">.</span><span class="nf">capacity</span> <span class="o">&gt;</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">capacity</span>
        <span class="c1"># @fields is full, we need to allocate a larger one</span>
        <span class="n">new_fields</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">capacity</span><span class="p">)</span>
        <span class="n">new_fields</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="vi">@fields</span><span class="p">)</span> <span class="c1"># copy content</span>
        <span class="n">old_fields</span> <span class="o">=</span> <span class="vi">@fields</span>
        <span class="c1"># Ensure `@fields` isn't updated before its content has been filled</span>
        <span class="no">Atomic</span><span class="p">.</span><span class="nf">write</span> <span class="p">{</span> <span class="vi">@fields</span> <span class="o">=</span> <span class="n">new_fields</span> <span class="p">}</span>
      <span class="k">end</span>

      <span class="vi">@fields</span><span class="p">[</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">field_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
      <span class="vi">@shape</span> <span class="o">=</span> <span class="n">next_shape</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, if another thread is currently reading inside the old <code class="language-plaintext highlighter-rouge">@fields</code>, it doesn’t matter because it will remain valid
memory until the garbage collector notices it’s no longer referenced by anyone.</p>

<p>And just like that, we now have fully lock-free class instance variable reads and writes!</p>

<p>Well… no. Because we overlooked two complications.</p>

<h2 id="removing-instance-variables">Removing Instance Variables</h2>

<p>Perhaps you don’t know about it, because it’s quite a rare thing to do, but in Ruby, you can remove an object’s instance variables:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Test</span>
  <span class="nb">p</span> <span class="n">instance_variable_defined?</span><span class="p">(</span><span class="ss">:@foo</span><span class="p">)</span> <span class="c1"># =&gt; false</span>
  <span class="vi">@foo</span> <span class="o">=</span> <span class="mi">1</span>
  <span class="nb">p</span> <span class="n">instance_variable_defined?</span><span class="p">(</span><span class="ss">:@foo</span><span class="p">)</span> <span class="c1"># =&gt; true</span>

  <span class="n">remove_instance_variable</span><span class="p">(</span><span class="ss">:@foo</span><span class="p">)</span>
  <span class="nb">p</span> <span class="n">instance_variable_defined?</span><span class="p">(</span><span class="ss">:@foo</span><span class="p">)</span> <span class="c1"># =&gt; false</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And while this is an extremely rare operation, it can happen, hence we must handle it in a thread safe way.</p>

<p>Let’s look at its pseudo-implementation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Module</span>
  <span class="k">def</span> <span class="nf">remove_instance_variable</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
    <span class="n">removed_index</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">field_index_for</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>

    <span class="c1"># The variable didn't exist in the first place</span>
    <span class="k">return</span> <span class="k">unless</span> <span class="n">removed_index</span>

    <span class="n">next_shape</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">remove_instance_variable</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>

    <span class="c1"># Shift fields left</span>
    <span class="n">removed_index</span><span class="p">.</span><span class="nf">upto</span><span class="p">(</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">fields_count</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">index</span><span class="o">|</span>
      <span class="vi">@fields</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">=</span> <span class="vi">@fields</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span>
    <span class="k">end</span>

    <span class="vi">@shape</span> <span class="o">=</span> <span class="n">next_shape</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So when removing an instance variable, we get a new shape that is shorter than the previous one, which means that
all the variables indexed after the one we removed are now lower, so we need to shift all the fields.</p>

<p>To better illustrate, consider the following code:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="vi">@a</span> <span class="o">=</span> <span class="mi">1</span>
<span class="vi">@b</span> <span class="o">=</span> <span class="mi">2</span>
<span class="vi">@c</span> <span class="o">=</span> <span class="mi">3</span>
<span class="n">remove_instance_variable</span><span class="p">(</span><span class="ss">:@b</span><span class="p">)</span>
</code></pre></div></div>

<p>In the snippet above, <code class="language-plaintext highlighter-rouge">@fields</code> will change from <code class="language-plaintext highlighter-rouge">[1, 2, 3]</code> to <code class="language-plaintext highlighter-rouge">[1, 3]</code>, and that’s not really possible to do this in a thread-safe way.</p>

<p>We could, of course, do this shifting in a copy of <code class="language-plaintext highlighter-rouge">@fields</code>, and then swap <code class="language-plaintext highlighter-rouge">@fields</code> atomically, but one major problem would remain: the old shape and the new
shape are fundamentally incompatible.</p>

<p>If you are accessing <code class="language-plaintext highlighter-rouge">@c</code> using the old fields with the new shape, you will get <code class="language-plaintext highlighter-rouge">2</code> which is incorrect.</p>

<p>If you are accessing <code class="language-plaintext highlighter-rouge">@c</code> using new fields with the old shape, you will get whatever is outside the array, or perhaps a segmentation fault.</p>

<p>So in this case, we can’t rely on clever ordering of writes to keep a consistent view of the instance variables for all ractors.</p>

<p>For the anecdote, this isn’t how the initial implementation of object shapes in Ruby worked.</p>

<p>Early in Ruby 3.2 development, <code class="language-plaintext highlighter-rouge">#remove_instance_variable</code> wouldn’t produce a shorter shape, but instead
a child shape of type <code class="language-plaintext highlighter-rouge">UNDEF</code> that would record that the variable at offset <code class="language-plaintext highlighter-rouge">1</code> needs to be considered not defined.</p>

<p>However it was found that this could cause an infinite amount of shapes to be created by misbehaving code:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">obj</span> <span class="o">=</span> <span class="no">Object</span><span class="p">.</span><span class="nf">new</span>
<span class="kp">loop</span> <span class="k">do</span>
  <span class="n">obj</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="ss">:@foo</span><span class="p">)</span>
  <span class="n">obj</span><span class="p">.</span><span class="nf">remove_instance_variable</span><span class="p">(</span><span class="ss">:@foo</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So instead <a href="https://github.com/ruby/ruby/pull/6866">the implementation was changed to rebuild the shape tree</a>.</p>

<p>That previous implementation would have been useful in this case, as it would have prevented this race condition.
But ultimately it doesn’t matter, because there is another complication I didn’t mention.</p>

<h2 id="complex-shape">Complex Shape</h2>

<p>The other major complication I deliberately overlooked in my explanation thus far, is the existence of complex shapes.</p>

<p>Since shapes are append-only, Ruby code that defines instance variables in random order or often removes instance variables
can potentially generate an infinite combination of shapes, and each shape uses some amount of memory.</p>

<p>That’s why Ruby keeps track of how many shape variations a given class causes, and after a specific threshold (currently 8),
Ruby gives up and marks the class as “too complex”.</p>

<p>If you run this script on a recent Ruby, you will see a performance warning:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Warning</span><span class="p">[</span><span class="ss">:performance</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>

<span class="k">class</span> <span class="nc">TooComplex</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="mi">10</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span>
      <span class="nb">instance_variable_set</span><span class="p">(</span><span class="s2">"@iv_</span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span>
      <span class="n">remove_instance_variable</span><span class="p">(</span><span class="s2">"@iv_</span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">TooComplex</span><span class="p">.</span><span class="nf">new</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/complex.rb:6: warning: The class TooComplex reached 8 shape variations,
instance variables accesses will be slower and memory usage increased.
It is recommended to define instance variables in a consistent order,
for instance by eagerly defining them all in the #initialize method.
</code></pre></div></div>

<p>When this happens, any operation on an instance of that class that would result in a new shape being created instead results
in some sort of “singleton” shape, known as the complex shape, and in that case instance variables are stored in a Hash
instead of being stored in an array. It’s slower and uses more memory, but limits the creation of new shapes.</p>

<p>So the real <code class="language-plaintext highlighter-rouge">#instance_variable_get</code> and <code class="language-plaintext highlighter-rouge">#instance_variable_set</code> implementations are more complicated than what I described at the start of the post.
In reality, they look more like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Module</span>
  <span class="k">def</span> <span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
    <span class="k">if</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">too_complex?</span>
      <span class="vi">@fields</span><span class="p">[</span><span class="n">variable_name</span><span class="p">]</span> <span class="c1"># @fields is is Hash</span>
    <span class="k">elsif</span> <span class="n">field_index</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">field_index_for</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
      <span class="vi">@fields</span><span class="p">[</span><span class="n">field_index</span><span class="p">]</span> <span class="c1"># @fields is an Array</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">instance_variable_set</span><span class="p">(</span><span class="n">variable_name</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
    <span class="k">raise</span> <span class="no">FrozenError</span> <span class="k">if</span> <span class="nb">frozen?</span>
    <span class="c1"># The main ractor is the only one allowed to write instance variables</span>
    <span class="k">raise</span> <span class="no">Ractor</span><span class="o">::</span><span class="no">IsolationError</span> <span class="k">unless</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">main_ractor?</span>

    <span class="k">if</span> <span class="n">shape</span><span class="p">.</span><span class="nf">too_complex?</span>
      <span class="k">return</span> <span class="vi">@field_index</span><span class="p">[</span><span class="n">variable_name</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
    <span class="k">end</span>

    <span class="k">if</span> <span class="n">field_index</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">field_index_for</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
      <span class="c1"># The variable already exists, we replace its value</span>
      <span class="vi">@fields</span><span class="p">[</span><span class="n">field_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
    <span class="k">else</span>
      <span class="c1"># The variable doesn't exist, we have to make a shape transition</span>
      <span class="n">next_shape</span> <span class="o">=</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">add_instance_variable</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>

      <span class="k">if</span> <span class="n">next_shape</span><span class="p">.</span><span class="nf">too_complex?</span>
        <span class="n">new_fields</span> <span class="o">=</span> <span class="p">{}</span>
        <span class="vi">@shape</span><span class="p">.</span><span class="nf">each_ancestor</span> <span class="k">do</span> <span class="o">|</span><span class="n">shape</span><span class="o">|</span>
          <span class="n">new_fields</span><span class="p">[</span><span class="n">shape</span><span class="p">.</span><span class="nf">variable_name</span><span class="p">]</span> <span class="o">=</span> <span class="vi">@fields</span><span class="p">[</span><span class="n">shape</span><span class="p">.</span><span class="nf">field_index</span><span class="p">]</span>
        <span class="k">end</span>

        <span class="vi">@fields</span> <span class="o">=</span> <span class="n">new_fields</span>
        <span class="vi">@shape</span> <span class="o">=</span> <span class="n">next_shape</span>

        <span class="k">return</span> <span class="vi">@fields</span><span class="p">[</span><span class="n">variable_name</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
      <span class="k">end</span>

      <span class="k">if</span> <span class="n">next_shape</span><span class="p">.</span><span class="nf">capacity</span> <span class="o">&gt;</span> <span class="vi">@shape</span><span class="p">.</span><span class="nf">capacity</span>
        <span class="c1"># @fields is full, we need to allocate a larger one</span>
        <span class="n">new_fields</span> <span class="o">=</span> <span class="no">Array</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">capacity</span><span class="p">)</span>
        <span class="n">new_fields</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="vi">@fields</span><span class="p">)</span> <span class="c1"># copy content</span>
        <span class="n">old_fields</span> <span class="o">=</span> <span class="vi">@fields</span>
        <span class="c1"># Ensure `@fields` isn't updated before its content has been filled</span>
        <span class="no">Atomic</span><span class="p">.</span><span class="nf">write</span> <span class="p">{</span> <span class="vi">@fields</span> <span class="o">=</span> <span class="n">new_fields</span> <span class="p">}</span>
      <span class="k">end</span>

      <span class="vi">@fields</span><span class="p">[</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">field_index</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
      <span class="vi">@shape</span> <span class="o">=</span> <span class="n">next_shape</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And this code is now riddled with race conditions because regular and complex shapes are radically different,
even in the happy path case where we’re adding a new instance variable, we might turn <code class="language-plaintext highlighter-rouge">@fields</code> from an array into
a <code class="language-plaintext highlighter-rouge">Hash</code>.
So if <code class="language-plaintext highlighter-rouge">@shape</code> and <code class="language-plaintext highlighter-rouge">@fields</code> aren’t perfectly synchronized together, we might end up trying to access a Hash
like an Array, and vice-versa, which will likely end up in a VM crash.</p>

<h2 id="128bit-atomics">128bit Atomics</h2>

<p>One solution could have been to ensure <code class="language-plaintext highlighter-rouge">@shape</code> and <code class="language-plaintext highlighter-rouge">@fields</code> are written atomically together, but unfortunately in this case
it isn’t really possible.</p>

<p>First, because it would require to write two pointer-sized (64bit) values in a single atomic operation, which is possible
on some modern CPUs using SIMD instruction, but Ruby supports many different platforms, and there is no way all of them
would have support for it.</p>

<p>And second, because the constraint with this is that both fields need to be contiguous.
You can’t atomically write two pointer-sized values that are distant from each other.
Semantically you are treating two contiguous 64bit values are a single 128bit one, and for reasons I won’t get into here,
<code class="language-plaintext highlighter-rouge">@shape</code> and <code class="language-plaintext highlighter-rouge">@fields</code> can’t be made contiguous.</p>

<h2 id="delegation">Delegation</h2>

<p>That’s where it came to me that we could instead bundle the <code class="language-plaintext highlighter-rouge">@shape</code> and <code class="language-plaintext highlighter-rouge">@fields</code> in their own GC-managed object,
so that when we have to update both atomically, we can work on a copy and then swap the pointer:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Module</span>
  <span class="k">def</span> <span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
    <span class="vi">@fields_object</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">instance_variable_set</span><span class="p">(</span><span class="n">variable_name</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
    <span class="k">raise</span> <span class="no">FrozenError</span> <span class="k">if</span> <span class="nb">frozen?</span>
    <span class="c1"># The main ractor is the only one allowed to write instance variables</span>
    <span class="k">raise</span> <span class="no">Ractor</span><span class="o">::</span><span class="no">IsolationError</span> <span class="k">unless</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">main_ractor?</span>

    <span class="n">new_fields_object</span> <span class="o">=</span> <span class="vi">@fields_object</span> <span class="p">?</span> <span class="vi">@fields_object</span><span class="p">.</span><span class="nf">dup</span> <span class="p">:</span> <span class="no">Object</span><span class="p">.</span><span class="nf">new</span>
    <span class="n">new_fields_object</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="n">variable_name</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
    <span class="no">Atomic</span><span class="p">.</span><span class="nf">write</span> <span class="p">{</span> <span class="vi">@fields_object</span> <span class="o">=</span> <span class="n">new_fields_object</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">remove_instance_variable</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
    <span class="k">raise</span> <span class="no">FrozenError</span> <span class="k">if</span> <span class="nb">frozen?</span>
    <span class="c1"># The main ractor is the only one allowed to write instance variables</span>
    <span class="k">raise</span> <span class="no">Ractor</span><span class="o">::</span><span class="no">IsolationError</span> <span class="k">unless</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">main_ractor?</span>

    <span class="n">new_fields_object</span> <span class="o">=</span> <span class="vi">@fields_object</span> <span class="p">?</span> <span class="vi">@fields_object</span><span class="p">.</span><span class="nf">dup</span> <span class="p">:</span> <span class="no">Object</span><span class="p">.</span><span class="nf">new</span>
    <span class="n">new_fields_object</span><span class="p">.</span><span class="nf">remove_instance_variable</span><span class="p">(</span><span class="n">variable_name</span><span class="p">)</span>
    <span class="no">Atomic</span><span class="p">.</span><span class="nf">write</span> <span class="p">{</span> <span class="vi">@fields_object</span> <span class="o">=</span> <span class="n">new_fields_object</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It really is that trivial. Instead of storing instance variables in the class or module, we store them in a regular <code class="language-plaintext highlighter-rouge">Object</code>,
and on mutation, we first clone the current state, do our unsafe mutation, and finally atomically swap the <code class="language-plaintext highlighter-rouge">@fields_object</code> reference.</p>

<p>Of course, doing it exactly like this would cause a huge increase in object allocation, so in the actual code I added lots
of special cases to directly mutate the existing object rather than to copy it when it is safe to do so, but conceptually
this is <a href="https://github.com/byroot/ruby/commit/989bce8eef24c6dc6aeb7495d7c57c4324016e72">exactly what my current patch is doing</a>.</p>

<p>That patch is mostly a proof of concept, in the end, I don’t think we should use an actual <code class="language-plaintext highlighter-rouge">T_OBJECT</code> for various reasons,
but I already have a follow-up patch that replaces it with a <code class="language-plaintext highlighter-rouge">T_IMEMO</code>, which is an internal type invisible to Ruby users.</p>

<p>With this solution I was able to remove the locks around class instance variables, and now the ractor version
of the micro-benchmark runs almost 3 times faster than the single-threaded version:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ hyperfine -w 1 './miniruby --yjit ../test.rb' './miniruby --yjit ../test.rb ractor'
Benchmark 1: ./miniruby --yjit ../test.rb
  Time (mean ± σ):     166.3 ms ±   1.1 ms    [User: 164.4 ms, System: 1.5 ms]
  Range (min … max):   164.0 ms … 168.5 ms    18 runs

Benchmark 2: ./miniruby --yjit ../test.rb ractor
  Time (mean ± σ):      59.3 ms ±   2.6 ms    [User: 211.4 ms, System: 1.5 ms]
  Range (min … max):    57.9 ms …  67.7 ms    48 runs

Summary
  ./miniruby --yjit ../test.rb ractor ran
    2.80 ± 0.12 times faster than ./miniruby --yjit ../test.rb
</code></pre></div></div>

<p>That’s still far from the 8 times faster you might expect, but profiling indicates that it’s now a scheduling problem,
which we’ll eventually fix too, and it’s still over 13 times faster than on Ruby 3.4:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ hyperfine -w 1 'ruby --disable-all --yjit ../test.rb ractor' './ruby --disable-all --yjit ../test.rb ractor'
Benchmark 1: ruby --disable-all --yjit ../test.rb ractor
  Time (mean ± σ):     772.3 ms ±   9.0 ms    [User: 1023.8 ms, System: 1325.6 ms]
  Range (min … max):   759.3 ms … 790.5 ms    10 runs

Benchmark 2: ./ruby --disable-all --yjit ../test.rb ractor
  Time (mean ± σ):      56.8 ms ±   1.4 ms    [User: 205.7 ms, System: 1.6 ms]
  Range (min … max):    55.8 ms …  65.6 ms    50 runs

Summary
  ./ruby --disable-all --yjit ../test.rb ractor ran
   13.59 ± 0.36 times faster than ruby --disable-all --yjit ../test.rb ractor
</code></pre></div></div>

<p>Hopefully, I’ll get this merged in the next couple of weeks.</p>

<h2 id="wont-this-increase-memory-usage">Won’t This Increase Memory Usage?</h2>

<p>You may be thinking that this is all well and good, but that using another object to store classes and modules instance
variables in another object will increase Ruby’s memory usage.</p>

<p>Well, probably not. Previously the <code class="language-plaintext highlighter-rouge">@fields</code> memory was managed by <code class="language-plaintext highlighter-rouge">malloc</code>, and while it depends on which implementation
of <code class="language-plaintext highlighter-rouge">malloc</code> you are using, most of them will have an overhead of <code class="language-plaintext highlighter-rouge">16B</code> per allocated pointer, which is exactly the overhead
of a Ruby object.</p>

<p>So overall it shouldn’t cause memory usage to increase.</p>

<h2 id="cherry-on-top">Cherry On Top</h2>

<p>This solution has another incidental benefit, which is that it fixes both a bug and a performance regression recently introduced
when <a href="https://bugs.ruby-lang.org/issues/21311">the new Namespace feature was merged</a>.</p>

<p>Under namespaces, core classes are supposed to have a different set of instance variables, and frozen status, in each namespace,
but this doesn’t work well at all with shapes because right now the shape is stored in the object header, hence all objects
including classes and modules, only have a single shape.</p>

<p>By delegating instance variable management to another object, classes can now have one <code class="language-plaintext highlighter-rouge">@fields_object</code> per namespace,
encompassing both the shape and the fields, hence properly namespace class instance variables.</p>

<p>It wasn’t at all a motivation for this change, but it’s a nice side effect.</p>]]></content><author><name></name></author><category term="ruby" /><category term="performance" /><summary type="html"><![CDATA[In a previous post about ractors, I explained why I think it’s really unlikely you’d ever be able to run an entire application inside a ractor, but that they could still be situationally very useful to move CPU-bound work out of the main thread, and to unlock some parallel algorithm.]]></summary></entry><entry><title type="html">Unlocking Ractors: object_id</title><link href="https://byroot.github.io/ruby/performance/2025/04/26/unlocking-ractors-object-id.html" rel="alternate" type="text/html" title="Unlocking Ractors: object_id" /><published>2025-04-26T10:03:51+00:00</published><updated>2025-04-26T10:03:51+00:00</updated><id>https://byroot.github.io/ruby/performance/2025/04/26/unlocking-ractors-object-id</id><content type="html" xml:base="https://byroot.github.io/ruby/performance/2025/04/26/unlocking-ractors-object-id.html"><![CDATA[<p>In <a href="/ruby/performance/2025/02/27/whats-the-deal-with-ractors.html">a previous post about ractors</a>, I explained why I think it’s really unlikely you’d ever be able to run an entire application inside a ractor, but that they could
still be situationally very useful to move CPU-bound work out of the main thread, and to unlock some parallel algorithm.</p>

<p>But as I mentioned, this is unfortunately not yet viable because there are many known implementation bugs that can lead
to interpreter crashes, and that while they are supposed to execute in parallel, the Ruby VM still has one true global
lock that Ractors need to acquire to perform certain operations, making them often perform worse than the equivalent
single-threaded code.</p>

<p>But things are evolving rapidly.
Since then, there is now a team of people working on fixing exactly that: tackling known bugs and eliminating or reducing the remaining contention points.</p>

<p>The one example I gave to illustrate this remaining contention, was the <code class="language-plaintext highlighter-rouge">fstring_table</code>, which in short is a big internal
hash table used to deduplicate strings, which Ruby does whenever you use a String as a key in a Hash.
Because looking into that table while another Ractor is inserting a new entry would result in a crash (or worse),
until last week Ruby had to acquire the remaining VM lock whenever it touched that table.</p>

<p>But <a href="https://bugs.ruby-lang.org/issues/21268">John Hawthorn recently replaced it with a lock-free Hash-Set</a>, and now this
contention point is gone. If you re-run the JSON benchmarks from the previous post using the latest Ruby master,
the Ractor version is now twice as fast as the single-threaded version, instead of being 3 times slower.</p>

<p>This still isn’t perfect though, as the benchmark uses 5 ractors, hence in an ideal world should be almost 5 times faster
then the single-threaded example, so we still have a lot of work to do to eliminate or reduce the remaining contention
points.</p>

<p>One of such remaining contention points, that you likely didn’t suspect would be one, is
<a href="https://docs.ruby-lang.org/en/3.4/Object.html#method-i-object_id">the <code class="language-plaintext highlighter-rouge">#object_id</code> method</a>.
And on my way back from RubyKaigi, I started working on tackling it.</p>

<p>But before we delve into what I plan to do about it, let’s talk about how this method came to be a contention point.</p>

<h2 id="a-little-bit-of-history">A Little Bit Of History</h2>

<p>Up until Ruby 2.6, the <code class="language-plaintext highlighter-rouge">#object_id</code> implementation used to be quite trivial:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">VALUE</span>
<span class="nf">rb_obj_id</span><span class="p">(</span><span class="n">VALUE</span> <span class="n">obj</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">STATIC_SYM_P</span><span class="p">(</span><span class="n">obj</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">(</span><span class="n">SYM2ID</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">RVALUE</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="mi">4</span> <span class="o">&lt;&lt;</span> <span class="mi">2</span><span class="p">))</span> <span class="o">|</span> <span class="n">FIXNUM_FLAG</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">FLONUM_P</span><span class="p">(</span><span class="n">obj</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">return</span> <span class="n">LL2NUM</span><span class="p">((</span><span class="n">SIGNED_VALUE</span><span class="p">)</span><span class="n">obj</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">SPECIAL_CONST_P</span><span class="p">(</span><span class="n">obj</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">return</span> <span class="n">LONG2NUM</span><span class="p">((</span><span class="n">SIGNED_VALUE</span><span class="p">)</span><span class="n">obj</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">LL2NUM</span><span class="p">((</span><span class="n">SIGNED_VALUE</span><span class="p">)(</span><span class="n">obj</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Of course, it’s C so it might be a bit cryptic to the uninitiated, but in short, for the common case of a heap allocated
object, its <code class="language-plaintext highlighter-rouge">object_id</code> would be the address where the object is stored, divided by two.
So in a way, <code class="language-plaintext highlighter-rouge">#object_id</code> used to return you an actual pointer to the object.</p>

<p>This made implementing the lesser-known counterpart of <code class="language-plaintext highlighter-rouge">#object_id</code>, <a href="https://docs.ruby-lang.org/en/2.5.0/ObjectSpace.html#method-c-_id2ref"><code class="language-plaintext highlighter-rouge">ObjectSpace._id2ref</code></a>,
just as trivial, multiply the <code class="language-plaintext highlighter-rouge">object_id</code> by two, and here you go, you now have a pointer to the corresponding object.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">s</span> <span class="o">=</span> <span class="s2">"I am a string"</span>
<span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">_id2ref</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="nf">object_id</span><span class="p">).</span><span class="nf">equal?</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="c1"># =&gt; true</span>
</code></pre></div></div>

<p>But there was actually a major problem with that implementation, which is that the Ruby heap is composed of standard-size slots.
When an object is no longer referenced, the GC reclaims the object slot and will most likely re-use it for a future object.</p>

<p>Hence if you were to hold onto an <code class="language-plaintext highlighter-rouge">object_id</code>, and use <code class="language-plaintext highlighter-rouge">ObjectSpace._id2ref</code>, it’s not actually certain the object you get
back is the one you got the <code class="language-plaintext highlighter-rouge">object_id</code> from, it might be a totally different object.</p>

<p>It also meant that if you are holding onto an <code class="language-plaintext highlighter-rouge">object_id</code> as a way to know if you’ve already seen a given object,
you may run into some false positives.</p>

<p>That’s why <a href="https://bugs.ruby-lang.org/issues/15408">in 2018 there was already a feature request to deprecate both <code class="language-plaintext highlighter-rouge">#object_id</code> and <code class="language-plaintext highlighter-rouge">_id2ref</code></a>.
Back then Matz agreed to deprecated <code class="language-plaintext highlighter-rouge">_id2ref</code> for Ruby 2.7, but pointed out that removing <code class="language-plaintext highlighter-rouge">#object_id</code> would be too much of a breaking change,
and that it is a useful API.
However, this somehow fell through the cracks, and <code class="language-plaintext highlighter-rouge">_id2ref</code> was never formally deprecated, which is <a href="https://github.com/ruby/ruby/pull/13157">something I’d like to
do for Ruby 3.5</a>.</p>

<p>I’m not certain why <code class="language-plaintext highlighter-rouge">_id2ref</code> was added initially, given that <code class="language-plaintext highlighter-rouge">git blame</code> points to <a href="https://github.com/ruby/ruby/commit/210367ec889">a commit from 1999 that was generated by cvs2svn</a>.
But if I had to guess, I’d say it was added for <code class="language-plaintext highlighter-rouge">drb</code> which today remains the only significant user of that API in the stdlib, but <a href="https://github.com/ruby/drb/pull/35">even that is about to change</a>.</p>

<h2 id="gc-compaction">GC Compaction</h2>

<p>Regardless of why <code class="language-plaintext highlighter-rouge">_id2ref</code> was added, that major flaw in its design became a blocker for Aaron Patterson when <a href="https://bugs.ruby-lang.org/issues/15626">he implemented
GC compaction in Ruby 2.7</a>.
Since GC compaction implies that objects can be moved from one slot to another, <code class="language-plaintext highlighter-rouge">#object_id</code> could no longer be derived from
the object address, otherwise, it wouldn’t remain stable.</p>

<p>What Aaron did is conceptually simple:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Kernel</span>
  <span class="k">def</span> <span class="nf">object_id</span>
    <span class="k">unless</span> <span class="nb">id</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">OBJ_TO_ID_TABLE</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span>
      <span class="nb">id</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">next_obj_id</span>
      <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">next_obj_id</span> <span class="o">+=</span> <span class="mi">8</span>
      <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">OBJ_TO_ID_TABLE</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span> <span class="o">=</span> <span class="nb">id</span>
      <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span><span class="p">[</span><span class="nb">id</span><span class="p">]</span> <span class="o">=</span> <span class="nb">self</span>
    <span class="k">end</span>
    <span class="nb">id</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">module</span> <span class="nn">ObjectSpace</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">_id2ref</span><span class="p">(</span><span class="nb">id</span><span class="p">)</span>
    <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span><span class="p">[</span><span class="nb">id</span><span class="p">]</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In short, Ruby added two internal Hash tables. One of them with objects as keys and IDs as values, and the inverse for the other.
Whenever you access an object’s ID for the first time, a unique ID is created by incrementing an internal counter,
and the relation between the object and its ID is stored in the two hash tables.</p>

<p>As a Ruby user, you can observe this change easily by printing some <code class="language-plaintext highlighter-rouge">object_id</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">p</span> <span class="no">Object</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">object_id</span>
<span class="nb">p</span> <span class="no">Object</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">object_id</span>
</code></pre></div></div>

<p>Up to Ruby 2.6, the above code will print some large and seemingly random integers such as <code class="language-plaintext highlighter-rouge">50666405449360</code>, whereas on
Ruby 2.7 onwards, it will print small integers, likely <code class="language-plaintext highlighter-rouge">8</code> and <code class="language-plaintext highlighter-rouge">16</code>.</p>

<p>This change both solved the historical issue with <code class="language-plaintext highlighter-rouge">_id2ref</code> and allowed the GC to keep stable IDs when moving objects from one
address to the other, but made <code class="language-plaintext highlighter-rouge">object_id</code> way more costly than it used to be.</p>

<p>Ruby’s hash-table implementation stores 3 pointer-sized numbers per entry.
One for the key, one for the value, and one for the hashcode:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">st_table_entry</span> <span class="p">{</span>
    <span class="n">st_hash_t</span> <span class="n">hash</span><span class="p">;</span>
    <span class="n">st_data_t</span> <span class="n">key</span><span class="p">;</span>
    <span class="n">st_data_t</span> <span class="n">record</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>And given every <code class="language-plaintext highlighter-rouge">object_id</code> is stored in two hash-tables, that makes for a total of <code class="language-plaintext highlighter-rouge">48B</code> (plus some change) per <code class="language-plaintext highlighter-rouge">object_id</code>.
That’s quite a lot of memory for just a small number.</p>

<p>In addition, accessing the <code class="language-plaintext highlighter-rouge">object_id</code> now requires doing a hash lookup, when before it was a simple division, and whenever
the GC frees or moves an object that has an ID, it needs to update these two hash-tables.</p>

<p>To be clear, I don’t have any evidence that these two tables cause significant memory or CPU overhead in real-world Ruby applications.
I’m just saying that <code class="language-plaintext highlighter-rouge">#object_id</code> is way more expensive than one might expect.</p>

<h2 id="entering-ractors">Entering Ractors</h2>

<p>Then later on, when Koichi Sasada implemented Ractors since now multiple ractors could attempt to access these two hash-tables
concurrently, <a href="https://github.com/ruby/ruby/commit/da3438a5045">he had to add a lock around them in <code class="language-plaintext highlighter-rouge">#object_id</code></a>, turning
<code class="language-plaintext highlighter-rouge">#object_id</code> in a contention point:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Kernel</span>
  <span class="k">def</span> <span class="nf">object_id</span>
    <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
      <span class="k">unless</span> <span class="nb">id</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">OBJ_TO_ID_TABLE</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span>
        <span class="nb">id</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">next_obj_id</span>
        <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">next_obj_id</span> <span class="o">+=</span> <span class="mi">8</span>
        <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">OBJ_TO_ID_TABLE</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span> <span class="o">=</span> <span class="nb">id</span>
        <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span><span class="p">[</span><span class="nb">id</span><span class="p">]</span> <span class="o">=</span> <span class="nb">self</span>
      <span class="k">end</span>
      <span class="nb">id</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">module</span> <span class="nn">ObjectSpace</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">_id2ref</span><span class="p">(</span><span class="nb">id</span><span class="p">)</span>
    <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
      <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span><span class="p">[</span><span class="nb">id</span><span class="p">]</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>At this point, you may wonder if it’s really a big deal.
After all, <code class="language-plaintext highlighter-rouge">#object_id</code> is used a bit for debugging, but not so much in actual production code.
And this is mostly true, but it does come up in real-world code, e.g. <a href="https://github.com/mikel/mail/blob/d1d65b370b109b98e673a934e8b70a0c1f58cc59/lib/mail/message.rb#L1698">in the <code class="language-plaintext highlighter-rouge">mail</code> gem</a>,
<a href="https://github.com/rubocop/rubocop/blob/4a611564c4e1d8ec12a8e45e96490465e5141605/lib/rubocop/cop/variable_force/branch.rb#L129-L131">in <code class="language-plaintext highlighter-rouge">rubocop</code></a>,
and of course <a href="https://github.com/rails/rails/blob/99e27fa586af7db2b5334124a62eb3a464cdffd8/activesupport/lib/active_support/cache/strategy/local_cache.rb#L213-L215">quite a bit in Rails</a>.</p>

<p>But calling <code class="language-plaintext highlighter-rouge">Kernel#object_id</code> isn’t the only way you might rely on an object ID.</p>

<p>The <a href="https://docs.ruby-lang.org/en/3.4/Object.html#method-i-hash"><code class="language-plaintext highlighter-rouge">Object#hash</code></a> method for example rely on it:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">st_index_t</span>
<span class="nf">objid_hash</span><span class="p">(</span><span class="n">VALUE</span> <span class="n">obj</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">VALUE</span> <span class="n">object_id</span> <span class="o">=</span> <span class="n">rb_obj_id</span><span class="p">(</span><span class="n">obj</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">FIXNUM_P</span><span class="p">(</span><span class="n">object_id</span><span class="p">))</span>
        <span class="n">object_id</span> <span class="o">=</span> <span class="n">rb_big_hash</span><span class="p">(</span><span class="n">object_id</span><span class="p">);</span>

    <span class="k">return</span> <span class="p">(</span><span class="n">st_index_t</span><span class="p">)</span><span class="n">st_index_hash</span><span class="p">((</span><span class="n">st_index_t</span><span class="p">)</span><span class="n">NUM2LL</span><span class="p">(</span><span class="n">object_id</span><span class="p">));</span>
<span class="p">}</span>

<span class="n">VALUE</span>
<span class="nf">rb_obj_hash</span><span class="p">(</span><span class="n">VALUE</span> <span class="n">obj</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">long</span> <span class="n">hnum</span> <span class="o">=</span> <span class="n">any_hash</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">objid_hash</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">ST2FIX</span><span class="p">(</span><span class="n">hnum</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Common value classes such as <code class="language-plaintext highlighter-rouge">String</code>, <code class="language-plaintext highlighter-rouge">Array</code> etc, do define their own <code class="language-plaintext highlighter-rouge">#hash</code> method that doesn’t rely on the object ID,
but all other objects that are compared by identity by default will end up using <code class="language-plaintext highlighter-rouge">Object#hash</code>, hence accessing the <code class="language-plaintext highlighter-rouge">object_id</code>.</p>

<p>For instance here’s a quite class <code class="language-plaintext highlighter-rouge">#hash</code> implementation from one of Rails classes:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#  activerecord/lib/arel/nodes/delete_statement.rb</span>
  <span class="k">def</span> <span class="nf">hash</span>
    <span class="p">[</span><span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">,</span> <span class="vi">@relation</span><span class="p">,</span> <span class="vi">@wheres</span><span class="p">,</span> <span class="vi">@orders</span><span class="p">,</span> <span class="vi">@limit</span><span class="p">,</span> <span class="vi">@offset</span><span class="p">,</span> <span class="vi">@key</span><span class="p">].</span><span class="nf">hash</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>It absolutely isn’t obvious, but here we’re hashing a <code class="language-plaintext highlighter-rouge">Class</code> object, and classes are indexed by identity like a default object:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">Class</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">method</span><span class="p">(</span><span class="ss">:hash</span><span class="p">).</span><span class="nf">owner</span>
<span class="o">=&gt;</span> <span class="no">Kernel</span>
<span class="o">&gt;&gt;</span> <span class="no">Object</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">method</span><span class="p">(</span><span class="ss">:hash</span><span class="p">).</span><span class="nf">owner</span>
<span class="o">=&gt;</span> <span class="no">Kernel</span>
</code></pre></div></div>

<p>Hence the above code currently requires to lock the entire virtual machine, just to produce a hashcode.</p>

<h2 id="deoptimization">Deoptimization</h2>

<p>So what could we do to remove or reduce the need to synchronize the entire virtual machine when accessing object IDs?</p>

<p>Well first, given that <code class="language-plaintext highlighter-rouge">ObjectSpace._id2ref</code> is very rarely used, and will likely be marked as deprecated soon,
we can start by optimistically not creating nor updating the <code class="language-plaintext highlighter-rouge">id -&gt; object</code> table until someone needs it, which hopefully
won’t be the case in the vast majority of programs:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Kernel</span>
  <span class="k">def</span> <span class="nf">object_id</span>
    <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
      <span class="k">unless</span> <span class="nb">id</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">OBJ_TO_ID_TABLE</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span>
        <span class="nb">id</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">next_obj_id</span>
        <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">next_obj_id</span> <span class="o">+=</span> <span class="mi">8</span>
        <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">OBJ_TO_ID_TABLE</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span> <span class="o">=</span> <span class="nb">id</span>
        <span class="k">if</span> <span class="k">defined?</span><span class="p">(</span><span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span><span class="p">)</span>
          <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span><span class="p">[</span><span class="nb">id</span><span class="p">]</span> <span class="o">=</span> <span class="nb">self</span>
        <span class="k">end</span>
      <span class="k">end</span>
      <span class="nb">id</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">module</span> <span class="nn">ObjectSpace</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">_id2ref</span><span class="p">(</span><span class="nb">id</span><span class="p">)</span>
    <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
      <span class="k">unless</span> <span class="k">defined?</span><span class="p">(</span><span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span><span class="p">)</span>
        <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">OBJ_TO_ID_TABLE</span><span class="p">.</span><span class="nf">invert</span>
      <span class="k">end</span>
      <span class="no">ObjectSpace</span><span class="o">::</span><span class="no">ID_TO_OBJ_TABLE</span><span class="p">[</span><span class="nb">id</span><span class="p">]</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This doesn’t remove the lock yet, but assuming your program never calls <code class="language-plaintext highlighter-rouge">ObjectSpace._id2ref</code> it removes some work
from inside the lock, hence it shouldn’t be held as long.
And even if you don’t use Ractors, it should slightly reduce memory usage as well as remove work for the GC,
as demonstrated by a micro-benchmark:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>benchmark:
  baseline: "Object.new"
  object_id: "Object.new.object_id"
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>compare-ruby: ruby 3.5.0dev (2025-04-10T09:44:40Z master 684cfa42d7) +YJIT +PRISM [arm64-darwin24]
built-ruby: ruby 3.5.0dev (2025-04-10T10:13:43Z lazy-id-to-obj d3aa9626cc) +YJIT +PRISM [arm64-darwin24]
warming up..

|           |compare-ruby|built-ruby|
|:----------|-----------:|---------:|
|baseline   |     26.364M|   25.974M|
|           |       1.01x|         -|
|object_id  |     10.293M|   14.202M|
|           |           -|     1.38x|
</code></pre></div></div>

<p>As always, when possible, the most efficient way to speed up some code is to not call it if you can avoid it.</p>

<p>If you’re curious to see the actual implementation, <a href="https://github.com/ruby/ruby/pull/13115">you can have a look at the pull request</a>.</p>

<h2 id="inline-storage">Inline Storage</h2>

<p>But while saving a bit of memory and CPU is nice, we’re still not significantly reducing contention, so what else could we do?</p>

<p>The crux of the issue here is that the <code class="language-plaintext highlighter-rouge">object_id</code> is stored in a centralized hash table, and as long as it will be the case,
synchronization will be required, short of implementing a lock-free hash table, but this is quite tricky to do.
Much trickier than a hash-set John used for the <code class="language-plaintext highlighter-rouge">fstring_table</code>.</p>

<p>But more importantly, a centralized data structure to store all the IDs of all objects isn’t great for locality anyway.
More so, needing to do a hash lookup to access an object’s property is quite costly, when conceptually it should be stored directly
inside the object.</p>

<p>If you think about it, <code class="language-plaintext highlighter-rouge">object_id</code> isn’t very different from an instance variable:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Kernel</span>
  <span class="k">def</span> <span class="nf">object_id</span>
    <span class="vi">@__object_id</span> <span class="o">||=</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">generate_next_obj_id</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You’d need the id generation to be thread-safe, which is easily done using an atomic increment operation, but other than that,
assuming the object isn’t one of the special objects that is accessible from multiple ractors, you can mutate it to store the
<code class="language-plaintext highlighter-rouge">object_id</code> without having to lock the entire VM.</p>

<p>However, as is tradition, nothing is ever that simple.</p>

<h2 id="final-shapes">Final Shapes</h2>

<p>Since Ruby 3.2, objects use shapes to define how their instance variables are stored.</p>

<p>Here again, let’s use some pseudo-Ruby code to illustrate the basics of how they work.</p>

<p>To start, shapes are a tree-like structure. Every shape has a parent (except the root one)
and 0-N children:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Shape</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">type</span><span class="p">,</span> <span class="n">edge_name</span><span class="p">,</span> <span class="n">next_ivar_index</span><span class="p">)</span>
    <span class="vi">@parent</span> <span class="o">=</span> <span class="n">parent</span>
    <span class="vi">@type</span> <span class="o">=</span> <span class="n">type</span>
    <span class="vi">@edge_name</span> <span class="o">=</span> <span class="n">edge_name</span>
    <span class="vi">@next_ivar_index</span> <span class="o">=</span> <span class="n">next_ivar_index</span>
    <span class="vi">@edges</span> <span class="o">=</span> <span class="p">{}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">add_ivar</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
    <span class="vi">@edges</span><span class="p">[</span><span class="n">ivar_name</span><span class="p">]</span> <span class="o">||=</span> <span class="no">Shape</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="ss">:ivar</span><span class="p">,</span> <span class="n">ivar_name</span><span class="p">,</span> <span class="n">next_ivar_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>With this, when the Ruby VM has to execute code such as:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">role</span><span class="p">)</span>
    <span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>
    <span class="vi">@role</span> <span class="o">=</span> <span class="n">role</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It can compute the object shape on the fly such as:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Allocate the object</span>
<span class="n">object</span> <span class="o">=</span> <span class="n">new_object</span>
<span class="n">object</span><span class="p">.</span><span class="nf">shape</span> <span class="o">=</span> <span class="no">ROOT_SHAPE</span>

<span class="c1"># add @name</span>
<span class="n">next_shape</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">add_ivar</span><span class="p">(</span><span class="ss">:@name</span><span class="p">)</span>
<span class="n">object</span><span class="p">.</span><span class="nf">shape</span> <span class="o">=</span> <span class="n">next_shape</span>
<span class="n">object</span><span class="p">.</span><span class="nf">ivars</span><span class="p">[</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">next_ivar_index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nb">name</span>

<span class="c1"># add @role</span>
<span class="n">next_shape</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">add_ivar</span><span class="p">(</span><span class="ss">:@role</span><span class="p">)</span>
<span class="n">object</span><span class="p">.</span><span class="nf">shape</span> <span class="o">=</span> <span class="n">next_shape</span>
<span class="n">object</span><span class="p">.</span><span class="nf">ivars</span><span class="p">[</span><span class="n">next_shape</span><span class="p">.</span><span class="nf">next_ivar_index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">role</span>
</code></pre></div></div>

<p>This method may seem surprising, but it’s actually very efficient for various reasons I won’t get into here,
because I wrote <a href="https://railsatscale.com/2023-10-24-memoization-pattern-and-object-shapes/">another post about it a bit over a year ago</a>,
go read it if you are curious to know more.</p>

<p>But how instance variables are laid out isn’t the only thing that shapes record. They also keep track of how large an object
is, hence how many instance variables it can store, as well as whether it has been frozen.</p>

<p>Still in pseudo-Ruby code, it looks like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Shape</span>
  <span class="k">def</span> <span class="nf">add_ivar</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
    <span class="k">if</span> <span class="vi">@type</span> <span class="o">==</span> <span class="ss">:frozen</span>
      <span class="k">raise</span> <span class="s2">"Can't modify frozen object"</span>
    <span class="k">end</span>
    <span class="vi">@edges</span><span class="p">[</span><span class="n">ivar_name</span><span class="p">]</span> <span class="o">||=</span> <span class="no">Shape</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="ss">:ivar</span><span class="p">,</span> <span class="n">ivar_name</span><span class="p">,</span> <span class="n">next_ivar_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">freeze</span>
    <span class="vi">@edges</span><span class="p">[</span><span class="ss">:__frozen</span><span class="p">]</span> <span class="o">||=</span> <span class="no">Shape</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="ss">:frozen</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="n">next_ivar_index</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So <code class="language-plaintext highlighter-rouge">frozen</code> shapes are final. It is expected that a shape of type <code class="language-plaintext highlighter-rouge">frozen</code> won’t ever have any children.</p>

<p>But in the case of <code class="language-plaintext highlighter-rouge">object_id</code>, we want to be able to store the id on any object, regardless of whether they are frozen
or not. So the first step is to modify shapes to allow that, <a href="https://github.com/Shopify/ruby/commit/ca92bbe4f646658f9a420e61089cf5d6e27a5a71">which I did in a relatively simple commit</a>.</p>

<p>But here too there was a bit of a complication. In a few cases, for instance when calling <code class="language-plaintext highlighter-rouge">Object#dup</code>, Ruby needs to find
the unfrozen version of a shape. Previously, since frozen shapes couldn’t possibly have children, it was quite simple:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Object</span>
  <span class="k">def</span> <span class="nf">dup</span>
    <span class="n">new_object</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">allocate</span>
    <span class="k">if</span> <span class="nb">self</span><span class="p">.</span><span class="nf">shape</span><span class="p">.</span><span class="nf">type</span> <span class="o">==</span> <span class="ss">:frozen</span>
      <span class="n">new_object</span><span class="p">.</span><span class="nf">shape</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">shape</span><span class="p">.</span><span class="nf">parent</span>
    <span class="k">else</span>
      <span class="n">new_object</span><span class="p">.</span><span class="nf">shape</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">shape</span>
    <span class="k">end</span>
    <span class="c1"># ...</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Once you allow frozen shapes to have children, this operation becomes more involved, as you now need to go up the tree
to find the last non-frozen shape, then reapply all the child shapes you wish to carry over.</p>

<p>After this small refactoring was done, I could introduce a new type of shape: <code class="language-plaintext highlighter-rouge">SHAPE_OBJ_ID</code>, which behaves very similarly
to instance variable shapes:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Shape</span>
  <span class="k">def</span> <span class="nf">object_id</span>
    <span class="c1"># First check if there is an OBJ_ID shape in ancestors</span>
    <span class="n">shape</span> <span class="o">=</span> <span class="nb">self</span>
    <span class="k">while</span> <span class="n">shape</span><span class="p">.</span><span class="nf">parent</span>
      <span class="k">return</span> <span class="n">shape</span> <span class="k">if</span> <span class="n">shape</span><span class="p">.</span><span class="nf">type</span> <span class="o">==</span> <span class="ss">:obj_id</span>
      <span class="n">shape</span> <span class="o">=</span> <span class="n">shape</span><span class="p">.</span><span class="nf">parent</span>
    <span class="k">end</span>

    <span class="c1"># Otherwise create one.</span>
    <span class="vi">@edges</span><span class="p">[</span><span class="ss">:__object_id</span><span class="p">]</span> <span class="o">||=</span> <span class="no">Shape</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="ss">:obj_id</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="n">next_ivar_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And just like this, we’re now able to reserve some inline space inside any object to store the <code class="language-plaintext highlighter-rouge">object_id</code>,
and in <em>some cases</em> we’re able to access an object’s ID fully lock-free.</p>

<h2 id="lock-free-shapes">Lock Free Shapes</h2>

<p>Why I’m saying <em>in some cases</em> is because there are still a number of limitations.</p>

<p>First, since shapes are mostly immutable, we can access an object’s shape, and all its ancestors without taking a lock.
However, finding or creating a shape’s child currently still requires synchronizing the VM.
So even if my patch was to be applied, Ruby would still lock when accessing an object’s ID for the very first time,
it would only be lock-free on subsequent accesses.</p>

<p>Being able to find or create child shapes in a lock-free way would be useful way beyond the <code class="language-plaintext highlighter-rouge">object_id</code> use case, so
hopefully we’ll get to it in the future, I haven’t yet dedicated much thought to it, but I’m hopeful we can find
a solution. But even if we can’t do it lock-free, I think we could at least use a dedicated lock for it, so we wouldn’t
contend with all the other code paths that synchronize the entire VM, only paths that do the same operation.</p>

<p>Then, if the object is potentially shared between ractors, we also still need to acquire the lock before storing the ID,
as otherwise, concurrent writes may cause a race condition. Given we need to both update the object’s shape and write
the <code class="language-plaintext highlighter-rouge">object_id</code> inside the object, we can’t do it all in an atomic manner.</p>

<p>Finally, not all objects store their instance variables in the same way.</p>

<h2 id="generic-instance-variables">Generic Instance Variables</h2>

<p>As a Rubyist, you likely know that in Ruby everything is an object, but that doesn’t mean all objects are equal.</p>

<p>In the context of instance variables, there are essentially three types of objects: <code class="language-plaintext highlighter-rouge">T_OBJECT</code>, <code class="language-plaintext highlighter-rouge">T_CLASS/T_MODULE</code> and
then all the rest.</p>

<p><code class="language-plaintext highlighter-rouge">T_OBJECT</code> are your classic objects that inherit from the <code class="language-plaintext highlighter-rouge">BasicObject</code> class. Their instance variables are stored
inline directly inside the object slot, as long as it’s large enough. If it ends up overflowing, then a separated memory
location is allocated, and instance variables are moved there, the object slot then only contains a pointer to that auxiliary memory.</p>

<p><code class="language-plaintext highlighter-rouge">T_CLASS</code> and <code class="language-plaintext highlighter-rouge">T_MODULE</code> as their name suggests are all instances of the <code class="language-plaintext highlighter-rouge">Class</code> and <code class="language-plaintext highlighter-rouge">Module</code> classes. These are much
larger than regular objects, as they need to keep track of a lot of things, such as their method table, a pointer to the
parent class, etc:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">(</span><span class="no">Object</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="mi">40</span>
<span class="o">&gt;&gt;</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">memsize_of</span><span class="p">(</span><span class="no">Class</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="mi">192</span>
</code></pre></div></div>

<p>As such, they never store their instance variables inline, they always store them in auxiliary memory, and they have
dedicated space in their object slot to store the auxiliary memory pointer:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"># internal/class.h
</span><span class="k">struct</span> <span class="n">rb_classext_struct</span> <span class="p">{</span>
    <span class="n">VALUE</span> <span class="o">*</span><span class="n">iv_ptr</span><span class="p">;</span> <span class="c1">// iv = instance variable</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And finally, there are all the other objects, such as <code class="language-plaintext highlighter-rouge">T_STRING</code>, <code class="language-plaintext highlighter-rouge">T_ARRAY</code>, <code class="language-plaintext highlighter-rouge">T_HASH</code>, <code class="language-plaintext highlighter-rouge">T_REGEXP</code>, etc.
None of these have free space in their slot to store inline variables, and not even space to store the auxiliary memory
pointer.</p>

<p>So what does Ruby do when you do add an instance variable to such objects? Well, it stores it in a Hash-table of course!</p>

<p>In pseudo-Ruby, it would look like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">GenericIvarObject</span>
  <span class="k">class</span> <span class="nc">GenericStorage</span>
    <span class="nb">attr_accessor</span> <span class="ss">:shape</span>
    <span class="nb">attr_reader</span> <span class="ss">:ivars</span>

    <span class="k">def</span> <span class="nf">initialize</span>
      <span class="vi">@ivars</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">instance_variable_get</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
    <span class="n">store</span> <span class="o">=</span> <span class="no">RubyVM</span><span class="p">.</span><span class="nf">synchronize</span> <span class="k">do</span>
      <span class="no">GENERIC_STORAGE</span><span class="p">[</span><span class="nb">self</span><span class="p">]</span> <span class="o">||=</span> <span class="no">GenericStorage</span><span class="p">.</span><span class="nf">new</span>
    <span class="k">end</span>

    <span class="k">if</span> <span class="n">ivar_shape</span> <span class="o">=</span> <span class="n">store</span><span class="p">.</span><span class="nf">shape</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">ivar_name</span><span class="p">)</span>
      <span class="n">store</span><span class="p">.</span><span class="nf">ivars</span><span class="p">[</span><span class="n">ivar_shape</span><span class="p">.</span><span class="nf">next_ivar_index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As you probably have noticed or even guessed, since this is yet another global hash table, any access needs to be synchronized,
which means that for objects other than <code class="language-plaintext highlighter-rouge">T_OBJECT</code>, <code class="language-plaintext highlighter-rouge">T_CLASS</code> and <code class="language-plaintext highlighter-rouge">T_MODULE</code>,
my patch replaces one global synchronized hash with another…</p>

<p>So perhaps for these, keeping the original <code class="language-plaintext highlighter-rouge">object -&gt; id</code> table would be preferable, that’s something I still need to figure out.</p>

<h3 id="conclusion">Conclusion</h3>

<p>My patch isn’t finished. I still have to figure out how to best deal with “generic” objects, and probably refine the
implementation some more, and perhaps it won’t even be merged at all in the end.</p>

<p>But I wanted to share it because explaining something helps me think about the problem,
and also because while I don’t think <code class="language-plaintext highlighter-rouge">object_id</code> is currently the biggest Ractor bottleneck,
it’s a good showcase of the type of work that needs to be done to make Ractors more parallel.</p>

<p>If you are curious about the patch, here’s <a href="https://github.com/ruby/ruby/compare/master...byroot:ruby:object_id-in-shape-snapshot">what it currently looks like as of this writing</a>.</p>

<p>Similar work will have to be done for other internal tables, such as the symbol table and the various method tables.</p>]]></content><author><name></name></author><category term="ruby" /><category term="performance" /><summary type="html"><![CDATA[In a previous post about ractors, I explained why I think it’s really unlikely you’d ever be able to run an entire application inside a ractor, but that they could still be situationally very useful to move CPU-bound work out of the main thread, and to unlock some parallel algorithm.]]></summary></entry></feed>