<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en" xml:base="https://vincent.bernat.ch">
  <title>Vincent Bernat — Programming &gt; Go</title>
  <link href="https://vincent.bernat.ch/en/blog/atom-programming-go.xml" rel="self"/>
  <link href="https://vincent.bernat.ch/en/blog#tag-programming-go" rel="alternate"/>
  <id>http://www.luffy.cx/en/blog/atom-programming-go.xml/</id>
  <updated>2026-10-25T15:08:12Z</updated>

  <entry>
    <title type="html">Hacking the Go compiler to efficiently map IPv4 to IPv6</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-go-netip-addrto6" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-go-netip-addrto6#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-10-04T14:12:19Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-go-netip-addrto6.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="https://pkg.go.dev/net/netip#Addr" title="net/netip.Addr on Go Packages"><code>netip.Addr</code></a> features an <a href="https://pkg.go.dev/net/netip#Addr.Unmap" title="net/netip.Addr.Unmap on Go Packages"><code>Unmap()</code></a>
method returning the unwrapped IPv4 contained in an <a href="https://www.rfc-editor.org/rfc/rfc4291#section-2.5.5.2" title="RFC 4291: IP Version 6 Addressing Architecture">IPv4-mapped IPv6
address</a>: from <code>::ffff:203.0.113.10</code> or <code>::ffff:cb00:710a</code>, it returns
<code>203.0.113.10</code>.<sup id="fnref:why"><a class="footnote-ref" href="#fn:why">1</a></sup> There is no <code>Map()</code> or <code>To6()</code> method for the reverse
direction. Such a method is trivial to implement, but Go maintainers have
<a href="https://github.com/golang/go/issues/54365#issuecomment-1570607144" title="Issue #54365: proposal: net/netip: add netip.Addr.Map">rejected</a> it on the grounds that users should write
<code>netip.AddrFrom16(ip.As16())</code> and let the compiler optimize it.<sup id="fnref:equiv"><a class="footnote-ref" href="#fn:equiv">2</a></sup> Today,
this pattern is eight times slower than a native method. How can we teach the
compiler to optimize this sequence?</p>
<div class="toc">
<ul>
<li><a href="#the-alternatives">The alternatives</a><ul>
<li><a href="#modifying-the-go-standard-library">Modifying the Go standard library</a></li>
<li><a href="#as-a-helper">As a helper</a></li>
<li><a href="#as-an-unsafe-function">As an unsafe function</a></li>
<li><a href="#benchmarks">Benchmarks</a></li>
<li><a href="#assembly-code">Assembly code</a></li>
</ul>
</li>
<li><a href="#hacking-the-go-compiler">Hacking the Go compiler</a><ul>
<li><a href="#the-hammer">The hammer</a></li>
<li><a href="#the-screwdriver">The screwdriver</a><ul>
<li><a href="#on-paper">On paper</a></li>
<li><a href="#in-practice">In practice</a></li>
<li><a href="#out-of-order">Out of order</a></li>
<li><a href="#back-in-order">Back in order</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#next-steps">Next steps</a></li>
</ul>
</div>
<h1 id="the-alternatives">The alternatives</h1>
<p>Let’s explore three ways to implement the map semantics for <code>netip.Addr</code>. My
favorite is to add it to the Go standard library. Go maintainers prefer a small
external helper chaining <code>netip.AddrFrom16()</code> and <code>netip.Addr.As16()</code>, hoping
the compiler eventually optimizes it. The <a href="https://pkg.go.dev/unsafe" title="unsafe on Go Packages"><code>unsafe</code> package</a>
opens a third path, with the same performance as the first solution.</p>
<h2 id="modifying-the-go-standard-library">Modifying the Go standard library</h2>
<p>Internally, <a href="https://pkg.go.dev/net/netip#Addr" title="net/netip.Addr on Go Packages"><code>netip.Addr</code></a> stores any IP address as a
128-bit value with an extra field <code>z</code> to encode the family and the zone:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">Addr</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">addr</span><span class="w"> </span><span class="nx">uint128</span>
<span class="w">    </span><span class="nx">z</span><span class="w"> </span><span class="nx">unique</span><span class="p">.</span><span class="nx">Handle</span><span class="p">[</span><span class="nx">addrDetail</span><span class="p">]</span>
<span class="p">}</span>

<span class="kd">type</span><span class="w"> </span><span class="nx">addrDetail</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">isV6</span><span class="w">   </span><span class="kt">bool</span><span class="w">   </span><span class="c1">// IPv4 is false, IPv6 is true.</span>
<span class="w">    </span><span class="nx">zoneV6</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="c1">// != "" only if IsV6 is true.</span>
<span class="p">}</span>

<span class="kd">var</span><span class="w"> </span><span class="p">(</span>
<span class="w">    </span><span class="nx">z0</span><span class="w">    </span><span class="nx">unique</span><span class="p">.</span><span class="nx">Handle</span><span class="p">[</span><span class="nx">addrDetail</span><span class="p">]</span>
<span class="w">    </span><span class="nx">z4</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nx">unique</span><span class="p">.</span><span class="nx">Make</span><span class="p">(</span><span class="nx">addrDetail</span><span class="p">{})</span>
<span class="w">    </span><span class="nx">z6noz</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">unique</span><span class="p">.</span><span class="nx">Make</span><span class="p">(</span><span class="nx">addrDetail</span><span class="p">{</span><span class="nx">isV6</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">})</span>
<span class="p">)</span>
</code></pre></div>

<p><a href="https://pkg.go.dev/net/netip#AddrFrom4" title="net/netip.AddrFrom4 on Go Packages"><code>AddrFrom4()</code></a> encodes an IPv4 address as an
IPv4-mapped IPv6 address and sets <code>z</code> to the unique value <code>z4</code>:</p>
<div class="language-go codehilite"><pre><span/><code><span class="c1">// AddrFrom4 returns the address of the IPv4 address given by the bytes in addr.</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">AddrFrom4</span><span class="p">(</span><span class="nx">addr</span><span class="w"> </span><span class="p">[</span><span class="mi">4</span><span class="p">]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="nx">Addr</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">Addr</span><span class="p">{</span>
<span class="w">        </span><span class="nx">addr</span><span class="p">:</span><span class="w"> </span><span class="nx">uint128</span><span class="p">{</span>
<span class="w">            </span><span class="mi">0</span><span class="p">,</span>
<span class="w">            </span><span class="mh">0xffff00000000</span><span class="w"> </span><span class="o">|</span>
<span class="w">                </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">addr</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">24</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">addr</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">16</span><span class="w"> </span><span class="o">|</span>
<span class="w">                </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">addr</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">8</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">addr</span><span class="p">[</span><span class="mi">3</span><span class="p">])},</span>
<span class="w">        </span><span class="nx">z</span><span class="p">:</span><span class="w"> </span><span class="nx">z4</span><span class="p">,</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p><a href="https://pkg.go.dev/net/netip#Addr.Unmap" title="net/netip.Addr.Unmap on Go Packages"><code>Unmap()</code></a> turns an IPv4-mapped IPv6 address into an
IPv4 address by setting the <code>z</code> field to <code>z4</code>:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">ip</span><span class="w"> </span><span class="nx">Addr</span><span class="p">)</span><span class="w"> </span><span class="nx">Unmap</span><span class="p">()</span><span class="w"> </span><span class="nx">Addr</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">ip</span><span class="p">.</span><span class="nx">Is4In6</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">ip</span><span class="p">.</span><span class="nx">z</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">z4</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">ip</span>
<span class="p">}</span>
</code></pre></div>

<p>Implementing the reverse direction inside the Go standard library is trivial: we
set the <code>z</code> field to <code>z6noz</code> if the address is IPv4.</p>
<div class="language-go codehilite"><pre><span/><code><span class="c1">// To6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an</span>
<span class="c1">// IPv6 address unmodified.</span>
<span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">ip</span><span class="w"> </span><span class="nx">Addr</span><span class="p">)</span><span class="w"> </span><span class="nx">To6</span><span class="p">()</span><span class="w"> </span><span class="nx">Addr</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">ip</span><span class="p">.</span><span class="nx">Is4</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">ip</span><span class="p">.</span><span class="nx">z</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">z6noz</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">ip</span>
<span class="p">}</span>
</code></pre></div>

<div class="admonition">
<p class="admonition-title">Update (2026-10)</p>
<p>Instead of patching Go, we can use the <a href="https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies" title="go build documentation"><code>-overlay</code> flag</a> of <code>go
build</code> to hijack any package, including the standard library. In our case,
it can <a href="https://github.com/vincentbernat/go-netip-addrto6/commit/dcc2e1af2af48301d8e992db22d97682bda37b8c" title="Add an alternative using an overlay">add <code>To6()</code> to <code>net/netip</code></a>. Yet it is
cumbersome:<sup id="fnref:inconvenient"><a class="footnote-ref" href="#fn:inconvenient">3</a></sup> the flag expects a JSON file mapping absolute
paths in <code>GOROOT</code> to replacement files, and you need to add it to every <code>go</code>
command.</p>
</div>
<h2 id="as-a-helper">As a helper</h2>
<p>We can’t access the <code>z</code> field from outside the <code>net/netip</code> package. Instead, we
build a small helper around the <code>netip.AddrFrom16(ip.As16())</code> pattern:</p>
<div class="language-go codehilite"><pre><span/><code><span class="c1">// AddrTo6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an</span>
<span class="c1">// IPv6 address unmodified.</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">AddrTo6</span><span class="p">(</span><span class="nx">ip</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span><span class="p">)</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">ip</span><span class="p">.</span><span class="nx">Is4</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">ip</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">AddrFrom16</span><span class="p">(</span><span class="nx">ip</span><span class="p">.</span><span class="nx">As16</span><span class="p">())</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">ip</span>
<span class="p">}</span>
</code></pre></div>

<h2 id="as-an-unsafe-function">As an unsafe function</h2>
<p>Another solution uses the <code>unsafe</code> package to alter the <code>Addr</code> struct through
a proxy with the same memory layout:<sup id="fnref:tests"><a class="footnote-ref" href="#fn:tests">4</a></sup></p>
<div class="language-go codehilite"><pre><span/><code><span class="c1">// addrProxy has the same memory layout as netip.Addr.</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">addrProxy</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">addr</span><span class="w"> </span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="kt">uint64</span><span class="w">      </span><span class="c1">// netip.uint128</span>
<span class="w">    </span><span class="nx">z</span><span class="w">    </span><span class="nx">unsafe</span><span class="p">.</span><span class="nx">Pointer</span><span class="w"> </span><span class="c1">// unique.Handle[netip.addrDetail]</span>
<span class="p">}</span>

<span class="kd">var</span><span class="w"> </span><span class="p">(</span>
<span class="w">    </span><span class="nx">anyIPv6</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">IPv6Unspecified</span><span class="p">()</span>
<span class="w">    </span><span class="nx">netipZ6noz</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="o">*</span><span class="nx">addrProxy</span><span class="p">)(</span><span class="nx">unsafe</span><span class="p">.</span><span class="nx">Pointer</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">anyIPv6</span><span class="p">)).</span><span class="nx">z</span>
<span class="p">)</span>

<span class="c1">// AddrTo6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an</span>
<span class="c1">// IPv6 address unmodified.</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">AddrTo6</span><span class="p">(</span><span class="nx">ip</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span><span class="p">)</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="o">!</span><span class="nx">ip</span><span class="p">.</span><span class="nx">Is4</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="nx">ip</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="p">(</span><span class="o">*</span><span class="nx">addrProxy</span><span class="p">)(</span><span class="nx">unsafe</span><span class="p">.</span><span class="nx">Pointer</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">ip</span><span class="p">)).</span><span class="nx">z</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">netipZ6noz</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">ip</span>
<span class="p">}</span>
</code></pre></div>

<h2 id="benchmarks">Benchmarks</h2>
<p>On my computer, with Go 1.27.1, the standard library solution costs 0.88 ns per
operation, while the solution favored by Go maintainers costs 7.14 ns. The
unsafe solution matches the performance of the first one.</p>
<div class="language-text-only codehilite"><pre><span/><code>goos: linux
goarch: amd64
pkg: github.com/vincentbernat/go-netip-addrto6
cpu: AMD Ryzen 5 5600X 6-Core Processor
                   │     sec/op     │
AddrTo6/safe            7.137n ± 0%
AddrTo6/unsafe         0.8682n ± 2%
AddrTo6/builtin        0.8775n ± 2%
</code></pre></div>

<h2 id="assembly-code">Assembly code</h2>
<p>Let’s check the assembly code the compiler generates for each solution.<sup id="fnref:dis"><a class="footnote-ref" href="#fn:dis">5</a></sup>
The one built into the standard library looks like this:<sup id="fnref:inline"><a class="footnote-ref" href="#fn:inline">6</a></sup></p>
<div class="language-go-assembly codehilite"><pre><span/><code><span class="c1">// AX = input.addr.hi, BX = input.addr.lo, CX = input.z</span>
<span class="w"> </span><span class="nf">CMPQ</span><span class="w">  </span><span class="ss">net/netip·z4</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">     </span><span class="c1">; check "z" if this is an IPv4 address</span>
<span class="w"> </span><span class="nf">JNE</span><span class="w">   </span><span class="ss">end</span><span class="w">                      </span><span class="c1">; if not, stop here</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">  </span><span class="ss">net/netip·z6noz</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">  </span><span class="c1">; CX = netip.z6noz</span>
<span class="nl">end</span><span class="p">:</span>
<span class="w"> </span><span class="nf">RET</span>
<span class="c1">// return value = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>Go’s <a href="https://go.dev/doc/asm" title="A Quick Guide to Go's Assembler">assembly language</a> is not a direct representation of the underlying
machine language: it operates on a semi-abstract instruction set derived from
<a href="https://9p.io/sys/doc/asm.html" title=" A Manual for the Plan 9 assembler">Plan 9’s assembler</a>. It has four pseudo-registers: FP (frame pointer
for function arguments), PC (program counter), SB (static base pointer for
global symbols), and SP (stack pointer). It also has architecture-specific
registers like <code>AX</code>, <code>CX</code>, <code>DX</code>, <code>BX</code>, <code>SI</code>, <code>DI</code>, and <code>R8</code> to <code>R15</code>.
Instructions storing data use their last argument as the destination.
Instructions can carry an explicit size suffix: <code>MOVB</code> moves a byte, <code>MOVW</code> 16
bits, <code>MOVL</code> 32 bits, and <code>MOVQ</code> 64 bits. In the example above, the first
instruction compares the 64-bit value <code>z4</code> with the <code>CX</code> register.</p>
<p>The unsafe solution looks almost the same:</p>
<div class="language-go-assembly codehilite"><pre><span/><code><span class="c1">// AX = input.addr.hi, BX = input.addr.lo, CX = input.z</span>
<span class="w"> </span><span class="nf">CMPQ</span><span class="w">  </span><span class="ss">net/netip·z4</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">  </span><span class="c1">; check "z" if this is an IPv4 address</span>
<span class="w"> </span><span class="nf">JNE</span><span class="w">   </span><span class="ss">end</span><span class="w">                   </span><span class="c1">; if not, stop here</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">  </span><span class="ss">netipZ6noz</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">    </span><span class="c1">; CX = netip.z6noz</span>
<span class="nl">end</span><span class="p">:</span>
<span class="w"> </span><span class="nf">RET</span>
<span class="c1">// return value = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>The helper solution has far more instructions. To understand why, let’s look at
the code for <a href="https://pkg.go.dev/net/netip#Addr.As16" title="net/netip.Addr.As16 on Go Packages"><code>As16()</code></a> and
<a href="https://pkg.go.dev/net/netip#AddrFrom16" title="net/netip.AddrFrom16 on Go Packages"><code>AddrFrom16()</code></a>. They are short enough for the compiler
to inline them.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">ip</span><span class="w"> </span><span class="nx">Addr</span><span class="p">)</span><span class="w"> </span><span class="nx">As16</span><span class="p">()</span><span class="w"> </span><span class="p">(</span><span class="nx">a16</span><span class="w"> </span><span class="p">[</span><span class="mi">16</span><span class="p">]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">byteorder</span><span class="p">.</span><span class="nx">BEPutUint64</span><span class="p">(</span><span class="nx">a16</span><span class="p">[:</span><span class="mi">8</span><span class="p">],</span><span class="w"> </span><span class="nx">ip</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">hi</span><span class="p">)</span>
<span class="w">    </span><span class="nx">byteorder</span><span class="p">.</span><span class="nx">BEPutUint64</span><span class="p">(</span><span class="nx">a16</span><span class="p">[</span><span class="mi">8</span><span class="p">:],</span><span class="w"> </span><span class="nx">ip</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">lo</span><span class="p">)</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">a16</span>
<span class="p">}</span>

<span class="kd">func</span><span class="w"> </span><span class="nx">AddrFrom16</span><span class="p">(</span><span class="nx">addr</span><span class="w"> </span><span class="p">[</span><span class="mi">16</span><span class="p">]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="nx">Addr</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">Addr</span><span class="p">{</span>
<span class="w">        </span><span class="nx">addr</span><span class="p">:</span><span class="w"> </span><span class="nx">uint128</span><span class="p">{</span>
<span class="w">            </span><span class="nx">byteorder</span><span class="p">.</span><span class="nx">BEUint64</span><span class="p">(</span><span class="nx">addr</span><span class="p">[:</span><span class="mi">8</span><span class="p">]),</span>
<span class="w">            </span><span class="nx">byteorder</span><span class="p">.</span><span class="nx">BEUint64</span><span class="p">(</span><span class="nx">addr</span><span class="p">[</span><span class="mi">8</span><span class="p">:]),</span>
<span class="w">        </span><span class="p">},</span>
<span class="w">        </span><span class="nx">z</span><span class="p">:</span><span class="w"> </span><span class="nx">z6noz</span><span class="p">,</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>We can already guess the pattern to optimize: the code packs the IP address into
an array, copies it, then unpacks it. If we inline the Go code by hand, we get:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">func</span><span class="w"> </span><span class="nx">AddrTo6</span><span class="p">(</span><span class="nx">input</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span><span class="p">)</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="o">!</span><span class="nx">input</span><span class="p">.</span><span class="nx">Is4</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="nx">input</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">a16</span><span class="w"> </span><span class="p">[</span><span class="mi">16</span><span class="p">]</span><span class="kt">byte</span>
<span class="w">    </span><span class="nx">byteorder</span><span class="p">.</span><span class="nx">BEPutUint64</span><span class="p">(</span><span class="nx">a16</span><span class="p">[:</span><span class="mi">8</span><span class="p">],</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">hi</span><span class="p">)</span>
<span class="w">    </span><span class="nx">byteorder</span><span class="p">.</span><span class="nx">BEPutUint64</span><span class="p">(</span><span class="nx">a16</span><span class="p">[</span><span class="mi">8</span><span class="p">:],</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">lo</span><span class="p">)</span>

<span class="w">    </span><span class="nx">addr</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">a16</span>

<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">output</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span>
<span class="w">    </span><span class="nx">output</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">hi</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">byteorder</span><span class="p">.</span><span class="nx">BEUint64</span><span class="p">(</span><span class="nx">addr</span><span class="p">[:</span><span class="mi">8</span><span class="p">])</span>
<span class="w">    </span><span class="nx">output</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">lo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">byteorder</span><span class="p">.</span><span class="nx">BEUint64</span><span class="p">(</span><span class="nx">addr</span><span class="p">[</span><span class="mi">8</span><span class="p">:])</span>
<span class="w">    </span><span class="nx">output</span><span class="p">.</span><span class="nx">z</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">z6noz</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">output</span>
<span class="p">}</span>
</code></pre></div>

<p>As humans, we can mentally derive the optimized form:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">func</span><span class="w"> </span><span class="nx">AddrTo6</span><span class="p">(</span><span class="nx">input</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span><span class="p">)</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="o">!</span><span class="nx">input</span><span class="p">.</span><span class="nx">Is4</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="nx">input</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">output</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span>
<span class="w">    </span><span class="nx">output</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">hi</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">hi</span>
<span class="w">    </span><span class="nx">output</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">lo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">addr</span><span class="p">.</span><span class="nx">lo</span>
<span class="w">    </span><span class="nx">output</span><span class="p">.</span><span class="nx">z</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">z6noz</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">output</span>
<span class="p">}</span>
</code></pre></div>

<p id="asm-addrto6safe">Unfortunately, as of Go 1.26.8, the compiler is not smart enough to do the same:</p>
<div class="language-go-assembly codehilite"><pre><span/><code><span class="c1">// AX = input.addr.hi, BX = input.addr.lo, CX = input.z</span>
<span class="c1">; Push the stack (32 bytes):</span>
<span class="c1">;    0(SP) addr netip.uint128</span>
<span class="c1">;   16(SP) a16 [16]byte</span>
<span class="w"> </span><span class="nf">PUSHQ</span><span class="w">   </span><span class="nv">BP</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="nv">SP</span><span class="p">,</span><span class="w"> </span><span class="nv">BP</span>
<span class="w"> </span><span class="nf">SUBQ</span><span class="w">    </span><span class="o">$</span><span class="mi">32</span><span class="p">,</span><span class="w"> </span><span class="nv">SP</span>

<span class="w"> </span><span class="nf">CMPQ</span><span class="w">    </span><span class="ss">net/netip·z4</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">  </span><span class="c1">; check "z" if this is an IPv4 address</span>
<span class="w"> </span><span class="nf">JNE</span><span class="w">     </span><span class="ss">end</span><span class="w">                   </span><span class="c1">; if not, stop here</span>

<span class="c1">; Pack: byteorder.BEPutUint64(a16[:8], input.addr.hi)</span>
<span class="c1">;       byteorder.BEPutUint64(a16[8:], input.addr.lo)</span>
<span class="w"> </span><span class="nf">MOVBEQ</span><span class="w">  </span><span class="nv">AX</span><span class="p">,</span><span class="w"> </span><span class="ss">net/netip·a16</span><span class="o">+</span><span class="mi">16</span><span class="p">(</span><span class="nv">SP</span><span class="p">)</span>
<span class="w"> </span><span class="nf">MOVBEQ</span><span class="w">  </span><span class="nv">BX</span><span class="p">,</span><span class="w"> </span><span class="ss">net/netip·a16</span><span class="o">+</span><span class="mi">24</span><span class="p">(</span><span class="nv">SP</span><span class="p">)</span>

<span class="c1">; addr = a16, 16 bytes at once through the vector register X0</span>
<span class="w"> </span><span class="nf">MOVUPS</span><span class="w">  </span><span class="ss">net/netip·a16</span><span class="o">+</span><span class="mi">16</span><span class="p">(</span><span class="nv">SP</span><span class="p">),</span><span class="w"> </span><span class="nv">X0</span>
<span class="w"> </span><span class="nf">MOVUPS</span><span class="w">  </span><span class="nv">X0</span><span class="p">,</span><span class="w"> </span><span class="ss">net/netip·addr</span><span class="p">(</span><span class="nv">SP</span><span class="p">)</span>

<span class="c1">; CX = netip.z6noz</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="ss">net/netip·z6noz</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span>
<span class="c1">; Unpack: output.addr.hi = byteorder.BEUint64(addr[:8])</span>
<span class="c1">;         output.addr.lo = byteorder.BEUint64(addr[8:])</span>
<span class="w"> </span><span class="nf">MOVBEQ</span><span class="w">  </span><span class="ss">net/netip·addr</span><span class="p">(</span><span class="nv">SP</span><span class="p">),</span><span class="w"> </span><span class="nv">AX</span>
<span class="w"> </span><span class="nf">MOVBEQ</span><span class="w">  </span><span class="ss">net/netip·addr</span><span class="o">+</span><span class="mi">8</span><span class="p">(</span><span class="nv">SP</span><span class="p">),</span><span class="w"> </span><span class="nv">BX</span>

<span class="nl">end</span><span class="p">:</span>
<span class="w"> </span><span class="nf">ADDQ</span><span class="w">    </span><span class="o">$</span><span class="mi">32</span><span class="p">,</span><span class="w"> </span><span class="nv">SP</span>
<span class="w"> </span><span class="nf">POPQ</span><span class="w">    </span><span class="nv">BP</span>
<span class="w"> </span><span class="nf">RET</span>
<span class="c1">// return value = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>The compiler does a decent job on the byte shuffling: the eight byte stores of
<code>BEPutUint64()</code> become a single <code>MOVBEQ</code>, which stores a register byte-swapped.
The eight byte loads of <code>BEUint64()</code> become a single <code>MOVBEQ</code> the other way
round.<sup id="fnref:amd64v3"><a class="footnote-ref" href="#fn:amd64v3">7</a></sup> Three groups of instructions remain: a pack, a copy, and an
unpack.</p>
<h1 id="hacking-the-go-compiler">Hacking the Go compiler</h1>
<p>The Go compiler has <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/README.md" title="Introduction to the Go compiler">several phases</a>:</p>
<dl>
<dt>Parsing</dt>
<dd>The compiler <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/syntax" title="cmd/compile/internal/syntax">tokenizes and parses the source code</a>. It builds a syntax tree
  for each source file.</dd>
<dt>Type checking</dt>
<dd>The compiler <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/types2" title="cmd/compile/internal/types2">maps each identifier to the object it denotes</a>, folds
  constants, and infers the type of every expression.</dd>
<dt><abbr title="Intermediate Representation">IR</abbr> construction</dt>
<dd>The compiler converts the syntax tree and its types into its own <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ir" title="cmd/compile/internal/ir">intermediate
  representation</a> (<abbr title="Intermediate Representation">IR</abbr>). This process, called “<a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/noder" title="cmd/compile/internal/noder">noding</a>,” goes through
  a serialization format named unified <abbr title="Intermediate Representation">IR</abbr>.</dd>
<dt>Middle end</dt>
<dd>The compiler performs several optimization passes on the <abbr title="Intermediate Representation">IR</abbr>,
  such as <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/devirtualize" title="cmd/compile/internal/devirtualize">devirtualization</a>, <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/inline" title="cmd/compile/internal/inline">function call inlining</a>,
  and <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/escape" title="cmd/compile/internal/escape">escape analysis</a>.</dd>
<dt>Walk</dt>
<dd>This phase runs <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/walk" title="cmd/compile/internal/walk">two steps</a>: order of evaluation decomposes complex
  statements into simpler ones, and desugaring transforms higher-level Go
  constructs, like <code>switch</code> or channels, into more primitive instructions or
  calls to the runtime.</dd>
<dt>Generic <abbr title="Static Single Assignment">SSA</abbr></dt>
<dd>The compiler <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssagen" title="cmd/compile/internal/ssagen">converts the <abbr title="Intermediate Representation">IR</abbr></a> into Static Single Assignment (<abbr title="Static Single Assignment">SSA</abbr>)
  form, a lower-level intermediate representation suited for
  <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssa" title="cmd/compile/internal/ssa">machine-independent optimizations and rewrite rules</a>.</dd>
<dt>Machine code generation</dt>
<dd>The compiler rewrites the <abbr title="Static Single Assignment">SSA</abbr> form into <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssa/_gen" title="cmd/compile/internal/ssa/_gen">machine-specific variants</a>,
  allocates registers, and applies more optimization passes. At the end, <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/internal/obj" title="cmd/internal/obj">the
  assembler</a> turns the generated instructions into machine code.</dd>
</dl>
<h2 id="the-hammer">The hammer</h2>
<p>My first idea is to replace occurrences of <code>netip.AddrFrom16(ip.As16())</code> with
<code>netip.Addr{addr: ip.addr, z: netip.z6noz}</code> as early as possible, during the
“noding” process. Before that, the type checking phase prevents
us from accessing unexported struct fields.</p>
<p>Go 1.27 introduced a convenient debug option to dump the <abbr title="Intermediate Representation">IR</abbr> of a function at
interesting points during compilation:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span><span class="nv">GOTOOLCHAIN</span><span class="o">=</span>go1.27.1<span class="w"> </span><span class="nv">GOAMD64</span><span class="o">=</span>v3<span class="w"> </span>go<span class="w"> </span>build<span class="w"> </span>-a<span class="w"> </span>-gcflags<span class="o">=</span><span class="s2">"-d=astdump=AddrTo6Safe"</span><span class="w"> </span>.
<span class="go">Writing text ast output for AddrTo6Safe to AddrTo6Safe.ast</span>
<span class="go">Writing html ast output for AddrTo6Safe to AddrTo6Safe.html</span>
<span class="go">Writing html syntax output for AddrTo6Safe to AddrTo6Safe.syntax.html</span>
</code></pre></div>

<p>In the HTML file, the first column shows the <abbr title="Intermediate Representation">IR</abbr> as it comes out of noding:</p>
<div class="language-go-compiler-ir codehilite"><pre><span/><code><span class="k">DCLFUNC</span><span class="w"> </span>addrto6.AddrTo6Safe<span class="w"> </span><span class="na">ABI</span><span class="p">:</span><span class="no">ABIInternal</span><span class="w"> </span><span class="kt">FUNC</span><span class="p">-</span>func(netip.Addr)<span class="w"> </span>netip.Addr
<span class="k">DCLFUNC-Dcl</span>
<span class="w">. </span><span class="k">NAME</span><span class="p">-</span>addrto6.ip<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PPARAM</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">OnStack</span><span class="w"> </span><span class="na">Used</span><span class="w"> </span>netip.Addr
<span class="w">. </span><span class="k">NAME</span><span class="p">-</span>addrto6.~r0<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PPARAMOUT</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">OnStack</span><span class="w"> </span>netip.Addr
<span class="k">DCLFUNC-body</span>
<span class="w">. </span><span class="k">IF</span><span class="w"> </span><span class="c1"># ipv6_safe.go:11:2</span>
<span class="w">. </span><span class="k">IF-Cond</span>
<span class="w">. . </span><span class="k">CALLFUNC</span><span class="w"> </span>bool
<span class="w">. . </span><span class="k">CALLFUNC-Fun</span>
<span class="w">. . . </span><span class="k">METHEXPR</span><span class="w"> </span>addrto6.Is4<span class="w"> </span><span class="kt">FUNC</span><span class="p">-</span>func(netip.Addr)<span class="w"> </span>bool
<span class="w">. . . . </span><span class="k">TYPE</span><span class="w"> </span>netip.Addr<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PEXTERN</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span>type<span class="w"> </span>netip.Addr
<span class="w">. . </span><span class="k">CALLFUNC-Args</span>
<span class="w">. . . </span><span class="k">NAME</span><span class="p">-</span>addrto6.ip<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PPARAM</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">OnStack</span><span class="w"> </span><span class="na">Used</span><span class="w"> </span>netip.Addr
<span class="w">. </span><span class="k">IF-Body</span>
<span class="w">. . </span><span class="k">AS</span><span class="w"> </span><span class="c1"># ipv6_safe.go:12:6</span>
<span class="w">. . . </span><span class="k">NAME</span><span class="p">-</span>addrto6.ip<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PPARAM</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">OnStack</span><span class="w"> </span><span class="na">Used</span><span class="w"> </span>netip.Addr
<span class="w">. . . </span><span class="k">CALLFUNC</span><span class="w"> </span>netip.Addr
<span class="w">. . . </span><span class="k">CALLFUNC-Fun</span>
<span class="hll"><span class="w">. . . . </span><span class="k">NAME</span><span class="p">-</span>netip.AddrFrom16<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PFUNC</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">Used</span><span class="w"> </span><span class="kt">FUNC</span><span class="p">-</span>func([<span class="mi">16</span>]byte)<span class="w"> </span>netip.Addr
</span><span class="w">. . . </span><span class="k">CALLFUNC-Args</span>
<span class="w">. . . . </span><span class="k">CALLFUNC</span><span class="w"> </span><span class="kt">ARRAY</span><span class="p">-</span>[<span class="mi">16</span>]byte
<span class="w">. . . . </span><span class="k">CALLFUNC-Fun</span>
<span class="hll"><span class="w">. . . . . </span><span class="k">METHEXPR</span><span class="w"> </span>addrto6.As16<span class="w"> </span><span class="kt">FUNC</span><span class="p">-</span>func(netip.Addr)<span class="w"> </span>[<span class="mi">16</span>]byte
</span><span class="w">. . . . . . </span><span class="k">TYPE</span><span class="w"> </span>netip.Addr<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PEXTERN</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span>type<span class="w"> </span>netip.Addr
<span class="w">. . . . </span><span class="k">CALLFUNC-Args</span>
<span class="w">. . . . . </span><span class="k">NAME</span><span class="p">-</span>addrto6.ip<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PPARAM</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">OnStack</span><span class="w"> </span><span class="na">Used</span><span class="w"> </span>netip.Addr
<span class="w">. </span><span class="k">RETURN</span><span class="w"> </span><span class="c1"># ipv6_safe.go:14:2</span>
<span class="w">. </span><span class="k">RETURN-Results</span>
<span class="w">. . </span><span class="k">NAME</span><span class="p">-</span>addrto6.ip<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PPARAM</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">OnStack</span><span class="w"> </span><span class="na">Used</span><span class="w"> </span>netip.Addr
</code></pre></div>

<p>In the body of the <code>if</code> statement, we spot the calls to the method
<code>netip.Addr.As16()</code> and to the function <code>netip.AddrFrom16()</code>. Our goal is to
patch them with a struct literal:</p>
<div class="language-go-compiler-ir codehilite"><pre><span/><code><span class="k">IF-Body</span>
<span class="w">. </span><span class="k">AS</span><span class="w"> </span><span class="c1"># ipv6_safe.go:12:6</span>
<span class="w">. . </span><span class="k">NAME</span><span class="p">-</span>addrto6.ip<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PPARAM</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">OnStack</span><span class="w"> </span><span class="na">Used</span><span class="w"> </span>netip.Addr
<span class="w">. . </span><span class="k">STRUCTLIT</span><span class="w"> </span>netip.Addr
<span class="w">. . </span><span class="k">STRUCTLIT-List</span>
<span class="w">. . . </span><span class="k">STRUCTKEY</span><span class="w"> </span>netip.addr
<span class="w">. . . . </span><span class="k">DOT</span><span class="w"> </span>netip.addr<span class="w"> </span>netip.uint128
<span class="w">. . . . . </span><span class="k">NAME</span><span class="p">-</span>addrto6.ip<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PPARAM</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span><span class="na">OnStack</span><span class="w"> </span><span class="na">Used</span><span class="w"> </span>netip.Addr
<span class="w">. . . </span><span class="k">STRUCTKEY</span><span class="w"> </span>netip.z
<span class="w">. . . . </span><span class="k">NAME</span><span class="p">-</span>netip.z6noz<span class="w"> </span><span class="na">Class</span><span class="p">:</span><span class="no">PEXTERN</span><span class="w"> </span><span class="na">Offset</span><span class="p">:</span><span class="no">0</span><span class="w"> </span>unique.Handle[net/netip.addrDetail]
</code></pre></div>

<p>In <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/noder/reader.go" title="reader.go">noder’s <code>reader.go</code></a>, the <code>expr()</code> method builds the <abbr title="Intermediate Representation">IR</abbr> tree for
an expression. At the end of the <code>exprCall</code> case, we add a call to a
<code>rewriteAddrFrom16As16()</code> function. It takes the current node and returns the
struct literal on success, or <code>nil</code> if the rewrite is not possible. First, we
check that we have the expected pattern: a call to the <code>netip.AddrFrom16()</code>
function with a call to the <code>netip.Addr.As16()</code> method as its only argument:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">func</span><span class="w"> </span><span class="nx">rewriteAddrFrom16As16</span><span class="p">(</span><span class="nx">n</span><span class="w"> </span><span class="nx">ir</span><span class="p">.</span><span class="nx">Node</span><span class="p">)</span><span class="w"> </span><span class="nx">ir</span><span class="p">.</span><span class="nx">Node</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">call</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">n</span><span class="p">.(</span><span class="o">*</span><span class="nx">ir</span><span class="p">.</span><span class="nx">CallExpr</span><span class="p">)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="o">!</span><span class="nx">ok</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">call</span><span class="p">.</span><span class="nx">Op</span><span class="p">()</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nx">ir</span><span class="p">.</span><span class="nx">OCALLFUNC</span><span class="w"> </span><span class="o">||</span>
<span class="w">        </span><span class="nb">len</span><span class="p">(</span><span class="nx">call</span><span class="p">.</span><span class="nx">Args</span><span class="p">)</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">call</span><span class="p">.</span><span class="nx">Init</span><span class="p">())</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="o">||</span>
<span class="w">        </span><span class="o">!</span><span class="nx">isNetipFunc</span><span class="p">(</span><span class="nx">call</span><span class="p">.</span><span class="nx">Fun</span><span class="p">,</span><span class="w"> </span><span class="s">"AddrFrom16"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="nx">inner</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">call</span><span class="p">.</span><span class="nx">Args</span><span class="p">[</span><span class="mi">0</span><span class="p">].(</span><span class="o">*</span><span class="nx">ir</span><span class="p">.</span><span class="nx">CallExpr</span><span class="p">)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="o">!</span><span class="nx">ok</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">inner</span><span class="p">.</span><span class="nx">Op</span><span class="p">()</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nx">ir</span><span class="p">.</span><span class="nx">OCALLFUNC</span><span class="w"> </span><span class="o">||</span>
<span class="w">        </span><span class="nb">len</span><span class="p">(</span><span class="nx">inner</span><span class="p">.</span><span class="nx">Args</span><span class="p">)</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">inner</span><span class="p">.</span><span class="nx">Init</span><span class="p">())</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="o">||</span>
<span class="w">        </span><span class="o">!</span><span class="nx">isNetipFunc</span><span class="p">(</span><span class="nx">inner</span><span class="p">.</span><span class="nx">Fun</span><span class="p">,</span><span class="w"> </span><span class="s">"Addr.As16"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="nx">x</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">inner</span><span class="p">.</span><span class="nx">Args</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="w">    </span><span class="c1">// [...]</span>
<span class="p">}</span>
</code></pre></div>

<p>Then, we fetch <code>netip.z6noz</code>:</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">z6noz</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">lookupVar</span><span class="p">(</span><span class="nx">ir</span><span class="p">.</span><span class="nx">StaticCalleeName</span><span class="p">(</span><span class="nx">call</span><span class="p">.</span><span class="nx">Fun</span><span class="p">).</span><span class="nx">Sym</span><span class="p">().</span><span class="nx">Pkg</span><span class="p">,</span><span class="w"> </span><span class="s">"z6noz"</span><span class="p">)</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span>
<span class="p">}</span>
</code></pre></div>

<p>And we build the struct literal:</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">typ</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">call</span><span class="p">.</span><span class="nx">Type</span><span class="p">()</span>
<span class="nx">pos</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">call</span><span class="p">.</span><span class="nx">Pos</span><span class="p">()</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="p">[]</span><span class="nx">ir</span><span class="p">.</span><span class="nx">Node</span>
<span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">f</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">typ</span><span class="p">.</span><span class="nx">Fields</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nx">value</span><span class="w"> </span><span class="nx">ir</span><span class="p">.</span><span class="nx">Node</span>
<span class="w">    </span><span class="k">switch</span><span class="w"> </span><span class="nx">f</span><span class="p">.</span><span class="nx">Sym</span><span class="p">.</span><span class="nx">Name</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">case</span><span class="w"> </span><span class="s">"addr"</span><span class="p">:</span>
<span class="w">        </span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">typecheck</span><span class="p">.</span><span class="nx">DotField</span><span class="p">(</span><span class="nx">pos</span><span class="p">,</span><span class="w"> </span><span class="nx">x</span><span class="p">,</span><span class="w"> </span><span class="nx">i</span><span class="p">)</span>
<span class="w">    </span><span class="k">case</span><span class="w"> </span><span class="s">"z"</span><span class="p">:</span>
<span class="w">        </span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">z6noz</span>
<span class="w">    </span><span class="k">default</span><span class="p">:</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="nx">list</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">list</span><span class="p">,</span><span class="w"> </span><span class="nx">ir</span><span class="p">.</span><span class="nx">NewStructKeyExpr</span><span class="p">(</span><span class="nx">pos</span><span class="p">,</span><span class="w"> </span><span class="nx">f</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">))</span>
<span class="p">}</span>
<span class="nx">lit</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">ir</span><span class="p">.</span><span class="nx">NewCompLitExpr</span><span class="p">(</span><span class="nx">pos</span><span class="p">,</span><span class="w"> </span><span class="nx">ir</span><span class="p">.</span><span class="nx">OSTRUCTLIT</span><span class="p">,</span><span class="w"> </span><span class="nx">typ</span><span class="p">,</span><span class="w"> </span><span class="nx">list</span><span class="p">)</span>
<span class="nx">lit</span><span class="p">.</span><span class="nx">SetTypecheck</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">return</span><span class="w"> </span><span class="nx">lit</span>
</code></pre></div>

<p>Have a look at the <a href="https://github.com/vincentbernat/go/commit/netipmap-fix2" title="cmd/compile: optimize netip.AddrFrom16(ip.As16()) in the noder">complete patch</a>.<sup id="fnref:statement"><a class="footnote-ref" href="#fn:statement">8</a></sup> We can test it
with the following commands:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span><span class="nb">cd</span><span class="w"> </span>src
<span class="gp">$ </span>./make.bash
<span class="go">Building Go cmd/dist using /usr/lib/go-1.27. (go1.27.1 linux/amd64)</span>
<span class="go">Building Go toolchain1 and bootstrap cmd/go (go_bootstrap) using /usr/lib/go-1.27.</span>
<span class="go">Building Go toolchain2 using go_bootstrap and Go toolchain1.</span>
<span class="go">Building Go toolchain3 and commands using go_bootstrap and Go toolchain2.</span>
<span class="go">Checking command staleness for linux/amd64.</span>
<span class="go">---</span>
<span class="go">Installed Go for linux/amd64 in /home/bernat/code/free/go</span>
<span class="go">Installed commands in /home/bernat/code/free/go/bin</span>
<span class="go">*** You need to add /home/bernat/code/free/go/bin to your PATH.</span>
<span class="gp">$ </span><span class="nb">export</span><span class="w"> </span><span class="nv">PATH</span><span class="o">=</span><span class="nv">$PWD</span>/../bin:<span class="nv">$PATH</span>
<span class="gp">$ </span>go<span class="w"> </span>version
<span class="go">go version go1.28-devel_9834516e20 Sat Sep 12 08:23:11 2026 -0700 linux/amd64</span>
<span class="gp">$ </span>go<span class="w"> </span><span class="nb">test</span><span class="w"> </span>net/netip/...
<span class="go">ok      net/netip   0.224s</span>
<span class="gp">$ </span><span class="nb">cd</span><span class="w"> </span>../../go-netip-addrto6
<span class="gp">$ </span>go<span class="w"> </span><span class="nb">test</span><span class="w"> </span>.
<span class="go">ok      github.com/vincentbernat/go-netip-addrto6   0.062s</span>
</code></pre></div>

<p>The generated code for the helper is now the shortest possible version!</p>
<div class="language-go-assembly codehilite"><pre><span/><code><span class="c1">// AX = input.addr.hi, BX = input.addr.lo, CX = input.z</span>
<span class="w"> </span><span class="nf">CMPQ</span><span class="w">    </span><span class="ss">net/netip·z4</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">     </span><span class="c1">; check "z" if this is an IPv4 address</span>
<span class="w"> </span><span class="nf">JNE</span><span class="w">     </span><span class="ss">end</span><span class="w">                      </span><span class="c1">; if not, stop here</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="ss">net/netip·z6noz</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">  </span><span class="c1">; CX = netip.z6noz</span>
<span class="nl">end</span><span class="p">:</span>
<span class="w"> </span><span class="nf">RET</span>
<span class="c1">// return value = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>Go maintainers are unlikely to accept this change. It relies on the internal
structure of <code>net/netip.Addr</code>. It’s an ugly hack in the noder, whose job is to
faithfully translate the type-checked AST into the <abbr title="Intermediate Representation">IR</abbr>. And it’s harder to
maintain than adding a <code>To6()</code> method.</p>
<h2 id="the-screwdriver">The screwdriver</h2>
<p>The right place for such an optimization is the <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssa/README.md" title="Introduction to the Go compiler's SSA backend">generic <abbr title="Static Single Assignment">SSA</abbr> phase</a>. One of
the last machine-independent passes is <code>memcombine</code>. With the appropriate debug
flag, the compiler dumps the <abbr title="Static Single Assignment">SSA</abbr> form after this pass:<sup id="fnref:html"><a class="footnote-ref" href="#fn:html">9</a></sup></p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span><span class="nv">GOTOOLCHAIN</span><span class="o">=</span>go1.26.8<span class="w"> </span><span class="nv">GOAMD64</span><span class="o">=</span>v3<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span>go<span class="w"> </span>build<span class="w"> </span>-a<span class="w"> </span>-gcflags<span class="o">=</span><span class="s1">'-d=ssa/memcombine/dump=AddrTo6Safe'</span><span class="w"> </span>.
<span class="gp">$ </span>head<span class="w"> </span>-5<span class="w"> </span>AddrTo6Safe_01__memcombine.dump
<span class="go">AddrTo6Safe func(netip.Addr) netip.Addr</span>
<span class="go">  b2:</span>
<span class="go">    (?) v1 = InitMem &lt;mem&gt;</span>
<span class="go">    (?) v2 = SP &lt;uintptr&gt;</span>
<span class="go">    (?) v3 = SB &lt;uintptr&gt;</span>
</code></pre></div>

<p>The result of the <code>memcombine</code> pass follows the same structure as the assembly
code for <code>AddrTo6Safe()</code> <a href="#asm-addrto6safe">we looked at earlier</a>: two stores,
one move, and two loads we would like to optimize away.</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="c1">; […]</span>
<span class="w">  </span><span class="nv">v502</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+0}</span><span class="w"> </span><span class="mi">[0]</span><span class="w">              </span><span class="c1">; input.addr.hi</span>
<span class="w">  </span><span class="nv">v490</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+8}</span><span class="w"> </span><span class="mi">[1]</span><span class="w">              </span><span class="c1">; input.addr.lo</span>
<span class="w">  </span><span class="nv">v466</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;*netip.addrDetail&gt;</span><span class="w"> </span><span class="na">{ip+16}</span><span class="w"> </span><span class="mi">[2]</span><span class="w">  </span><span class="c1">; input.z</span>
<span class="c1">; […]</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v442</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v490</span><span class="w">                      </span><span class="c1">; bswap(input.addr.lo)</span>
<span class="w">  </span><span class="nv">v542</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v502</span><span class="w">                      </span><span class="c1">; bswap(input.addr.hi)</span>
<span class="w">  </span><span class="nv">v161</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v542</span><span class="w"> </span><span class="nv">v23</span><span class="w">          </span><span class="c1">; a16[:8] = bswap(hi)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>

<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>

<span class="w">  </span><span class="nv">v415</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v285</span><span class="w">                    </span><span class="c1">; &amp;addr[8]</span>
<span class="w">  </span><span class="nv">v416</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v286</span><span class="w">                    </span><span class="c1">; addr[:8]</span>
<span class="w">  </span><span class="nv">v299</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v416</span><span class="w">                      </span><span class="c1">; output.addr.hi</span>
<span class="w">  </span><span class="nv">v174</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v415</span><span class="w"> </span><span class="nv">v286</span><span class="w">                    </span><span class="c1">; addr[8:]</span>
<span class="w">  </span><span class="nv">v39</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v174</span><span class="w">                      </span><span class="c1">; output.addr.lo</span>
<span class="c1">; […]</span>
</code></pre></div>

<p>Each line features a value identifier (<code>v442</code>), an operation with its type
(<code>Bswap64 &lt;uint64&gt;</code>), and its arguments (<code>v490</code>).<sup id="fnref:lines"><a class="footnote-ref" href="#fn:lines">10</a></sup> Values are the basic
building blocks of <abbr title="Static Single Assignment">SSA</abbr> and are defined exactly once. Square brackets enclose
integer parameters (<code>[8]</code>) and curly braces contain auxiliary arguments
(<code>{netip.addr}</code>). Operations writing to memory produce a new memory state. Every
memory operation takes the current state as its last argument, which keeps them
in order.</p>
<h3 id="on-paper">On paper</h3>
<p>Let’s focus on <code>output.addr.lo</code>, aka <code>v39</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v490</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+8}</span><span class="w"> </span><span class="mi">[1]</span><span class="w">              </span><span class="c1">; input.addr.lo</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v442</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v490</span><span class="w">                      </span><span class="c1">; bswap(input.addr.lo)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="w">  </span><span class="nv">v415</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v285</span><span class="w">                    </span><span class="c1">; &amp;addr[8]</span>
<span class="w">  </span><span class="nv">v174</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v415</span><span class="w"> </span><span class="nv">v286</span><span class="w">                    </span><span class="c1">; addr[8:]</span>
<span class="w">  </span><span class="nv">v39</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v174</span><span class="w">                      </span><span class="c1">; output.addr.lo</span>
</code></pre></div>

<p>To simplify this code, we could apply three rewriting rules:</p>
<ol>
<li>
<p>The first one adds a shortcut when <strong>loading through a move</strong>: <code>(Load (OffPtr
   [o] p) (Move p src mem)) =&gt; (Load (OffPtr [o] src) mem)</code>. This matches <code>v174</code>
   with its arguments <code>v415</code> and <code>v286</code> and creates a new value <code>v600</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v490</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+8}</span><span class="w"> </span><span class="mi">[1]</span><span class="w">              </span><span class="c1">; input.addr.lo</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v442</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v490</span><span class="w">                      </span><span class="c1">; bswap(input.addr.lo)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="w">  </span><span class="nv">v415</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v285</span><span class="w">                    </span><span class="c1">; &amp;addr[8]</span>
<span class="hll"><span class="w">  </span><span class="nv">v600</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
</span><span class="hll"><span class="w">  </span><span class="nv">v174</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v600</span><span class="w"> </span><span class="nv">v282</span><span class="w">                    </span><span class="c1">; a16[8:]</span>
</span><span class="w">  </span><span class="nv">v39</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v174</span><span class="w">                      </span><span class="c1">; output.addr.lo</span>
</code></pre></div>

</li>
<li>
<p>The second one simplifies a <strong>load following a store</strong>: <code>(Load p (Store p x
   _)) =&gt; x</code>. The load is <em>forwarded</em>: the stored value replaces it and no memory
   access remains. It matches <code>v174</code>. It notices that <code>v600</code> and <code>v173</code> are the
   same address and replaces <code>v174</code> with a copy of <code>v442</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v490</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+8}</span><span class="w"> </span><span class="mi">[1]</span><span class="w">              </span><span class="c1">; input.addr.lo</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v442</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v490</span><span class="w">                      </span><span class="c1">; bswap(input.addr.lo)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="w">  </span><span class="nv">v415</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v285</span><span class="w">                    </span><span class="c1">; &amp;addr[8]</span>
<span class="w">  </span><span class="nv">v600</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="hll"><span class="w">  </span><span class="nv">v174</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v442</span><span class="w">                         </span><span class="c1">; bswap(input.addr.lo)</span>
</span><span class="w">  </span><span class="nv">v39</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v174</span><span class="w">                      </span><span class="c1">; output.addr.lo</span>
</code></pre></div>

</li>
<li>
<p>The last step <strong>cancels the two byte swaps</strong>: <code>(Bswap64 (Bswap64 x)) =&gt; x</code>.
   <code>v39</code> becomes a copy of <code>v490</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v490</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+8}</span><span class="w"> </span><span class="mi">[1]</span><span class="w">              </span><span class="c1">; input.addr.lo</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v442</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v490</span><span class="w">                      </span><span class="c1">; bswap(input.addr.lo)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="w">  </span><span class="nv">v415</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v285</span><span class="w">                    </span><span class="c1">; &amp;addr[8]</span>
<span class="w">  </span><span class="nv">v600</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v174</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v442</span><span class="w">                         </span><span class="c1">; bswap(input.addr.lo)</span>
<span class="hll"><span class="w">  </span><span class="nv">v39</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v490</span><span class="w">                         </span><span class="c1">; output.addr.lo = input.addr.lo</span>
</span></code></pre></div>

</li>
</ol>
<p>If we ignore the values not needed to compute <code>v39</code>, only this <abbr title="Static Single Assignment">SSA</abbr> form remains:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v490</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+8}</span><span class="w"> </span><span class="mi">[1]</span><span class="w">  </span><span class="c1">; input.addr.lo</span>
<span class="w">  </span><span class="nv">v39</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v490</span><span class="w">             </span><span class="c1">; output.addr.lo = input.addr.lo</span>
</code></pre></div>

<p>Let’s switch to <code>output.addr.hi</code>, aka <code>v299</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v502</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+0}</span><span class="w"> </span><span class="mi">[0]</span><span class="w">              </span><span class="c1">; input.addr.hi</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v542</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v502</span><span class="w">                      </span><span class="c1">; bswap(input.addr.hi)</span>
<span class="w">  </span><span class="nv">v161</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v542</span><span class="w"> </span><span class="nv">v23</span><span class="w">          </span><span class="c1">; a16[:8] = bswap(hi)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="w">  </span><span class="nv">v416</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v286</span><span class="w">                    </span><span class="c1">; addr[:8]</span>
<span class="w">  </span><span class="nv">v299</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v416</span><span class="w">                      </span><span class="c1">; output.addr.hi</span>
</code></pre></div>

<p>To optimize it away, we also apply three rewriting rules:</p>
<ol>
<li>
<p>The first one also adds a shortcut when <strong>loading through a move</strong>, but
   without an offset: <code>(Load p (Move p src mem)) =&gt; (Load src mem)</code>. This rewrites <code>v416</code>
   to use arguments from <code>v286</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v502</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+0}</span><span class="w"> </span><span class="mi">[0]</span><span class="w">              </span><span class="c1">; input.addr.hi</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v542</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v502</span><span class="w">                      </span><span class="c1">; bswap(input.addr.hi)</span>
<span class="w">  </span><span class="nv">v161</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v542</span><span class="w"> </span><span class="nv">v23</span><span class="w">          </span><span class="c1">; a16[:8] = bswap(hi)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="hll"><span class="w">  </span><span class="nv">v416</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">                     </span><span class="c1">; a16[:8]</span>
</span><span class="w">  </span><span class="nv">v299</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v416</span><span class="w">                      </span><span class="c1">; output.addr.hi</span>
</code></pre></div>

</li>
<li>
<p>The second rule <strong>forwards a value stored one step earlier</strong>, skipping over a
   store to another address: <code>(Load p (Store q _ (Store p x _))) =&gt; x</code>. This
   matches <code>v416</code>: <code>x</code> is <code>v542</code>, <code>p</code> is <code>v22</code> (<code>&amp;a16</code>), <code>q</code> is <code>v173</code>
   (<code>&amp;a16[8]</code>), and <code>p</code> and <code>q</code> do not overlap for <code>uint64</code>.</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v502</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+0}</span><span class="w"> </span><span class="mi">[0]</span><span class="w">              </span><span class="c1">; input.addr.hi</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v542</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v502</span><span class="w">                      </span><span class="c1">; bswap(input.addr.hi)</span>
<span class="w">  </span><span class="nv">v161</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v542</span><span class="w"> </span><span class="nv">v23</span><span class="w">          </span><span class="c1">; a16[:8] = bswap(hi)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="hll"><span class="w">  </span><span class="nv">v416</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v542</span><span class="w">                         </span><span class="c1">; bswap(input.addr.hi)</span>
</span><span class="w">  </span><span class="nv">v299</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v416</span><span class="w">                      </span><span class="c1">; output.addr.hi</span>
</code></pre></div>

</li>
<li>
<p>The third rule <strong>cancels two byte swaps</strong>: <code>(Bswap64 (Bswap64 x)) =&gt; x</code>.
   <code>v299</code> becomes a copy of <code>v502</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v502</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+0}</span><span class="w"> </span><span class="mi">[0]</span><span class="w">              </span><span class="c1">; input.addr.hi</span>
<span class="w">  </span><span class="nv">v22</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.a16}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v1</span><span class="w">     </span><span class="c1">; &amp;a16</span>
<span class="w">  </span><span class="nv">v173</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[8]</span><span class="w"> </span><span class="nv">v22</span><span class="w">                     </span><span class="c1">; &amp;a16[8]</span>
<span class="w">  </span><span class="nv">v542</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v502</span><span class="w">                      </span><span class="c1">; bswap(input.addr.hi)</span>
<span class="w">  </span><span class="nv">v161</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v542</span><span class="w"> </span><span class="nv">v23</span><span class="w">          </span><span class="c1">; a16[:8] = bswap(hi)</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{uint64}</span><span class="w"> </span><span class="nv">v173</span><span class="w"> </span><span class="nv">v442</span><span class="w"> </span><span class="nv">v161</span><span class="w">        </span><span class="c1">; a16[8:] = bswap(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="w">  </span><span class="nv">v416</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v542</span><span class="w">                         </span><span class="c1">; bswap(input.addr.hi)</span>
<span class="hll"><span class="w">  </span><span class="nv">v299</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v502</span><span class="w">                         </span><span class="c1">; output.addr.hi = input.addr.hi</span>
</span></code></pre></div>

</li>
</ol>
<p>If we remove the values not used to compute <code>v299</code>, we get this <abbr title="Static Single Assignment">SSA</abbr> form:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v502</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ArgIntReg</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="na">{ip+0}</span><span class="w"> </span><span class="mi">[0]</span><span class="w"> </span><span class="c1">; input.addr.hi</span>
<span class="w">  </span><span class="nv">v299</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v502</span><span class="w">            </span><span class="c1">; output.addr.hi = input.addr.hi</span>
</code></pre></div>

<h3 id="in-practice">In practice</h3>
<p>Most of these rules already exist in <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssa/_gen/generic.rules" title="generic.rules"><code>generic.rules</code></a>. They use
conditions to validate their context: <code>ssa.IsSamePtr()</code> for the same address,
<code>ssa.Disjoint()</code> for addresses that do not overlap. The rule forwarding a stored
value to a load already exists with three variants looking through several other
stores. Here are the two we need:</p>
<div class="language-go-compiler-ssa-rules codehilite"><pre><span/><code><span class="p">(</span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;t1&gt;</span><span class="w"> </span>p1<span class="w"> </span><span class="p">(</span><span class="k">Store</span><span class="w"> </span><span class="na">{t2}</span><span class="w"> </span>p2<span class="w"> </span>x<span class="w"> </span>_<span class="p">))</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">ssa.IsSamePtr</span><span class="p">(</span>p1,<span class="w"> </span>p2<span class="p">)</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">copyCompatibleType</span><span class="p">(</span>t1,<span class="w"> </span>x.Type<span class="p">)</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">t1.Size</span><span class="p">()</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nf">t2.Size</span><span class="p">()</span>
<span class="w">    </span><span class="o">=&gt;</span><span class="w"> </span>x
<span class="p">(</span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;t1&gt;</span><span class="w"> </span>p1<span class="w"> </span><span class="p">(</span><span class="k">Store</span><span class="w"> </span><span class="na">{t2}</span><span class="w"> </span>p2<span class="w"> </span>_<span class="w"> </span><span class="p">(</span><span class="k">Store</span><span class="w"> </span><span class="na">{t3}</span><span class="w"> </span>p3<span class="w"> </span>x<span class="w"> </span>_<span class="p">)))</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">ssa.IsSamePtr</span><span class="p">(</span>p1,<span class="w"> </span>p3<span class="p">)</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">copyCompatibleType</span><span class="p">(</span>t1,<span class="w"> </span>x.Type<span class="p">)</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">t1.Size</span><span class="p">()</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nf">t3.Size</span><span class="p">()</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">ssa.Disjoint</span><span class="p">(</span>p3,<span class="w"> </span>t3,<span class="w"> </span>p2,<span class="w"> </span>t2<span class="p">)</span>
<span class="w">    </span><span class="o">=&gt;</span><span class="w"> </span>x
</code></pre></div>

<p>Go 1.27 added the rule loading through a move with <a href="https://go-review.googlesource.com/c/go/+/748200" title="cmd/compile: forward small Load through Move to avoid redundant copies">CL 748200</a> to fix
<a href="https://github.com/golang/go/issues/77720" title="cmd/compile: field access on struct-returning method copies entire struct">issue #77720</a>:</p>
<div class="language-go-compiler-ssa-rules codehilite"><pre><span/><code><span class="p">(</span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;t1&gt;</span><span class="w"> </span>op1:<span class="p">(</span><span class="k">OffPtr</span><span class="w"> </span><span class="mi">[o1]</span><span class="w"> </span>p1<span class="p">)</span><span class="w"> </span>move:<span class="p">(</span><span class="k">Move</span><span class="w"> </span><span class="mi">[n]</span><span class="w"> </span>p2<span class="w"> </span>src<span class="w"> </span>mem<span class="p">))</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span>o1<span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span>o1<span class="o">+</span><span class="nf">t1.Size</span><span class="p">()</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span>n<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">ssa.IsSamePtr</span><span class="p">(</span>p1,<span class="w"> </span>p2<span class="p">)</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="nf">ssa.IsVolatile</span><span class="p">(</span>src<span class="p">)</span>
<span class="w">    </span><span class="o">=&gt;</span><span class="w"> </span><span class="nd">@move.Block</span><span class="w"> </span><span class="p">(</span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;t1&gt;</span><span class="w"> </span><span class="p">(</span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;op1.Type&gt;</span><span class="w"> </span><span class="mi">[o1]</span><span class="w"> </span>src<span class="p">)</span><span class="w"> </span>mem<span class="p">)</span>
</code></pre></div>

<p>It lacks a variant without an offset:</p>
<div class="language-go-compiler-ssa-rules codehilite"><pre><span/><code><span class="p">(</span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;t1&gt;</span><span class="w"> </span>p1<span class="w"> </span>move:<span class="p">(</span><span class="k">Move</span><span class="w"> </span><span class="mi">[n]</span><span class="w"> </span>p2<span class="w"> </span>src<span class="w"> </span>mem<span class="p">))</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span>p1.Op<span class="w"> </span><span class="o">!=</span><span class="w"> </span>ssaop.OpOffPtr
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">t1.Size</span><span class="p">()</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span>n<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">ssa.IsSamePtr</span><span class="p">(</span>p1,<span class="w"> </span>p2<span class="p">)</span>
<span class="w">    </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="nf">ssa.IsVolatile</span><span class="p">(</span>src<span class="p">)</span>
<span class="w">    </span><span class="o">=&gt;</span><span class="w"> </span><span class="nd">@move.Block</span><span class="w"> </span><span class="p">(</span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;t1&gt;</span><span class="w"> </span><span class="p">(</span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;p1.Type&gt;</span><span class="w"> </span><span class="mi">[0]</span><span class="w"> </span>src<span class="p">)</span><span class="w"> </span>mem<span class="p">)</span>
</code></pre></div>

<p>There is no generic rule to cancel two byte swaps, but the <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssa/_gen/AMD64.rules" title="AMD64.rules">AMD64 lowering
pass</a> includes this rule:</p>
<div class="language-go-compiler-ssa-rules codehilite"><pre><span/><code><span class="p">(</span><span class="k">BSWAP(Q|L)</span><span class="w"> </span><span class="p">(</span><span class="k">BSWAP(Q|L)</span><span class="w"> </span>p<span class="p">))</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span>p
</code></pre></div>

<p>After switching to Go’s development branch and <a href="https://github.com/vincentbernat/go/commit/netipmap-fix3" title="cmd/compile/internal/ssa: forward small offset-less Load through Move">adding the missing
rule</a>, the generated assembly code is worse than with Go 1.26.8,
even though our additional rule slightly improves the situation at the end:</p>
<div class="language-go-assembly codehilite"><pre><span/><code><span class="c1">// AX = input.addr.hi, BX = input.addr.lo, CX = input.z</span>
<span class="c1">; Push the stack (16 bytes):</span>
<span class="c1">;    0(SP) a16 [16]byte</span>
<span class="w"> </span><span class="nf">PUSHQ</span><span class="w">   </span><span class="nv">BP</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="nv">SP</span><span class="p">,</span><span class="w"> </span><span class="nv">BP</span>
<span class="w"> </span><span class="nf">SUBQ</span><span class="w">    </span><span class="o">$</span><span class="mi">16</span><span class="p">,</span><span class="w"> </span><span class="nv">SP</span>

<span class="w"> </span><span class="nf">CMPQ</span><span class="w">    </span><span class="ss">net/netip·z4</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">       </span><span class="c1">; check "z" if this is an IPv4 address</span>
<span class="w"> </span><span class="nf">JNE</span><span class="w">     </span><span class="ss">end</span><span class="w">                        </span><span class="c1">; if not, stop here</span>

<span class="c1">; The four forwarded bytes: the low half of input.addr.lo is taken apart and</span>
<span class="c1">; put back together in registers</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="nv">BX</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span><span class="w">                     </span><span class="c1">; DX = input.addr.lo</span>
<span class="w"> </span><span class="nf">SHRQ</span><span class="w">    </span><span class="o">$</span><span class="mi">24</span><span class="p">,</span><span class="w"> </span><span class="nv">BX</span><span class="w">                    </span><span class="c1">; BX = input.addr.lo &gt;&gt; 24</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="nv">DX</span><span class="p">,</span><span class="w"> </span><span class="nv">SI</span><span class="w">                     </span><span class="c1">; SI = input.addr.lo</span>
<span class="w"> </span><span class="nf">SHRQ</span><span class="w">    </span><span class="o">$</span><span class="mi">16</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span><span class="w">                    </span><span class="c1">; DX = input.addr.lo &gt;&gt; 16</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="nv">SI</span><span class="p">,</span><span class="w"> </span><span class="nv">DI</span><span class="w">                     </span><span class="c1">; DI = input.addr.lo, kept for the pack</span>
<span class="w"> </span><span class="nf">SHRQ</span><span class="w">    </span><span class="o">$</span><span class="mi">8</span><span class="p">,</span><span class="w"> </span><span class="nv">SI</span><span class="w">                     </span><span class="c1">; SI = input.addr.lo &gt;&gt; 8</span>
<span class="w"> </span><span class="nf">MOVBLZX</span><span class="w"> </span><span class="ss">DIB</span><span class="p">,</span><span class="w"> </span><span class="nv">R8</span><span class="w">                    </span><span class="c1">; R8 = byte(input.addr.lo)</span>
<span class="w"> </span><span class="nf">MOVBLZX</span><span class="w"> </span><span class="ss">SIB</span><span class="p">,</span><span class="w"> </span><span class="nv">SI</span><span class="w">                    </span><span class="c1">; SI = byte(input.addr.lo &gt;&gt; 8)</span>
<span class="w"> </span><span class="nf">SHLQ</span><span class="w">    </span><span class="o">$</span><span class="mi">8</span><span class="p">,</span><span class="w"> </span><span class="nv">SI</span>
<span class="w"> </span><span class="nf">ORQ</span><span class="w">     </span><span class="nv">R8</span><span class="p">,</span><span class="w"> </span><span class="nv">SI</span><span class="w">                     </span><span class="c1">; SI = two low bytes of input.addr.lo</span>
<span class="w"> </span><span class="nf">MOVBLZX</span><span class="w"> </span><span class="nv">DL</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span><span class="w">                     </span><span class="c1">; DX = byte(input.addr.lo &gt;&gt; 16)</span>
<span class="w"> </span><span class="nf">SHLQ</span><span class="w">    </span><span class="o">$</span><span class="mi">16</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span>
<span class="w"> </span><span class="nf">ORQ</span><span class="w">     </span><span class="nv">SI</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span>
<span class="w"> </span><span class="nf">MOVBLZX</span><span class="w"> </span><span class="nv">BL</span><span class="p">,</span><span class="w"> </span><span class="nv">BX</span><span class="w">                     </span><span class="c1">; BX = byte(input.addr.lo &gt;&gt; 24)</span>
<span class="w"> </span><span class="nf">SHLQ</span><span class="w">    </span><span class="o">$</span><span class="mi">24</span><span class="p">,</span><span class="w"> </span><span class="nv">BX</span>
<span class="w"> </span><span class="nf">ORQ</span><span class="w">     </span><span class="nv">DX</span><span class="p">,</span><span class="w"> </span><span class="nv">BX</span><span class="w">                     </span><span class="c1">; BX = input.addr.lo &amp; 0xffffffff</span>

<span class="c1">; Pack: byteorder.BEPutUint64(a16[:8], input.addr.hi)</span>
<span class="c1">;       byteorder.BEPutUint64(a16[8:], input.addr.lo)</span>
<span class="w"> </span><span class="nf">MOVBEQ</span><span class="w">  </span><span class="nv">AX</span><span class="p">,</span><span class="w"> </span><span class="ss">net/netip·a16</span><span class="p">(</span><span class="nv">SP</span><span class="p">)</span>
<span class="w"> </span><span class="nf">MOVBEQ</span><span class="w">  </span><span class="nv">DI</span><span class="p">,</span><span class="w"> </span><span class="ss">net/netip·a16</span><span class="o">+</span><span class="mi">8</span><span class="p">(</span><span class="nv">SP</span><span class="p">)</span>

<span class="c1">; The four other bytes of input.addr.lo, read one by one from a16</span>
<span class="w"> </span><span class="nf">MOVBLZX</span><span class="w"> </span><span class="ss">net/netip·a16</span><span class="o">+</span><span class="mi">11</span><span class="p">(</span><span class="nv">SP</span><span class="p">),</span><span class="w"> </span><span class="nv">DX</span><span class="w">   </span><span class="c1">; a16[11]</span>
<span class="w"> </span><span class="nf">SHLQ</span><span class="w">    </span><span class="o">$</span><span class="mi">32</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span>
<span class="w"> </span><span class="nf">ORQ</span><span class="w">     </span><span class="nv">DX</span><span class="p">,</span><span class="w"> </span><span class="nv">BX</span>
<span class="w"> </span><span class="nf">MOVBLZX</span><span class="w"> </span><span class="ss">net/netip·a16</span><span class="o">+</span><span class="mi">10</span><span class="p">(</span><span class="nv">SP</span><span class="p">),</span><span class="w"> </span><span class="nv">DX</span><span class="w">   </span><span class="c1">; a16[10]</span>
<span class="w"> </span><span class="nf">SHLQ</span><span class="w">    </span><span class="o">$</span><span class="mi">40</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span>
<span class="w"> </span><span class="nf">ORQ</span><span class="w">     </span><span class="nv">DX</span><span class="p">,</span><span class="w"> </span><span class="nv">BX</span>
<span class="w"> </span><span class="nf">MOVBLZX</span><span class="w"> </span><span class="ss">net/netip·a16</span><span class="o">+</span><span class="mi">9</span><span class="p">(</span><span class="nv">SP</span><span class="p">),</span><span class="w"> </span><span class="nv">DX</span><span class="w">    </span><span class="c1">; a16[9]</span>
<span class="w"> </span><span class="nf">SHLQ</span><span class="w">    </span><span class="o">$</span><span class="mi">48</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span>
<span class="w"> </span><span class="nf">ORQ</span><span class="w">     </span><span class="nv">DX</span><span class="p">,</span><span class="w"> </span><span class="nv">BX</span>
<span class="w"> </span><span class="nf">MOVBLZX</span><span class="w"> </span><span class="ss">net/netip·a16</span><span class="o">+</span><span class="mi">8</span><span class="p">(</span><span class="nv">SP</span><span class="p">),</span><span class="w"> </span><span class="nv">DX</span><span class="w">    </span><span class="c1">; a16[8]</span>
<span class="w"> </span><span class="nf">SHLQ</span><span class="w">    </span><span class="o">$</span><span class="mi">56</span><span class="p">,</span><span class="w"> </span><span class="nv">DX</span>

<span class="c1">; output.z = netip.z6noz</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="ss">net/netip·z6noz</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span>
<span class="c1">; output.addr.hi = byteorder.BEUint64(a16[:8])</span>
<span class="hll"><span class="w"> </span><span class="nf">MOVBEQ</span><span class="w">  </span><span class="ss">net/netip·a16</span><span class="p">(</span><span class="nv">SP</span><span class="p">),</span><span class="w"> </span><span class="nv">AX</span>
</span><span class="c1">; output.addr.lo assembled from the previous steps</span>
<span class="w"> </span><span class="nf">ORQ</span><span class="w">     </span><span class="nv">DX</span><span class="p">,</span><span class="w"> </span><span class="nv">BX</span>

<span class="nl">end</span><span class="p">:</span>
<span class="w"> </span><span class="nf">LEAVEQ</span>
<span class="w"> </span><span class="nf">RET</span>
<span class="c1">// return value = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>The rule loading through a move, added in Go 1.27, introduced this regression.</p>
<h3 id="out-of-order">Out of order</h3>
<p>Let’s not give up now! In reality, the rewriting rules run before <code>memcombine</code>,
notably in the <code>late opt</code> pass. At this point, the inlined versions of
<code>BEPutUint64()</code> and <code>BEUint64()</code> still expand to sixteen byte stores and sixteen
byte loads, matching their <a href="https://pkg.go.dev/internal/byteorder#BEUint64" title="internal/byteorder.BEUint64 on Go Packages">source code</a>:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">func</span><span class="w"> </span><span class="nx">BEUint64</span><span class="p">(</span><span class="nx">b</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="kt">uint64</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">_</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">b</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span><span class="w"> </span><span class="c1">// bounds check hint to compiler; see golang.org/issue/14808</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">7</span><span class="p">])</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">6</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">8</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">5</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">16</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">4</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">24</span><span class="w"> </span><span class="o">|</span>
<span class="w">        </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">3</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">32</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">40</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">48</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span><span class="o">&lt;&lt;</span><span class="mi">56</span>
<span class="p">}</span>
</code></pre></div>

<p>Let’s follow two bytes of <code>output.addr.lo</code>: <code>addr[15]</code> and <code>addr[11]</code>. Here is
a simplified <abbr title="Static Single Assignment">SSA</abbr> form before <code>late opt</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v273</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Trunc64to8</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v490</span><span class="w">                     </span><span class="c1">; byte(input.addr.lo)</span>
<span class="w">  </span><span class="nv">v226</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Trunc64to8</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v225</span><span class="w">                     </span><span class="c1">; byte(input.addr.lo &gt;&gt; 32)</span>
<span class="c1">; […]</span>
<span class="w">  </span><span class="nv">v235</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{byte}</span><span class="w"> </span><span class="nv">v233</span><span class="w"> </span><span class="nv">v226</span><span class="w"> </span><span class="nv">v223</span><span class="w">          </span><span class="c1">; a16[11] = byte(lo &gt;&gt; 32)</span>
<span class="w">  </span><span class="nv">v247</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{byte}</span><span class="w"> </span><span class="nv">v245</span><span class="w"> </span><span class="nv">v238</span><span class="w"> </span><span class="nv">v235</span><span class="w">          </span><span class="c1">; a16[12] = …</span>
<span class="w">  </span><span class="nv">v259</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{byte}</span><span class="w"> </span><span class="nv">v257</span><span class="w"> </span><span class="nv">v250</span><span class="w"> </span><span class="nv">v247</span><span class="w">          </span><span class="c1">; a16[13] = …</span>
<span class="w">  </span><span class="nv">v271</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{byte}</span><span class="w"> </span><span class="nv">v269</span><span class="w"> </span><span class="nv">v262</span><span class="w"> </span><span class="nv">v259</span><span class="w">          </span><span class="c1">; a16[14] = …</span>
<span class="w">  </span><span class="nv">v282</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Store</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{byte}</span><span class="w"> </span><span class="nv">v280</span><span class="w"> </span><span class="nv">v273</span><span class="w"> </span><span class="nv">v271</span><span class="w">          </span><span class="c1">; a16[15] = byte(lo)</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">LocalAddr</span><span class="w"> </span><span class="kt">&lt;*[16]byte&gt;</span><span class="w"> </span><span class="na">{netip.addr}</span><span class="w"> </span><span class="nv">v2</span><span class="w"> </span><span class="nv">v282</span><span class="w"> </span><span class="c1">; &amp;addr</span>
<span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Move</span><span class="w"> </span><span class="kt">&lt;mem&gt;</span><span class="w"> </span><span class="na">{[16]byte}</span><span class="w"> </span><span class="mi">[16]</span><span class="w"> </span><span class="nv">v285</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; addr = a16</span>
<span class="c1">; […]</span>
<span class="w">  </span><span class="nv">v433</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[15]</span><span class="w"> </span><span class="nv">v285</span><span class="w">                   </span><span class="c1">; &amp;addr[15]</span>
<span class="w">  </span><span class="nv">v435</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v433</span><span class="w"> </span><span class="nv">v286</span><span class="w">                      </span><span class="c1">; addr[15]</span>
<span class="w">  </span><span class="nv">v479</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[11]</span><span class="w"> </span><span class="nv">v285</span><span class="w">                   </span><span class="c1">; &amp;addr[11]</span>
<span class="w">  </span><span class="nv">v481</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v479</span><span class="w"> </span><span class="nv">v286</span><span class="w">                      </span><span class="c1">; addr[11]</span>
</code></pre></div>

<p>The first rule loads through the move: <code>(Load (OffPtr [o] p) (Move p src mem))
=&gt; (Load (OffPtr [o] src) mem)</code>. It matches both loads, which now read <code>a16</code>
with the memory state before the copy:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v600</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[15]</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="c1">; &amp;a16[15]</span>
<span class="w">  </span><span class="nv">v435</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v600</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; a16[15]</span>
<span class="w">  </span><span class="nv">v601</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[11]</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="c1">; &amp;a16[11]</span>
<span class="w">  </span><span class="nv">v481</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v601</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; a16[11]</span>
</code></pre></div>

<p>The second rule shortcuts a load following a store: <code>(Load p (Store p x _)) =&gt;
x</code>. It matches <code>v435</code>, as <code>v282</code> stores <code>a16[15]</code>. It does not match <code>v481</code>:
<code>v235</code> stores <code>a16[11]</code> four stores earlier in the chain, while the variants of
this rule look through three stores at most.</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="hll"><span class="w">  </span><span class="nv">v435</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Copy</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v273</span><span class="w">        </span><span class="c1">; byte(input.addr.lo)</span>
</span><span class="w">  </span><span class="nv">v601</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OffPtr</span><span class="w"> </span><span class="kt">&lt;*byte&gt;</span><span class="w"> </span><span class="mi">[11]</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="c1">; &amp;a16[11]</span>
<span class="w">  </span><span class="nv">v481</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v601</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; a16[11]</span>
</code></pre></div>

<p>The same happens to the other bytes: the rule forwards the four bytes stored
last, <code>a16[12]</code> to <code>a16[15]</code>. The twelve other loads now read <code>a16</code> instead of
<code>addr</code>.</p>
<p><code>BEUint64()</code> becomes a chain of <code>Or64</code>, each one adding a byte shifted into
place. <code>memcombine</code> is a <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssacompile/memcombine.go" title="memcombine.go">pass written in Go</a>, not a set of rewrite
rules. It starts from the last <code>Or64</code> of the chain and collects up to eight
terms. If each term is a byte load, extended to 64 bits and shifted, and if the
eight loads read consecutive addresses from the same pointer with the same
memory state, it replaces the whole chain with a single 64-bit load and a byte
swap. Otherwise, it tries again with four, then two terms, and from each
intermediate <code>Or64</code>. Here is the loop checking each term in a simplified version
of <code>combineLoads()</code>:</p>
<div class="language-go codehilite"><pre><span/><code><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="kt">int64</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">n</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">a</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span>
<span class="w">    </span><span class="nx">shift</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="kt">int64</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">Op</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nx">shiftOp</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">shift</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">peelShift</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">Op</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nx">extOp</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="nx">load</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">Args</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">load</span><span class="p">.</span><span class="nx">Op</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nx">ssaop</span><span class="p">.</span><span class="nx">OpLoad</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">load</span><span class="p">.</span><span class="nx">Args</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nx">mem</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="nx">p</span><span class="p">,</span><span class="w"> </span><span class="nx">off</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">splitPtr</span><span class="p">(</span><span class="nx">load</span><span class="p">.</span><span class="nx">Args</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">p</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nx">base</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="kc">false</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="nx">r</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">LoadRecord</span><span class="p">{</span><span class="nx">load</span><span class="p">:</span><span class="w"> </span><span class="nx">load</span><span class="p">,</span><span class="w"> </span><span class="nx">offset</span><span class="p">:</span><span class="w"> </span><span class="nx">off</span><span class="p">,</span><span class="w"> </span><span class="nx">shift</span><span class="p">:</span><span class="w"> </span><span class="nx">shift</span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>For <code>output.addr.hi</code>, the eight loads read <code>a16</code> with the same memory state
<code>v282</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v13</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">               </span><span class="c1">; a16[0]</span>
<span class="w">  </span><span class="nv">v530</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v14</span><span class="w"> </span><span class="nv">v282</span><span class="w">               </span><span class="c1">; a16[1]</span>
<span class="w">  </span><span class="nv">v488</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v504</span><span class="w"> </span><span class="nv">v282</span><span class="w">              </span><span class="c1">; a16[2]</span>
<span class="w">  </span><span class="nv">v405</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v537</span><span class="w"> </span><span class="nv">v282</span><span class="w">              </span><span class="c1">; a16[3]</span>
<span class="w">  </span><span class="nv">v385</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v397</span><span class="w"> </span><span class="nv">v282</span><span class="w">              </span><span class="c1">; a16[4]</span>
<span class="w">  </span><span class="nv">v361</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v373</span><span class="w"> </span><span class="nv">v282</span><span class="w">              </span><span class="c1">; a16[5]</span>
<span class="w">  </span><span class="nv">v196</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v63</span><span class="w"> </span><span class="nv">v282</span><span class="w">               </span><span class="c1">; a16[6]</span>
<span class="w">  </span><span class="nv">v432</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;byte&gt;</span><span class="w"> </span><span class="nv">v315</span><span class="w"> </span><span class="nv">v282</span><span class="w">              </span><span class="c1">; a16[7]</span>
<span class="w">  </span><span class="nv">v319</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ZeroExt8to64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v432</span><span class="w">         </span><span class="c1">; uint64(a16[7])</span>
<span class="w">  </span><span class="nv">v329</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ZeroExt8to64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v196</span><span class="w">         </span><span class="c1">; uint64(a16[6])</span>
<span class="w">  </span><span class="nv">v330</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Lsh64x64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="mi">[true]</span><span class="w"> </span><span class="nv">v329</span><span class="w"> </span><span class="nv">v138</span><span class="w"> </span><span class="c1">; uint64(a16[6]) &lt;&lt; 8</span>
<span class="w">  </span><span class="nv">v331</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v319</span><span class="w"> </span><span class="nv">v330</span><span class="w">            </span><span class="c1">; a16[7] | a16[6] &lt;&lt; 8</span>
<span class="c1">; […] same for a16[5] to a16[1]</span>
<span class="w">  </span><span class="nv">v401</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ZeroExt8to64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v13</span><span class="w">          </span><span class="c1">; uint64(a16[0])</span>
<span class="w">  </span><span class="nv">v402</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Lsh64x64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="mi">[true]</span><span class="w"> </span><span class="nv">v401</span><span class="w"> </span><span class="nv">v55</span><span class="w">  </span><span class="c1">; uint64(a16[0]) &lt;&lt; 56</span>
<span class="w">  </span><span class="nv">v403</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v402</span><span class="w"> </span><span class="nv">v391</span><span class="w">            </span><span class="c1">; | a16[0] &lt;&lt; 56 = output.addr.hi</span>
</code></pre></div>

<p><code>memcombine</code> merges them into one load and a swap:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v286</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Load</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v22</span><span class="w"> </span><span class="nv">v282</span><span class="w">   </span><span class="c1">; a16[:8]</span>
<span class="w">  </span><span class="nv">v285</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Bswap64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v286</span><span class="w">    </span><span class="c1">; output.addr.hi</span>
</code></pre></div>

<p>For <code>output.addr.lo</code>, here is the chain <code>memcombine</code> sees after <code>late opt</code>:</p>
<div class="language-go-compiler-ssa codehilite"><pre><span/><code><span class="w">  </span><span class="nv">v436</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ZeroExt8to64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v273</span><span class="w"> </span><span class="c1">; addr[15], forwarded</span>
<span class="w">  </span><span class="nv">v448</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v436</span><span class="w"> </span><span class="nv">v447</span><span class="w">    </span><span class="c1">; | addr[14] &lt;&lt; 8, forwarded</span>
<span class="w">  </span><span class="nv">v460</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v459</span><span class="w"> </span><span class="nv">v448</span><span class="w">    </span><span class="c1">; | addr[13] &lt;&lt; 16, forwarded</span>
<span class="w">  </span><span class="nv">v472</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v471</span><span class="w"> </span><span class="nv">v460</span><span class="w">    </span><span class="c1">; | addr[12] &lt;&lt; 24, forwarded</span>
<span class="w">  </span><span class="nv">v484</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v483</span><span class="w"> </span><span class="nv">v472</span><span class="w">    </span><span class="c1">; | a16[11] &lt;&lt; 32, loaded</span>
<span class="w">  </span><span class="nv">v496</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v495</span><span class="w"> </span><span class="nv">v484</span><span class="w">    </span><span class="c1">; | a16[10] &lt;&lt; 40, loaded</span>
<span class="w">  </span><span class="nv">v508</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v507</span><span class="w"> </span><span class="nv">v496</span><span class="w">    </span><span class="c1">; | a16[9] &lt;&lt; 48, loaded</span>
<span class="w">  </span><span class="nv">v520</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">Or64</span><span class="w"> </span><span class="kt">&lt;uint64&gt;</span><span class="w"> </span><span class="nv">v519</span><span class="w"> </span><span class="nv">v508</span><span class="w">    </span><span class="c1">; | a16[8] &lt;&lt; 56, loaded</span>
</code></pre></div>

<p>From <code>v520</code>, four of the eight terms are forwarded bytes, not loads from memory,
and <code>memcombine</code> can’t combine them. It doesn’t merge the four remaining loads
either, as they sit on top of the forwarded bytes.</p>
<h3 id="back-in-order">Back in order</h3>
<p>In summary, the rewriting rules run too early to be effective. A quick
workaround exists: run an earlier round of <code>memcombine</code> before <code>late opt</code>. After
<a href="https://github.com/vincentbernat/go/commit/netipmap-fix4" title="cmd/compile/internal/ssacompile: run memcombine before late opt">this change</a>, the generated code for the helper is back to the
shortest possible version:</p>
<div class="language-go-assembly codehilite"><pre><span/><code><span class="c1">// AX = input.addr.hi, BX = input.addr.lo, CX = input.z</span>
<span class="w"> </span><span class="nf">CMPQ</span><span class="w">    </span><span class="ss">net/netip·z4</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">     </span><span class="c1">; check "z" if this is an IPv4 address</span>
<span class="w"> </span><span class="nf">JNE</span><span class="w">     </span><span class="ss">end</span><span class="w">                      </span><span class="c1">; if not, stop here</span>
<span class="w"> </span><span class="nf">MOVQ</span><span class="w">    </span><span class="ss">net/netip·z6noz</span><span class="p">(</span><span class="nv">SB</span><span class="p">),</span><span class="w"> </span><span class="nv">CX</span><span class="w">  </span><span class="c1">; CX = netip.z6noz</span>
<span class="nl">end</span><span class="p">:</span>
<span class="w"> </span><span class="nf">RET</span>
<span class="c1">// return value = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>And the benchmark confirms it! ✌️</p>
<div class="language-text-only codehilite"><pre><span/><code>goos: linux
goarch: amd64
pkg: github.com/vincentbernat/go-netip-addrto6
cpu: AMD Ryzen 5 5600X 6-Core Processor
                   │   Go 1.26.8    │             Our branch              │
                   │     sec/op     │    sec/op     vs base               │
AddrTo6/safe          6.5470n ±  0%   0.8944n ± 4%  -86.34% (p=0.002 n=6)
AddrTo6/unsafe        0.9071n ±  3%   0.8682n ± 1%   -4.28% (p=0.002 n=6)
AddrTo6/builtin       0.8871n ±  2%   0.8785n ± 1%        ~ (p=0.310 n=6)
</code></pre></div>

<h1 id="next-steps">Next steps</h1>
<p>I think Go maintainers would reject this change because of the additional
<code>memcombine</code> pass. Instead, I plan to publish this blog post and bring up the
subject again as a follow-up to <a href="https://github.com/golang/go/issues/54365" title="proposal: net/netip: add netip.Addr.Map · Issue #54365 · golang/go">issue #54365</a>. Either the sheer complexity and
the Go 1.27 regression convince the maintainers that adding a <code>To6()</code> method is
simpler and more efficient, or they advise me on how to move forward. Either
way, digging into this subject taught me a lot about the Go compiler! ⚙️</p>
<div class="admonition">
<p class="admonition-title">Update (2026-10)</p>
<p>I opened <a href="https://github.com/golang/go/issues/81994" title="proposal: net/netip: add netip.Addr.To6() · Issue #81994 · golang/go">issue #81994</a> to propose <code>Addr.To6()</code>. Give it a 👍 if you want
it in Go!</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:why">
<p>IPv4-mapped IPv6 addresses let you handle only IPv6 addresses in your code
and keep conversions at a few well-defined boundaries. I use them in
<a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a>. <a class="footnote-backref" href="#fnref:why" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:equiv">
<p>This is not strictly equivalent. The zero value becomes <code>::</code> while it would
make more sense to leave it untouched. <a class="footnote-backref" href="#fnref:equiv" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:inconvenient">
<p>I suppose Go maintainers keep it inconvenient on purpose: they don’t want
random packages to patch the standard library or other packages. <a class="footnote-backref" href="#fnref:inconvenient" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:tests">
<p>To avoid catastrophic bugs when <code>netip.Addr</code>’s layout changes, you must <a href="https://github.com/vincentbernat/go-netip-addrto6/blob/main/ipv6_unsafe_test.go" title="Tests for unsafe AddrTo6() function">add
tests</a> to detect it. This is more dangerous if you put this code in a
package. Either ask users to run the tests themselves, or detect the change
at run time and panic. Hiding this optimization behind a build tag would
make users aware of this potential trap. <a class="footnote-backref" href="#fnref:tests" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:dis">
<p>I produced the assembly code with <code>GOTOOLCHAIN=go1.26.8 GOAMD64=v3 ./go-asm
'\.asm'</code> from the <a href="https://github.com/vincentbernat/go-netip-addrto6">companion repository</a>, then edited it a bit to keep this
article from turning into an endless rabbit hole. Due to an unfortunate
sequence of events, we switch between versions: Go 1.27 introduced a
regression that blurs the point of this article. <a class="footnote-backref" href="#fnref:dis" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:inline">
<p>The function is short enough for the compiler to inline it, so the assembly
code may vary depending on the surrounding code. <a class="footnote-backref" href="#fnref:inline" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:amd64v3">
<p><code>MOVBEQ</code> requires <code>GOAMD64=v3</code>, matching the <a href="https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels" title="X86-64 on Wikipedia">x86-64-v3
microarchitecture</a> from 2013. Otherwise, the compiler translates
<code>BEPutUint64()</code> to <code>BSWAPQ</code>+<code>MOVQ</code> and <code>BEUint64()</code> to <code>MOVQ</code>+<code>BSWAPQ</code>. <a class="footnote-backref" href="#fnref:amd64v3" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:statement">
<p>When the call is used as a statement, a struct literal alone is invalid. The
patch then turns it into <code>_ = netip.Addr{…}</code>. <a class="footnote-backref" href="#fnref:statement" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:html">
<p>The compiler can also write an HTML file with the result of each pass:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span><span class="nv">GOSSAFUNC</span><span class="o">=</span>AddrTo6Safe<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="nv">GOTOOLCHAIN</span><span class="o">=</span>go1.26.8<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="nv">GOAMD64</span><span class="o">=</span>v3<span class="w"> </span>go<span class="w"> </span>build<span class="w"> </span>-a<span class="w"> </span>.
<span class="go">dumped SSA for AddrTo6Safe,1 to ./ssa.html</span>
</code></pre></div>

<p><a class="footnote-backref" href="#fnref:html" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:lines">
<p>Source line numbers in parentheses follow value identifiers, but I removed
them from the examples. The <abbr title="Static Single Assignment">SSA</abbr> form also features branches, but we don’t
need them to understand our optimizations. <a class="footnote-backref" href="#fnref:lines" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Sharding a routing table for lock-free reads in Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-akvorado-rib-sharding" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-akvorado-rib-sharding#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-05-24T19:00:00Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-akvorado-rib-sharding.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>To associate routing information—like AS paths or <abbr title="Border Gateway Protocol">BGP</abbr> communities—to flows,
<a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> can import routes through the <a href="https://www.rfc-editor.org/rfc/rfc7854" title="RFC 7854: BGP Monitoring Protocol (BMP)"><abbr title="Border Gateway Protocol">BGP</abbr> Monitoring Protocol</a> (<abbr title="BGP Monitoring Protocol">BMP</abbr>). As
the Internet routing table contains more than <a href="https://bgp.potaroo.net" title="BGP Routing Table Analysis Reports">1 million routes</a>, Akvorado
needs to <strong>scale to tens of millions of routes</strong>.<sup id="fnref:optimize"><a class="footnote-ref" href="#fn:optimize">1</a></sup> This has been a
long-standing challenge,<sup id="fnref:past"><a class="footnote-ref" href="#fn:past">2</a></sup> but I expect this issue is now fixed by using
<strong><abbr title="Routing Information Base">RIB</abbr> sharding</strong>, a method that splits the routing database into several parts
to enable concurrent updates.</p>
<div class="toc">
<ul>
<li><a href="#previous-implementation">Previous implementation</a><ul>
<li><a href="#storing-routes-in-a-map">Storing routes in a map</a></li>
<li><a href="#interning-routes">Interning routes</a></li>
<li><a href="#why-does-it-not-scale">Why does it not scale?</a></li>
</ul>
</li>
<li><a href="#rib-sharding">RIB sharding</a><ul>
<li><a href="#first-step-basic-sharding">First step: basic sharding</a></li>
<li><a href="#second-step-lock-free-reads">Second step: lock-free reads</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="previous-implementation">Previous implementation</h1>
<p>Akvorado connects 2 elements to build its <abbr title="Routing Information Base">RIB</abbr>:</p>
<ol>
<li>a <strong>prefix tree</strong>, and</li>
<li>a <strong>list of routes</strong> attached to each prefix.</li>
</ol>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 711px"><span class="lf-media-inner" style="padding-bottom: 105.626%"><img alt="Akvorado BMP RIB implementation before sharding with the memory layout of each&#10;structure and a single lock." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-before.b60946a339a187.svg" width="711" height="751" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation without sharding. One single read/write lock.</figcaption></figure>
<p>In the diagram above, the <abbr title="Routing Information Base">RIB</abbr> stores five IPv4 prefixes and two IPv6 prefixes.
One of them, <code>2001:db8:1::/48</code>, contains three routes:</p>
<ul>
<li>from peer 3, next hop <code>2001:db8::3:1</code>, AS 65402, AS path <code>65402</code>, community
  <code>65402:31</code>,</li>
<li>from peer 4, next hop <code>2001:db8::4:1</code>, same <abbr title="Autonomous System Number">ASN</abbr>, AS path, and community,</li>
<li>from peer 5, next hop <code>2001:db8::5:1</code>, AS 65402, AS path <code>65401 65402</code>,
  community <code>65402:31</code>.</li>
</ul>
<p>The <code>rib</code> structure is defined in Go as follows:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">rib</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">tree</span><span class="w">          </span><span class="o">*</span><span class="nx">bart</span><span class="p">.</span><span class="nx">Table</span><span class="p">[</span><span class="nx">prefixIndex</span><span class="p">]</span>
<span class="w">    </span><span class="nx">routes</span><span class="w">        </span><span class="kt">map</span><span class="p">[</span><span class="nx">routeKey</span><span class="p">]</span><span class="nx">route</span>
<span class="w">    </span><span class="nx">nlris</span><span class="w">         </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">nlri</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextHops</span><span class="w">      </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">nextHop</span><span class="p">]</span>
<span class="w">    </span><span class="nx">rtas</span><span class="w">          </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">routeAttributes</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextPrefixID</span><span class="w">  </span><span class="nx">prefixIndex</span>
<span class="w">    </span><span class="nx">freePrefixIDs</span><span class="w"> </span><span class="p">[]</span><span class="nx">prefixIndex</span>
<span class="p">}</span>
</code></pre></div>

<p>The prefix tree uses the <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> package, an adaptation of Donald Knuth’s <a href="https://www.hariguchi.org/art/art.pdf" title="ART: A Fast Free Multibit Trie Based Routing Table">ART
algorithm</a>. The <a href="https://github.com/gaissmai/iprbench">benchmarks</a> demonstrate it outperforms other packages for
lookups, insertions, and memory usage.<sup id="fnref:performance"><a class="footnote-ref" href="#fn:performance">3</a></sup> Plus, the author is quite
helpful.</p>
<h2 id="storing-routes-in-a-map">Storing routes in a map</h2>
<p>The list of routes for each prefix is not stored directly in the prefix tree:
it would put too much pressure on the garbage collector by allocating per-prefix
arrays.</p>
<p>Instead, the <abbr title="Routing Information Base">RIB</abbr> assigns a unique 32-bit prefix identifier for each prefix,
either by picking the last available prefix identifier from the <code>freePrefixIDs</code>
array if any, or using the <code>nextPrefixID</code> value before incrementing it. Then,
the routes are stored in the <code>routes</code> map, leveraging the <a href="https://go.dev/blog/swisstable" title="Faster Go maps with Swiss Tables">optimized Swiss
table</a> in Go. To retrieve routes attached to a prefix, we look them up
one by one in the <code>routes</code> map with a 64-bit key combining the 32-bit prefix
index with a 32-bit route index matching the position of the route in the list.
Akvorado scans routes from the first to the last to find the best one.<sup id="fnref:scan"><a class="footnote-ref" href="#fn:scan">4</a></sup> It
knows there is no more route if the route key returns no result.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">prefixIndex</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeIndex</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeKey</span><span class="w"> </span><span class="kt">uint64</span>
</code></pre></div>

<h2 id="interning-routes">Interning routes</h2>
<p>A route contains a <abbr title="Border Gateway Protocol">BGP</abbr> peer identifier, a partial <abbr title="Network Layer Reachability Information">NLRI</abbr>,<sup id="fnref:nlri"><a class="footnote-ref" href="#fn:nlri">5</a></sup> the next hop, and the
attributes.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">route</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">peer</span><span class="w">       </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">nlri</span><span class="w">       </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">nlri</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextHop</span><span class="w">    </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">nextHop</span><span class="p">]</span>
<span class="w">    </span><span class="nx">attributes</span><span class="w"> </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">routeAttributes</span><span class="p">]</span>
<span class="w">    </span><span class="nx">prefixLen</span><span class="w">  </span><span class="kt">uint8</span>
<span class="p">}</span>

<span class="kd">type</span><span class="w"> </span><span class="nx">nlri</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">family</span><span class="w"> </span><span class="nx">bgp</span><span class="p">.</span><span class="nx">Family</span>
<span class="w">    </span><span class="nx">path</span><span class="w">   </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">rd</span><span class="w">     </span><span class="nx">RD</span>
<span class="p">}</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">nextHop</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeAttributes</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">asn</span><span class="w">              </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">asPath</span><span class="w">           </span><span class="p">[]</span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">communities</span><span class="w">      </span><span class="p">[]</span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">largeCommunities</span><span class="w"> </span><span class="p">[]</span><span class="nx">bgp</span><span class="p">.</span><span class="nx">LargeCommunity</span>
<span class="p">}</span>
</code></pre></div>

<p>To save memory and allocations, <abbr title="Network Layer Reachability Information">NLRI</abbr>, next hops, and route attributes are
“interned”: a 32-bit integer replaces the real value. The mechanism predates the
<a href="https://go.dev/blog/unique" title="New unique package"><code>unique</code> package</a> introduced in Go 1.23. We keep it because it has
different trade-offs:</p>
<ul>
<li>It uses <strong>explicit reference counting</strong> instead of relying on weak pointers.</li>
<li>It works with <strong>non-comparable values</strong> implementing <code>Hash()</code> and <code>Equal()</code>
  methods.<sup id="fnref:hash"><a class="footnote-ref" href="#fn:hash">6</a></sup></li>
<li>It uses <strong>explicit pool instances</strong>. This will be useful for sharding.</li>
<li>It has <strong>better performance</strong>. See for example this <a href="https://github.com/akvorado/akvorado/pull/2244/changes/682b6063af50780dcd64e46b39d5e66c5074d9ab" title="outlet/routing: use Go's unique instead of intern">benchmark</a>.</li>
<li>It consumes <strong>half the memory</strong> thanks to unsigned 32-bit references instead
  of pointers.</li>
<li>But it is <strong>not safe for concurrent use</strong>.</li>
</ul>
<h2 id="why-does-it-not-scale">Why does it not scale?</h2>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>At <a href="https://www.free.fr/freebox">AS 12322</a>, we don’t use <abbr title="BGP Monitoring Protocol">BMP</abbr> yet.<sup id="fnref:cisco"><a class="footnote-ref" href="#fn:cisco">7</a></sup> But <a href="https://github.com/bogi788" title="bogi788 on GitHub">Gerhard Bogner</a>
had the patience, availability, and technical skills to help me <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">debug this
issue</a>.</p>
</div>
<p>The global read/write lock is a bottleneck in this implementation. But how?
There are several users of the <abbr title="Routing Information Base">RIB</abbr>, each with its own set of constraints:</p>
<ul>
<li>
<p>The <strong>Kafka workers</strong> look up the <abbr title="Routing Information Base">RIB</abbr> to enrich flows with routing
  information. They are bound by the number of Kafka partitions.<sup id="fnref:KIP-932"><a class="footnote-ref" href="#fn:KIP-932">8</a></sup>
  Akvorado also adjusts their number to ensure efficient batching to ClickHouse.
  On our setup, the number of workers oscillates between 8 and 16. As we want
  to observe the latest data, we cannot afford for the Kafka workers to lag too
  much.</p>
</li>
<li>
<p>The <strong>monitored routers</strong> send route updates through the <abbr title="BGP Monitoring Protocol">BMP</abbr> protocol. When
  connecting, they can send millions of routes.<sup id="fnref:bmpconfig"><a class="footnote-ref" href="#fn:bmpconfig">9</a></sup> After the initial
  synchronization, updates are sent continuously and may spike from time to
  time. The router detects a stuck <abbr title="BGP Monitoring Protocol">BMP</abbr> station when its TCP window is full and
  resets the session in this case. While Akvorado implements a large incoming
  buffer, it still needs to update the received routes with the write lock held
  fast enough to avoid being detected as stuck.</p>
</li>
<li>
<p>When a <strong>remote <abbr title="Border Gateway Protocol">BGP</abbr> peer goes down</strong>, Akvorado flushes the associated routes by
  walking the <abbr title="Routing Information Base">RIB</abbr> with the write lock held. When a <strong>monitored router goes
  down</strong>, Akvorado waits a bit but eventually flushes all the associated routes.</p>
</li>
</ul>
<p>In short: on a busy setup, lock contention is high for both readers and
writers, and neither can lag too much behind.</p>
<h1 id="rib-sharding"><abbr title="Routing Information Base">RIB</abbr> sharding</h1>
<h2 id="first-step-basic-sharding">First step: basic sharding</h2>
<p>To remove the global lock, the <abbr title="Routing Information Base">RIB</abbr> is split into several “shards,” each one
handling a subset of the prefixes:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Akvorado BMP RIB implementation after sharding with the memory layout of each&#10;structure and one lock per shard." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-step1.71c5ecf8b46b57.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation with sharding.</figcaption></figure>
<p>The prefix tree stays global and is protected by a single lock. Each shard gets
its read/write lock, its route map, and its intern pools to store NLRIs, next
hops, and route attributes, which would not have been possible with <a href="https://go.dev/blog/unique" title="New unique package">Go’s
<code>unique</code> package</a>. The prefix indexes are also sharded: the 8 most
significant bits are the shard index and the 24 remaining bits are the local
prefix index.</p>
<p>Gerhard <a href="https://github.com/akvorado/akvorado/discussions/2287#discussioncomment-16020731">confirmed</a> that after <a href="https://github.com/akvorado/akvorado/commit/7e6bbf2210fdf7116d2ee168b307b9906cc223c0" title="outlet/routing: implement RIB sharding for BMP">this blind change</a>,
the <abbr title="BGP Monitoring Protocol">BMP</abbr> receiver chugged steadily. 🎉</p>
<p>Later, I wrote a <a href="https://github.com/akvorado/akvorado/blob/0811c40cc2065380e3a6230c2796312838e57850/outlet/routing/provider/bmp/concurrent_test.go">concurrent benchmark</a> over half a million synthetic but
plausible routes<sup id="fnref:plausible"><a class="footnote-ref" href="#fn:plausible">10</a></sup> partitioned over 0 to 8 writers, churning routes as
fast as possible, while 1 to 16 readers continuously look up a set of 10,000
routes. I don’t know if this benchmark is realistic, but it confirms the
improvements for both read and write latencies:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 970px"><span class="lf-media-inner" style="padding-bottom: 40.928%"><img alt="Two heatmaps. One for read latency ratio, the other for write latency ratio.&#10;Both of them comparing the speedup with colored tiles between the code before&#10;sharding and after sharding. Most tiles are&#10;green." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-heatmap.0ed40d587fea78.svg" width="970" height="397" class="lf-media"/></span></div><figcaption>Read and write latency performance improvement after sharding.</figcaption></figure>
<p>It also shows that a high number of writers degrades read latency.</p>
<h2 id="second-step-lock-free-reads">Second step: lock-free reads</h2>
<p>The single read/write lock protecting the prefix tree is the next target. The
<a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> package provides alternative mutation methods returning an updated tree
using copy-on-write. Readers don’t need the global lock any more, leaving it
only to synchronize writers. The prefix tree is boxed in an atomic pointer.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Akvorado BMP RIB implementation for sharding with lock-free reads. It shows&#10;the memory layout of each structure." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-step2.2996a7f0868135.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation with sharding and lock-free reads.</figcaption></figure>
<p>Without a lock, readers can now fetch a stale prefix index when walking their
copy of the tree if a concurrent writer removes the last route attached to this
prefix index and recycles it for another prefix. To avoid this issue, we combine
the prefix index with a generation number and store them in the tree:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">generation</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">prefixRef</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">idx</span><span class="w"> </span><span class="nx">prefixIndex</span>
<span class="w">    </span><span class="nx">gen</span><span class="w"> </span><span class="nx">generation</span>
<span class="p">}</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">rib</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">mu</span><span class="w">     </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span>
<span class="w">    </span><span class="nx">tree</span><span class="w">   </span><span class="nx">atomic</span><span class="p">.</span><span class="nx">Pointer</span><span class="p">[</span><span class="nx">bart</span><span class="p">.</span><span class="nx">Table</span><span class="p">[</span><span class="nx">prefixRef</span><span class="p">]]</span>
<span class="w">    </span><span class="nx">shards</span><span class="w"> </span><span class="p">[]</span><span class="o">*</span><span class="nx">ribShard</span>
<span class="p">}</span>
</code></pre></div>

<p>Each shard stores the generation number for each local prefix index. The
generation number increases by one if the associated prefix index is freed. When
looking up the routes attached to a prefix index, the reader checks if the
generation number matches. Otherwise, it assumes the index was recycled and the
list of routes is empty.<sup id="fnref:retry"><a class="footnote-ref" href="#fn:retry">11</a></sup> You can see this case in the diagram above for
prefix index 5, stored with a generation index of 3, while the current value in
the <code>[]generations</code> array is 4. The generation number could overflow, but it is
not a problem as lookups are quick.</p>
<p>Running the concurrent benchmark against this new implementation shows the
improvements for the read latency as soon as the cost of the copy-on-write
prefix tree is amortized.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1256px"><span class="lf-media-inner" style="padding-bottom: 46.815%"><img alt="Six heatmaps. Three for read latency ratio, three others for write latency&#10;ratio. They compare the numbers without sharding, with sharding, and with&#10;lock-free reads, pair by pair. For read latency, most tiles are green, showing&#10;an improvement of the second step. For write latency, the speedup is negative&#10;for a low number of readers." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-heatmap2.8bbce411e680da.svg" width="1256" height="588" class="lf-media"/></span></div><figcaption>Read and write latency performance improvement after lock-free reads. The middle column shows the cumulative improvements of both steps.</figcaption></figure>
<hr/>
<p>Among the multiple attempts to optimize the <abbr title="BGP Monitoring Protocol">BMP</abbr> component, <abbr title="Routing Information Base">RIB</abbr> sharding is one
of the more satisfying. <a href="https://github.com/akvorado/akvorado/releases/tag/v2.2.0">Akvorado 2.2</a> implements the first step.
<a href="https://github.com/akvorado/akvorado/pull/2433" title="More efficient RIB tree for BMP">PR #2433</a>, drafted while writing this blog post, implements the second step
and was released with <a href="https://github.com/akvorado/akvorado/releases/tag/v2.4.0">Akvorado 2.4</a>. 🪓</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:optimize">
<p>Each router exporting flows doesn’t need to send its routes. When
Akvorado does not find a route from a specific device, it falls back to a
route sent by another device. It is up to the operator to decide if this
is a good enough approximation. <a class="footnote-backref" href="#fnref:optimize" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:past">
<p>I made many attempts to scale the <abbr title="BGP Monitoring Protocol">BMP</abbr> component. See for example
<a href="https://github.com/akvorado/akvorado/pull/254" title="inlet/bmp: avoid long lock times when flushing peers">PR #254</a>, <a href="https://github.com/akvorado/akvorado/pull/255" title="“Lockless” RIB design">PR #255</a>, <a href="https://github.com/akvorado/akvorado/pull/278" title="inlet/bmp: new lockless design">PR #278</a>, <a href="https://github.com/akvorado/akvorado/pull/2244" title="outlet/routing: read from RIB without any locks">PR #2244</a>, and <a href="https://github.com/akvorado/akvorado/pull/2245" title="outlet/routing: buffer BMP messages to avoid being flagged as “stuck”">PR #2245</a>.
Despite these efforts, this component remained problematic for some users.
See <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">discussion #2287</a> as the latest example. <a class="footnote-backref" href="#fnref:past" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:performance">
<p>It keeps improving: <a href="https://github.com/gaissmai/bart/releases/tag/v0.28.0" title="bart release v0.28.0: New FastNode implementation">bart 0.28.0</a> features a new
implementation that trades a bit of memory for greater lookup performance. I
did not test it yet, as I have been preparing this blog post for a couple
of months already. <a class="footnote-backref" href="#fnref:performance" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:scan">
<p>Akvorado prefers the route matching the exact next hop. Otherwise, it
falls back to any other route. This is an approximation. An alternative
would be to have one prefix tree for each <abbr title="Border Gateway Protocol">BGP</abbr> peer but it would require
configuring all routers to export their routes. <a href="http://www.pmacct.net/">pmacct</a>’s <abbr title="BGP Monitoring Protocol">BMP</abbr> daemon
implements this approach. <a class="footnote-backref" href="#fnref:scan" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:nlri">
<p>If we consider the <abbr title="Border Gateway Protocol">BGP</abbr> <abbr title="Routing Information Base">RIB</abbr> as a database, the Network Layer
Reachability Information (<abbr title="Network Layer Reachability Information">NLRI</abbr>) is the primary key. Its content depends on
the <abbr title="Border Gateway Protocol">BGP</abbr> family. With IPv4 or IPv6 unicast, this is the prefix. For VPNv4 and
VPNv6 families, it includes the route distinguisher. If you enable the
<a href="https://www.rfc-editor.org/rfc/rfc7911" title="RFC 7911: Advertisement of Multiple Paths in BGP">ADD-PATH</a> extension, the <abbr title="Network Layer Reachability Information">NLRI</abbr> also contains a path identifier.</p>
<p>In our implementation, we don’t store the prefix as we get it from the
looked-up IP address using the prefix length stored separately. <a class="footnote-backref" href="#fnref:nlri" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:hash">
<p>The <code>Hash()</code> methods rely on the <a href="https://pkg.go.dev/hash/maphash" title="hash/maphash on Go Packages"><code>hash/maphash</code></a> package
and on the <a href="https://pkg.go.dev/unsafe" title="unsafe on Go Packages"><code>unsafe</code></a> package to avoid memory copies. See for
example the <a href="https://github.com/akvorado/akvorado/blob/97fced5e855ac220e96148e132f643dc180cd097/outlet/routing/provider/bmp/rib.go#L89-L96"><code>Hash()</code> function for the <code>nlri</code> structure</a>. <a class="footnote-backref" href="#fnref:hash" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:cisco">
<p>Despite being an author or co-author of the first <abbr title="BGP Monitoring Protocol">BMP</abbr>-related RFCs since
2016 (<a href="https://www.rfc-editor.org/rfc/rfc7854" title="RFC 7854: BGP Monitoring Protocol (BMP)">RFC 7854</a>, <a href="https://www.rfc-editor.org/rfc/rfc8671" title="RFC 8671: Support for Adj-RIB-Out in the BGP Monitoring Protocol (BMP)">RFC 8671</a>, <a href="https://www.rfc-editor.org/rfc/rfc9069" title="RFC 9069: Support for Local RIB in the BGP Monitoring Protocol (BMP)">RFC 9069</a>), Cisco did not implement it
in a usable way in IOS XR until version 24.2.1. We still need to upgrade a
few routers to enable this feature. <a class="footnote-backref" href="#fnref:cisco" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:KIP-932">
<p><a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" title="KIP-932: Queues for Kafka">KIP-932</a> introduces, in Kafka 4.2, the concept of share groups to
enable cooperative consumption on the same partition. This is not supported
in Akvorado yet. <a class="footnote-backref" href="#fnref:KIP-932" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:bmpconfig">
<p>You can configure <abbr title="BGP Monitoring Protocol">BMP</abbr> to send routes for each <abbr title="Border Gateway Protocol">BGP</abbr> peer before or after
applying the incoming policies. In this case, you can get more than one
million routes for each transit peer. You can also tell <abbr title="BGP Monitoring Protocol">BMP</abbr> to send the
local <abbr title="Routing Information Base">RIB</abbr>, which only contains the best path for each prefix. <a class="footnote-backref" href="#fnref:bmpconfig" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:plausible">
<p>The prefixes are random, but the prefix size distribution and the
AS path length distribution follow the <a href="https://bgp.potaroo.net/as2.0/" title="AS65000 BGP Routing Table Analysis Report">data provided by Geoff Huston</a>. <a class="footnote-backref" href="#fnref:plausible" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:retry">
<p>Alternatively, we could retry the lookup, but it would be pointless:
the <abbr title="Routing Information Base">RIB</abbr> is an <a href="https://en.wikipedia.org/wiki/Eventual_consistency" title="Eventual consistency on Wikipedia">eventually consistent</a> database, and an empty list was a
correct answer at some point in the recent past. <a class="footnote-backref" href="#fnref:retry" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Using eBPF to load-balance traffic across UDP sockets with Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-reuseport-ebpf-go" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-reuseport-ebpf-go#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-01-05T07:51:52Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-reuseport-ebpf-go.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="/en/blog/2022-akvorado-flow-collector" title="Akvorado: a flow collector, enricher, and visualizer">Akvorado</a> collects <a href="https://www.rfc-editor.org/rfc/rfc3176" title="RFC 3176: InMon Corporation's sFlow: A Method for Monitoring Traffic in Switched and Routed Networks">sFlow</a> and <a href="https://www.rfc-editor.org/rfc/rfc7011" title="RFC 7011: Specification of the IP Flow Information Export (IPFIX) Protocol for the Exchange of Flow Information">IPFIX</a> flows over UDP. Because UDP does
not retransmit lost packets, it needs to process them quickly. Akvorado runs
several workers listening to the same port. The kernel should load-balance
received packets fairly between these workers. However, this does not work as
expected. A couple of workers exhibit high packet loss:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>curl<span class="w"> </span>-s<span class="w"> </span><span class="m">127</span>.0.0.1:8080/api/v0/inlet/metrics<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span>-n<span class="w"> </span>s/akvorado_inlet_flow_input_udp_in_dropped//p
<span class="go">packets_total{listener="0.0.0.0:2055",worker="0"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="1"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="2"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="3"} 1.614933572278264e+15</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="4"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="5"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="6"} 9.59964121598348e+14</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="7"} 0</span>
</code></pre></div>

<p>eBPF can help by implementing an alternate balancing algorithm. 🐝</p>
<div class="toc">
<ul>
<li><a href="#options-for-load-balancing">Options for load-balancing</a><ul>
<li><a href="#so_reuseport-option">SO_REUSEPORT option</a></li>
<li><a href="#so_attach_reuseport_ebpf-option">SO_ATTACH_REUSEPORT_EBPF option</a></li>
</ul>
</li>
<li><a href="#load-balancing-with-ebpf-and-go">Load-balancing with eBPF and Go</a><ul>
<li><a href="#ebpf-program-in-c">eBPF program in C</a><ul>
<li><a href="#header-files">Header files</a></li>
<li><a href="#compilation">Compilation</a></li>
</ul>
</li>
<li><a href="#usage-from-go">Usage from Go</a></li>
<li><a href="#graceful-restart">Graceful restart</a></li>
</ul>
</li>
<li><a href="#addendum">Addendum</a></li>
</ul>
</div>
<h1 id="options-for-load-balancing">Options for load-balancing</h1>
<p>There are three methods to load-balance UDP packets across workers:</p>
<ol>
<li>One worker receives the packets and dispatches them to the other workers.</li>
<li>All workers share the same socket.</li>
<li>Each worker has its own socket, listening to the same port, with the
   <code>SO_REUSEPORT</code> socket option.</li>
</ol>
<h2 id="so_reuseport-option"><code>SO_REUSEPORT</code> option</h2>
<p>Tom Hebert <a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=c617f398edd4db2b8567a28e899a88f8f574798d" title="Merge commit adding the SO_REUSEPORT option">added</a> the <code>SO_REUSEPORT</code> socket option in Linux 3.9. The
cover letter for his patch series explains why this new option is better than
the two existing ones from a performance point of view:</p>
<blockquote>
<p><code>SO_REUSEPORT</code> allows multiple listener sockets to be bound to the same port.
[…] Received packets are distributed to multiple sockets bound to the same
port using a 4-tuple hash.</p>
<p>The motivating case for <code>SO_RESUSEPORT</code> in TCP would be something like a web
server binding to port 80 running with multiple threads, where each thread
might have it’s own listener socket. This could be done as an alternative to
other models:</p>
<ol>
<li>have one listener thread which dispatches completed connections to workers, or</li>
<li>accept on a single listener socket from multiple threads.</li>
</ol>
<p>In case #1, the listener thread can easily become the bottleneck with high
connection turn-over rate. In case #2, the proportion of connections accepted
per thread tends to be uneven under high connection load. […] We have seen the
disproportion to be as high as 3:1 ratio between thread accepting most
connections and the one accepting the fewest. With <code>SO_REUSEPORT</code> the
distribution is uniform.</p>
<p>The motivating case for <code>SO_REUSEPORT</code> in UDP would be something like a DNS
server. An alternative would be to receive on the same socket from multiple
threads. As in the case of TCP, the load across these threads tends to be
disproportionate and we also see a lot of contection on the socket lock.</p>
</blockquote>
<p>Akvorado uses the <code>SO_REUSEPORT</code> option to dispatch the packets across the
workers. However, because the distribution uses a 4-tuple hash, a single socket
handles all the flows from one exporter.</p>
<h2 id="so_attach_reuseport_ebpf-option"><code>SO_ATTACH_REUSEPORT_EBPF</code> option</h2>
<p>In Linux 4.5, Craig Gallek <a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=538950a1b7527a0a52ccd9337e3fcd304f027f13" title="soreuseport: setsockopt SO_ATTACH_REUSEPORT_[CE]BPF">added</a> the <code>SO_ATTACH_REUSEPORT_EBPF</code>
option to attach an eBPF program to select the target UDP socket. In Linux 4.6,
he <a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=c125e80b88687b25b321795457309eaaee4bf270" title="soreuseport: fast reuseport TCP socket selection">extended</a> it to support TCP. The <a href="https://manpages.debian.org/socket.7.html" title="Manual page for socket(7)"><code>socket(7)</code></a> manual page
documents this mechanism:<sup id="fnref:accuracy"><a class="footnote-ref" href="#fn:accuracy">1</a></sup></p>
<blockquote>
<p>The BPF program must return an index between 0 and N-1 representing the socket
which should receive the packet (where N is the number of sockets in the
group). If the BPF program returns an invalid index, socket selection will
fall back to the plain <code>SO_REUSEPORT</code> mechanism.</p>
</blockquote>
<p>In Linux 4.19, Martin KaFai Lau <a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=2dbb9b9e6df67d444fbe425c7f6014858d337adf" title="bpf: Introduce BPF_PROG_TYPE_SK_REUSEPORT">added</a> the
<a href="https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_SK_REUSEPORT/" title="eBPF Docs: program type BPF_PROG_TYPE_SK_REUSEPORT"><code>BPF_PROG_TYPE_SK_REUSEPORT</code></a> program type. Such an eBPF program
selects the socket from a <code>BPF_MAP_TYPE_REUSEPORT_ARRAY</code> map instead. This new
approach is more reliable when switching target sockets from one instance to
another—for example, when upgrading, a new instance can add its sockets and
remove the old ones.</p>
<h1 id="load-balancing-with-ebpf-and-go">Load-balancing with eBPF and Go</h1>
<p>Altering the load-balancing algorithm for a group of sockets requires two steps:</p>
<ol>
<li>write and compile an eBPF program in C,<sup id="fnref:rust"><a class="footnote-ref" href="#fn:rust">2</a></sup> and</li>
<li>load it and attach it in Go.</li>
</ol>
<h2 id="ebpf-program-in-c">eBPF program in C</h2>
<p>A simple load-balancing algorithm is to randomly choose the destination socket.
The kernel provides the <code>bpf_get_prandom_u32()</code> helper function to get a
pseudo-random number.</p>
<div class="language-c codehilite"><pre><span/><code><span class="k">volatile</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">__u32</span><span class="w"> </span><span class="n">num_sockets</span><span class="p">;</span><span class="w"> </span><span class="c1">// ❶</span>

<span class="k">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">__uint</span><span class="p">(</span><span class="n">type</span><span class="p">,</span><span class="w"> </span><span class="n">BPF_MAP_TYPE_REUSEPORT_SOCKARRAY</span><span class="p">);</span>
<span class="w">    </span><span class="n">__type</span><span class="p">(</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">__u32</span><span class="p">);</span>
<span class="w">    </span><span class="n">__type</span><span class="p">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">__u64</span><span class="p">);</span>
<span class="w">    </span><span class="n">__uint</span><span class="p">(</span><span class="n">max_entries</span><span class="p">,</span><span class="w"> </span><span class="mi">256</span><span class="p">);</span>
<span class="p">}</span><span class="w"> </span><span class="n">socket_map</span><span class="w"> </span><span class="n">SEC</span><span class="p">(</span><span class="s">".maps"</span><span class="p">);</span><span class="w"> </span><span class="c1">// ❷</span>

<span class="n">SEC</span><span class="p">(</span><span class="s">"sk_reuseport"</span><span class="p">)</span>
<span class="kt">int</span><span class="w"> </span><span class="n">reuseport_balance_prog</span><span class="p">(</span><span class="k">struct</span><span class="w"> </span><span class="nc">sk_reuseport_md</span><span class="w"> </span><span class="o">*</span><span class="n">reuse_md</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="n">__u32</span><span class="w"> </span><span class="n">index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bpf_get_prandom_u32</span><span class="p">()</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="n">num_sockets</span><span class="p">;</span><span class="w"> </span><span class="c1">// ❸</span>
<span class="w">    </span><span class="n">bpf_sk_select_reuseport</span><span class="p">(</span><span class="n">reuse_md</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">socket_map</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">index</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">);</span><span class="w"> </span><span class="c1">// ❹</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">SK_PASS</span><span class="p">;</span><span class="w"> </span><span class="c1">// ❺</span>
<span class="p">}</span>

<span class="kt">char</span><span class="w"> </span><span class="n">_license</span><span class="p">[]</span><span class="w"> </span><span class="n">SEC</span><span class="p">(</span><span class="s">"license"</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"GPL"</span><span class="p">;</span>
</code></pre></div>

<p>In ❶, we declare a volatile constant for the number of sockets in the group. We
will initialize this constant before loading the eBPF program into the kernel.
In ❷, we define the socket map. We will populate it with the socket file
descriptors. In ❸, we randomly select the index of the target socket.<sup id="fnref:random"><a class="footnote-ref" href="#fn:random">3</a></sup>
In ❹, we invoke the <code>bpf_sk_select_reuseport()</code> helper to record our decision.
Finally, in ❺, we accept the packet.</p>
<h3 id="header-files">Header files</h3>
<p>If you compile the C source with <code>clang</code>, you get errors due to missing headers.
The recommended way to solve this is to generate a <code>vmlinux.h</code> file with
<code>bpftool</code>:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>bpftool<span class="w"> </span>btf<span class="w"> </span>dump<span class="w"> </span>file<span class="w"> </span>/sys/kernel/btf/vmlinux<span class="w"> </span>format<span class="w"> </span>c<span class="w"> </span>&gt;<span class="w"> </span>vmlinux.h
</code></pre></div>

<p>Then, include the following headers:<sup id="fnref:kernel"><a class="footnote-ref" href="#fn:kernel">4</a></sup></p>
<div class="language-c codehilite"><pre><span/><code><span class="cp">#include</span><span class="w"> </span><span class="cpf">"vmlinux.h"</span>
<span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;bpf/bpf_helpers.h&gt;</span>
</code></pre></div>

<p>For my 6.17 kernel, the generated <code>vmlinux.h</code> is quite big: 2.7 MiB. Moreover,
<code>bpf/bpf_helpers.h</code> is shipped with libbpf. This adds another dependency for
users. As the eBPF program is quite small, I prefer to put the strict minimum in
<code>vmlinux.h</code> by <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/main/vmlinux.h" title="vmlinux.h file">cherry-picking the definitions I need</a>.</p>
<h3 id="compilation">Compilation</h3>
<p>The <a href="https://ebpf-go.dev/" title="The eBPF Library for Go">eBPF Library for Go</a> ships <code>bpf2go</code>, a tool to compile eBPF programs and
to generate some scaffolding code. We create a <code>gen.go</code> file with the following
content:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kn">package</span><span class="w"> </span><span class="nx">main</span>

<span class="c1">//go:generate go tool bpf2go -tags linux reuseport reuseport_kern.c</span>
</code></pre></div>

<p>After running <code>go generate ./...</code>, we can inspect the resulting objects with
<code>readelf</code> and <code>llvm-objdump</code>:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>readelf<span class="w"> </span>-S<span class="w"> </span>reuseport_bpfeb.o
<span class="go">There are 14 section headers, starting at offset 0x840:</span>
<span class="go">  [Nr] Name              Type             Address           Offset</span>
<span class="go">[…]</span>
<span class="go">  [ 3] sk_reuseport      PROGBITS         0000000000000000  00000040</span>
<span class="go">  [ 6] .maps             PROGBITS         0000000000000000  000000c8</span>
<span class="go">  [ 7] license           PROGBITS         0000000000000000  000000e8</span>
<span class="go">[…]</span>
<span class="gp">$ </span>llvm-objdump<span class="w"> </span>-S<span class="w"> </span>reuseport_bpfeb.o
<span class="go">reuseport_bpfeb.o:  file format elf64-bpf</span>
<span class="go">Disassembly of section sk_reuseport:</span>
<span class="go">0000000000000000 &lt;reuseport_balance_prog&gt;:</span>
<span class="go">; {</span>
<span class="go">       0:   bf 61 00 00 00 00 00 00     r6 = r1</span>
<span class="go">;     __u32 index = bpf_get_prandom_u32() % num_sockets;</span>
<span class="go">       1:   85 00 00 00 00 00 00 07     call 0x7</span>
<span class="go">[…]</span>
</code></pre></div>

<h2 id="usage-from-go">Usage from Go</h2>
<p>Let’s set up 10 workers listening to the same port.<sup id="fnref:listenaddr"><a class="footnote-ref" href="#fn:listenaddr">5</a></sup> Each socket
enables the <code>SO_REUSEPORT</code> option before binding:<sup id="fnref:s1"><a class="footnote-ref" href="#fn:s1">6</a></sup></p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">var</span><span class="w"> </span><span class="p">(</span>
<span class="w">    </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span>
<span class="w">    </span><span class="nx">fds</span><span class="w"> </span><span class="p">[]</span><span class="kt">uintptr</span>
<span class="w">    </span><span class="nx">conns</span><span class="w"> </span><span class="p">[]</span><span class="o">*</span><span class="nx">net</span><span class="p">.</span><span class="nx">UDPConn</span>
<span class="p">)</span>
<span class="nx">workers</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">10</span>
<span class="nx">listenAddr</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">"127.0.0.1:0"</span>
<span class="nx">listenConfig</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">ListenConfig</span><span class="p">{</span>
<span class="w">    </span><span class="nx">Control</span><span class="p">:</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="w"> </span><span class="nx">syscall</span><span class="p">.</span><span class="nx">RawConn</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">c</span><span class="p">.</span><span class="nx">Control</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">fd</span><span class="w"> </span><span class="kt">uintptr</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="hll"><span class="w">            </span><span class="nx">err</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SetsockoptInt</span><span class="p">(</span><span class="kt">int</span><span class="p">(</span><span class="nx">fd</span><span class="p">),</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SOL_SOCKET</span><span class="p">,</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SO_REUSEPORT</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span>
</span><span class="w">            </span><span class="nx">fds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">fds</span><span class="p">,</span><span class="w"> </span><span class="nx">fd</span><span class="p">)</span>
<span class="w">        </span><span class="p">})</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="nx">err</span>
<span class="w">    </span><span class="p">},</span>
<span class="p">}</span>
<span class="k">for</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">workers</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">pconn</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">listenConfig</span><span class="p">.</span><span class="nx">ListenPacket</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">Context</span><span class="p">(),</span><span class="w"> </span><span class="s">"udp"</span><span class="p">,</span><span class="w"> </span><span class="nx">listenAddr</span><span class="p">)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"ListenPacket() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="nx">udpConn</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">pconn</span><span class="p">.(</span><span class="o">*</span><span class="nx">net</span><span class="p">.</span><span class="nx">UDPConn</span><span class="p">)</span>
<span class="w">    </span><span class="nx">listenAddr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">udpConn</span><span class="p">.</span><span class="nx">LocalAddr</span><span class="p">().</span><span class="nx">String</span><span class="p">()</span>
<span class="w">    </span><span class="nx">conns</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">conns</span><span class="p">,</span><span class="w"> </span><span class="nx">udpConn</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div>

<p>The second step is to load the eBPF program, initialize the <code>num_sockets</code>
variable, populate the socket map, and attach the program to the first
socket.<sup id="fnref:s2"><a class="footnote-ref" href="#fn:s2">7</a></sup></p>
<div class="language-go codehilite"><pre><span/><code><span class="c1">// Load the eBPF collection.</span>
<span class="nx">spec</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">loadReuseport</span><span class="p">()</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"loadVariables() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// Set "num_sockets" global variable to the number of file descriptors we will register</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">spec</span><span class="p">.</span><span class="nx">Variables</span><span class="p">[</span><span class="s">"num_sockets"</span><span class="p">].</span><span class="nx">Set</span><span class="p">(</span><span class="kt">uint32</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">fds</span><span class="p">)));</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"NumSockets.Set() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// Load the map and the program into the kernel.</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">objs</span><span class="w"> </span><span class="nx">reuseportObjects</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">spec</span><span class="p">.</span><span class="nx">LoadAndAssign</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">objs</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"loadReuseportObjects() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">t</span><span class="p">.</span><span class="nx">Cleanup</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">objs</span><span class="p">.</span><span class="nx">Close</span><span class="p">()</span><span class="w"> </span><span class="p">})</span>

<span class="c1">// Assign the file descriptors to the socket map.</span>
<span class="k">for</span><span class="w"> </span><span class="nx">worker</span><span class="p">,</span><span class="w"> </span><span class="nx">fd</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">fds</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">objs</span><span class="p">.</span><span class="nx">reuseportMaps</span><span class="p">.</span><span class="nx">SocketMap</span><span class="p">.</span><span class="nx">Put</span><span class="p">(</span><span class="kt">uint32</span><span class="p">(</span><span class="nx">worker</span><span class="p">),</span><span class="w"> </span><span class="kt">uint64</span><span class="p">(</span><span class="nx">fd</span><span class="p">));</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"SocketMap.Put() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Attach the eBPF program to the first socket.</span>
<span class="nx">socketFD</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="kt">int</span><span class="p">(</span><span class="nx">fds</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="nx">progFD</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">objs</span><span class="p">.</span><span class="nx">reuseportPrograms</span><span class="p">.</span><span class="nx">ReuseportBalanceProg</span><span class="p">.</span><span class="nx">FD</span><span class="p">()</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SetsockoptInt</span><span class="p">(</span><span class="nx">socketFD</span><span class="p">,</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SOL_SOCKET</span><span class="p">,</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SO_ATTACH_REUSEPORT_EBPF</span><span class="p">,</span><span class="w"> </span><span class="nx">progFD</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"SetsockoptInt() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div>

<p>We are now ready to process incoming packets. Each worker is a goroutine
incrementing a counter for each received packet:<sup id="fnref:s3"><a class="footnote-ref" href="#fn:s3">8</a></sup></p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">var</span><span class="w"> </span><span class="nx">wg</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span>
<span class="nx">receivedPackets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">workers</span><span class="p">)</span>
<span class="k">for</span><span class="w"> </span><span class="nx">worker</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">workers</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">conn</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conns</span><span class="p">[</span><span class="nx">worker</span><span class="p">]</span>
<span class="w">    </span><span class="nx">packets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">receivedPackets</span><span class="p">[</span><span class="nx">worker</span><span class="p">]</span>
<span class="w">    </span><span class="nx">wg</span><span class="p">.</span><span class="nx">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">payload</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="mi">9000</span><span class="p">)</span>
<span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Read</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">                </span><span class="k">if</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nx">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">ErrClosed</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">                    </span><span class="k">return</span>
<span class="w">                </span><span class="p">}</span>
<span class="w">                </span><span class="nx">t</span><span class="p">.</span><span class="nx">Logf</span><span class="p">(</span><span class="s">"Read() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">            </span><span class="p">}</span>
<span class="w">            </span><span class="o">*</span><span class="nx">packets</span><span class="o">++</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">    </span><span class="p">})</span>
<span class="p">}</span>
</code></pre></div>

<p>Let’s send 1000 packets:</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">sentPackets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">1000</span>
<span class="nx">conn</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">Dial</span><span class="p">(</span><span class="s">"udp"</span><span class="p">,</span><span class="w"> </span><span class="nx">conns</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">LocalAddr</span><span class="p">().</span><span class="nx">String</span><span class="p">())</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"Dial() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">defer</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Close</span><span class="p">()</span>
<span class="k">for</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">sentPackets</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"hello world!"</span><span class="p">));</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"Write() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>If we print the content of the <code>receivedPackets</code> array, we can check the
balancing works as expected, with each worker getting about 100 packets:</p>
<div class="language-text-only codehilite"><pre><span/><code>=== RUN   TestUDPWorkerBalancing
    balancing_test.go:84: receivedPackets[0] = 107
    balancing_test.go:84: receivedPackets[1] = 92
    balancing_test.go:84: receivedPackets[2] = 99
    balancing_test.go:84: receivedPackets[3] = 105
    balancing_test.go:84: receivedPackets[4] = 107
    balancing_test.go:84: receivedPackets[5] = 96
    balancing_test.go:84: receivedPackets[6] = 102
    balancing_test.go:84: receivedPackets[7] = 105
    balancing_test.go:84: receivedPackets[8] = 99
    balancing_test.go:84: receivedPackets[9] = 88

    balancing_test.go:91: receivedPackets = 1000
    balancing_test.go:92: sentPackets     = 1000
</code></pre></div>

<h2 id="graceful-restart">Graceful restart</h2>
<p>You can also use <code>SO_ATTACH_REUSEPORT_EBPF</code> to gracefully restart an
application. A new instance of the application binds to the same address and
prepares its own version of the socket map. Once it attaches the eBPF program to
the first socket, the kernel steers incoming packets to this new instance. The
old instance needs to drain the already-received packets before shutting down.</p>
<p>To check we are not losing any packet, we spawn a goroutine to send as many
packets as possible:</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">sentPackets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span>
<span class="nx">notSentPackets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span>
<span class="nx">done</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kt">chan</span><span class="w"> </span><span class="kt">bool</span><span class="p">)</span>
<span class="nx">conn</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">Dial</span><span class="p">(</span><span class="s">"udp"</span><span class="p">,</span><span class="w"> </span><span class="nx">conns1</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">LocalAddr</span><span class="p">().</span><span class="nx">String</span><span class="p">())</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"Dial() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">defer</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Close</span><span class="p">()</span>
<span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"hello world!"</span><span class="p">));</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="nx">notSentPackets</span><span class="o">++</span>
<span class="w">        </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="nx">sentPackets</span><span class="o">++</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">        </span><span class="k">select</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span>
<span class="w">            </span><span class="k">return</span>
<span class="w">        </span><span class="k">default</span><span class="p">:</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}()</span>
</code></pre></div>

<p>Then, while the Go routine runs, we start the second set of workers. Once they
are running, they start receiving packets. If we gracefully stop the initial set
of workers, not a single packet is lost!<sup id="fnref:s4"><a class="footnote-ref" href="#fn:s4">9</a></sup></p>
<div class="language-text-only codehilite"><pre><span/><code>=== RUN   TestGracefulRestart
    graceful_test.go:135: receivedPackets1[0] = 165
    graceful_test.go:135: receivedPackets1[1] = 195
    graceful_test.go:135: receivedPackets1[2] = 194
    graceful_test.go:135: receivedPackets1[3] = 190
    graceful_test.go:135: receivedPackets1[4] = 213
    graceful_test.go:135: receivedPackets1[5] = 187
    graceful_test.go:135: receivedPackets1[6] = 170
    graceful_test.go:135: receivedPackets1[7] = 190
    graceful_test.go:135: receivedPackets1[8] = 194
    graceful_test.go:135: receivedPackets1[9] = 155

    graceful_test.go:139: receivedPackets2[0] = 1631
    graceful_test.go:139: receivedPackets2[1] = 1582
    graceful_test.go:139: receivedPackets2[2] = 1594
    graceful_test.go:139: receivedPackets2[3] = 1611
    graceful_test.go:139: receivedPackets2[4] = 1571
    graceful_test.go:139: receivedPackets2[5] = 1660
    graceful_test.go:139: receivedPackets2[6] = 1587
    graceful_test.go:139: receivedPackets2[7] = 1605
    graceful_test.go:139: receivedPackets2[8] = 1631
    graceful_test.go:139: receivedPackets2[9] = 1689

    graceful_test.go:147: receivedPackets = 18014
    graceful_test.go:148: sentPackets     = 18014
</code></pre></div>

<p>Unfortunately, gracefully shutting down a UDP socket is not trivial in Go.<sup id="fnref:c"><a class="footnote-ref" href="#fn:c">10</a></sup>
Previously, we were terminating workers by closing their sockets. However, if we
close them too soon, the application loses packets that were assigned to them
but not yet processed. Before stopping, a worker needs to call <code>conn.Read()</code>
until there are no more packets. A solution is to set a deadline for
<code>conn.Read()</code> and check if we should stop the goroutine when the deadline is
exceeded:</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">payload</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="mi">9000</span><span class="p">)</span>
<span class="k">for</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">conn</span><span class="p">.</span><span class="nx">SetReadDeadline</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Now</span><span class="p">().</span><span class="nx">Add</span><span class="p">(</span><span class="mi">50</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">))</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Read</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nx">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">ErrDeadlineExceeded</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="k">select</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span>
<span class="w">                </span><span class="k">return</span>
<span class="w">            </span><span class="k">default</span><span class="p">:</span>
<span class="w">                </span><span class="k">continue</span>
<span class="w">            </span><span class="p">}</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">        </span><span class="nx">t</span><span class="p">.</span><span class="nx">Logf</span><span class="p">(</span><span class="s">"Read() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="o">*</span><span class="nx">packets</span><span class="o">++</span>
<span class="p">}</span>
</code></pre></div>

<p>With TCP, this aspect is simpler: after enabling the <code>net.ipv4.tcp_migrate_req</code>
sysctl, the kernel automatically migrates waiting connections to a random socket
in the same group. Alternatively, eBPF can also <a href="https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_SK_REUSEPORT/#socket-migration" title="eBPF and socket migration">control this
migration</a>. Both features are available since Linux 5.14.</p>
<h1 id="addendum">Addendum</h1>
<p>After implementing this strategy in Akvorado, all workers now drop packets! 😱</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>curl<span class="w"> </span>-s<span class="w"> </span><span class="m">127</span>.0.0.1:8080/api/v0/inlet/metrics<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span>-n<span class="w"> </span>s/akvorado_inlet_flow_input_udp_in_dropped//p
<span class="go">packets_total{listener="0.0.0.0:2055",worker="0"} 838673</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="1"} 843675</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="2"} 837922</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="3"} 841443</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="4"} 840668</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="5"} 850274</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="6"} 835488</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="7"} 834479</span>
</code></pre></div>

<p>The root cause is the default limit of 32 records for Kafka batch sizes. This
limit is too low because the brokers have a large overhead when handling each
batch: they need to <a href="https://www.confluent.io/blog/kafka-producer-internals-handling-producer-request/" title="Handling the Producer Request: Kafka Producer and Consumer Internals, Part 2">ensure they persist correctly before acknowledging
them</a>. Increasing the limit to <a href="https://github.com/akvorado/akvorado/commit/39cc3728261f5185ba85c5fdd8b034c4c9697a55">4096 records</a> fixes this issue.</p>
<p>While load-balancing incoming flows with eBPF remains useful, it did not solve
the main issue. At least the even distribution of dropped packets helped
identify the real bottleneck. 😅</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:accuracy">
<p>The current version of the manual page is incomplete and does not
cover the evolution introduced in Linux 4.19. There is a <a href="https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/commit/?id=41788bdd42312828532c4ddbadc0a4d28426d4fd" title="man/man7/socket.7: Fix documentation for SO_ATTACH_REUSEPORT_PORT">pending patch</a>
about this. <a class="footnote-backref" href="#fnref:accuracy" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:rust">
<p><a href="https://aya-rs.dev/book/" title="Building eBPF Programs with Aya">Rust</a> is another option. However, the program we use is so trivial that
it does not make sense to use Rust. <a class="footnote-backref" href="#fnref:rust" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:random">
<p>As <code>bpf_get_prandom_u32()</code> returns a pseudo-random 32-bit unsigned
value, this method exhibits a very slight bias towards the first indexes.
This is unlikely to be worth fixing. <a class="footnote-backref" href="#fnref:random" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:kernel">
<p>Some examples include <code>&lt;linux/bpf.h&gt;</code> instead of <code>"vmlinux.h"</code>. This
makes your eBPF program dependent on the installed kernel headers. <a class="footnote-backref" href="#fnref:kernel" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:listenaddr">
<p><code>listenAddr</code> is initially set to <code>127.0.0.1:0</code> to allocate a
random port. After the first iteration, it is updated with the allocated
port. <a class="footnote-backref" href="#fnref:listenaddr" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:s1">
<p>This is the <code>setupSockets()</code> function in <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/fixtures_test.go#L16-L38"><code>fixtures_test.go</code></a>. <a class="footnote-backref" href="#fnref:s1" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:s2">
<p>This is the <code>setupEBPF()</code> function in <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/fixtures_test.go#L40-L68"><code>fixtures_test.go</code></a>. <a class="footnote-backref" href="#fnref:s2" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:s3">
<p>The complete code is in <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/balancing_test.go"><code>balancing_test.go</code></a> <a class="footnote-backref" href="#fnref:s3" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:s4">
<p>The complete code is in <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/graceful_test.go"><code>graceful_test.go</code></a> <a class="footnote-backref" href="#fnref:s4" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:c">
<p>In C, we would <code>poll()</code> both the socket and a pipe used to signal for
shutdown. When the second condition is triggered, we drain the socket by
executing a series of non-blocking <code>read()</code> until we get <code>EWOULDBLOCK</code>. <a class="footnote-backref" href="#fnref:c" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Compressing embedded files in Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2025-go-embed-compressed" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2025-go-embed-compressed#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-12-07T22:05:14Z</updated>
    <id>http://www.luffy.cx/en/blog/2025-go-embed-compressed.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Go’s <a href="https://gobyexample.com/embed-directive" title="Go by Example: Embed Directive">embed</a> feature lets you bundle static assets into an executable, but it
stores them uncompressed. This wastes space: a web interface with documentation
can bloat your binary by dozens of megabytes. A <a href="https://github.com/golang/go/issues/61322" title="proposal: embed: add -embed=compress flag to go #61322">proposition to optionally
enable compression</a> was declined because it is difficult to handle all use
cases. One solution? Put all the assets into a ZIP archive! 🗜️</p>
<div class="toc">
<ul>
<li><a href="#code">Code</a></li>
<li><a href="#space-gain">Space gain</a></li>
<li><a href="#performance-loss">Performance loss</a></li>
</ul>
</div>
<h1 id="code">Code</h1>
<p>The Go standard library includes a <a href="https://pkg.go.dev/archive/zip" title="archive/zip on Go Packages">module to read and write ZIP archives</a>. It
contains a function that turns a ZIP archive into an <a href="https://pkg.go.dev/io/fs#FS" title="io/fs.FS on Go Packages"><code>io/fs.FS</code></a>
structure that can replace <a href="https://pkg.go.dev/embed#FS" title="embed.FS on Go Packages"><code>embed.FS</code></a> in most contexts.<sup id="fnref:missing"><a class="footnote-ref" href="#fn:missing">1</a></sup></p>
<div class="language-go codehilite"><pre><span/><code><span class="kn">package</span><span class="w"> </span><span class="nx">embed</span>

<span class="kn">import</span><span class="w"> </span><span class="p">(</span>
<span class="w">  </span><span class="s">"archive/zip"</span>
<span class="w">  </span><span class="s">"bytes"</span>
<span class="w">  </span><span class="nx">_</span><span class="w"> </span><span class="s">"embed"</span>
<span class="w">  </span><span class="s">"fmt"</span>
<span class="w">  </span><span class="s">"io/fs"</span>
<span class="w">  </span><span class="s">"sync"</span>
<span class="p">)</span>

<span class="c1">//go:embed data/embed.zip</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">embeddedZip</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span>

<span class="kd">var</span><span class="w"> </span><span class="nx">dataOnce</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">OnceValue</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="o">*</span><span class="nx">zip</span><span class="p">.</span><span class="nx">Reader</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">zip</span><span class="p">.</span><span class="nx">NewReader</span><span class="p">(</span><span class="nx">bytes</span><span class="p">.</span><span class="nx">NewReader</span><span class="p">(</span><span class="nx">embeddedZip</span><span class="p">),</span><span class="w"> </span><span class="kt">int64</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">embeddedZip</span><span class="p">)))</span>
<span class="w">  </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nb">panic</span><span class="p">(</span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Sprintf</span><span class="p">(</span><span class="s">"cannot read embedded archive: %s"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">))</span>
<span class="w">  </span><span class="p">}</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="nx">r</span>
<span class="p">})</span>

<span class="kd">func</span><span class="w"> </span><span class="nx">Data</span><span class="p">()</span><span class="w"> </span><span class="nx">fs</span><span class="p">.</span><span class="nx">FS</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="nx">dataOnce</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div>

<p>We can build the <code>embed.zip</code> archive with a rule in a <code>Makefile</code>. We specify the
files to embed as dependencies to ensure changes are detected.</p>
<div class="language-makefile codehilite"><pre><span/><code><span class="nf">common/embed/data/embed.zip</span><span class="o">:</span><span class="w"> </span><span class="n">console</span>/<span class="n">data</span>/<span class="n">frontend</span> <span class="n">console</span>/<span class="n">data</span>/<span class="n">docs</span>
<span class="nf">common/embed/data/embed.zip</span><span class="o">:</span><span class="w"> </span><span class="n">orchestrator</span>/<span class="n">clickhouse</span>/<span class="n">data</span>/<span class="n">protocols</span>.<span class="n">csv</span> 
<span class="nf">common/embed/data/embed.zip</span><span class="o">:</span><span class="w"> </span><span class="n">orchestrator</span>/<span class="n">clickhouse</span>/<span class="n">data</span>/<span class="n">icmp</span>.<span class="n">csv</span>
<span class="nf">common/embed/data/embed.zip</span><span class="o">:</span><span class="w"> </span><span class="n">orchestrator</span>/<span class="n">clickhouse</span>/<span class="n">data</span>/<span class="n">asns</span>.<span class="n">csv</span>
<span class="nf">common/embed/data/embed.zip</span><span class="o">:</span>
<span class="w">    </span>mkdir<span class="w"> </span>-p<span class="w"> </span>common/embed/data<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span>zip<span class="w"> </span>--quiet<span class="w"> </span>--recurse-paths<span class="w"> </span>--filesync<span class="w"> </span><span class="nv">$@</span><span class="w"> </span>$^
</code></pre></div>

<p>The <a href="https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html" title="GNU Make: Automatic Variables">automatic variable</a> <code>$@</code> is the rule target, while <code>$^</code> expands to all
the dependencies, modified or not.</p>
<h1 id="space-gain">Space gain</h1>
<p><a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a>, a flow collector written in Go, embeds several static assets:</p>
<ul>
<li>CSV files to translate port numbers, protocols, or AS numbers, and</li>
<li>HTML, CSS, JS, and image files for the web interface, and</li>
<li>the documentation.</li>
</ul>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1172px"><span class="lf-media-inner" style="padding-bottom: 48.294%"><img alt="Breakdown of space used by each package before and after introducing&#10;embed.zip. It is displayed as a treemap and we can see many embedded files&#10;replaced by a bigger one." src="https://media-bernat-ch.b-cdn.net/images/go-embed@1x.bd3e0b05874272.png" srcset="https://media-bernat-ch.b-cdn.net/images/go-embed@1x.bd3e0b05874272.png 1473w,https://media-bernat-ch.b-cdn.net/images/go-embed@2x.3f6d400bcda4ea.png 2344w" sizes="auto, (max-width: 972px) 100vw, 900px" width="1172" height="566" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAkoAAAEbAQMAAAAf1zkVAAAAA1BMVEWdnVjz7jvoAAAAK0lEQVR42u3BMQEAAADCoPVPbQlPoAAAAAAAAAAAAAAAAAAAAAAAAAAA4GBS6QABoXjikwAAAABJRU5ErkJggg==)"/></span></div><figcaption>Breakdown of the space used by each component before (left) and after (right) the introduction of <code>embed.zip</code>.</figcaption></figure>
<p>Embedding these assets into a ZIP archive reduced the size of the Akvorado
executable by more than 4 MiB:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>unzip<span class="w"> </span>-p<span class="w"> </span>common/embed/data/embed.zip<span class="w"> </span><span class="p">|</span><span class="w"> </span>wc<span class="w"> </span>-c<span class="w"> </span><span class="p">|</span><span class="w"> </span>numfmt<span class="w"> </span>--to<span class="o">=</span>iec
<span class="go">7.3M</span>
<span class="gp">$ </span>ll<span class="w"> </span>common/embed/data/embed.zip
<span class="go">-rw-r--r-- 1 bernat users 2.9M Dec  7 17:17 common/embed/data/embed.zip</span>
</code></pre></div>

<h1 id="performance-loss">Performance loss</h1>
<p>Reading from a compressed archive is not as fast as reading a flat file. A
<a href="https://github.com/akvorado/akvorado/blob/84bd9a64645667902cb57a5cf5a59749810928cc/common/embed/fs_test.go#L35-L51">simple benchmark</a> shows it is more than 4× slower. It also allocates some
memory.<sup id="fnref:improvements"><a class="footnote-ref" href="#fn:improvements">2</a></sup></p>
<div class="language-text-only codehilite"><pre><span/><code>goos: linux
goarch: amd64
pkg: akvorado/common/embed
cpu: AMD Ryzen 5 5600X 6-Core Processor
BenchmarkData/compressed-12     2262   526553 ns/op   610 B/op   10 allocs/op
BenchmarkData/uncompressed-12   9482   123175 ns/op     0 B/op    0 allocs/op
</code></pre></div>

<p>Each access to an asset requires a decompression step, as seen in this flame
graph:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1200px"><span class="lf-media-inner" style="padding-bottom: 23.167%"><object width="1200" height="278" type="image/svg+xml" data="https://media-bernat-ch.b-cdn.net/images/go-embed-flamegraph.72b79c037a2564.svg" class="lf-media">🖼 Flame graph when reading data from embed.zip compared to reading data
directly</object></span></div><figcaption>CPU flame graph comparing the time spent on CPU when reading data from embed.zip (left) versus reading data directly (right). Because the Go testing framework executes the benchmark for uncompressed data 4 times more often, it uses the same horizontal space as the benchmark for compressed data. The graph is interactive.</figcaption></figure>
<p>While a ZIP archive has an index to quickly find the requested file, seeking
inside a compressed file is currently not possible.<sup id="fnref:seek"><a class="footnote-ref" href="#fn:seek">3</a></sup> Therefore, the files
from a compressed archive do not implement the <code>io.ReaderAt</code> or <code>io.Seeker</code>
interfaces, unlike directly embedded files. This prevents some features, like
serving partial files or detecting MIME types when serving files over HTTP.</p>
<hr/>
<p>For Akvorado, this is an acceptable compromise to save a few mebibytes from an
executable of almost 100 MiB. Next week, I will continue this futile adventure
by explaining how I <a href="https://github.com/akvorado/akvorado/pull/2112" title="Don't disable DCE #2112">prevented Go from disabling dead code elimination</a>! 🦥</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:missing">
<p>You can safely read multiple files concurrently. However, it does
not implement the <code>ReadDir()</code> and <code>ReadFile()</code> methods. <a class="footnote-backref" href="#fnref:missing" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:improvements">
<p>You could keep frequently accessed assets in memory. This
reduces CPU usage and trades cached memory for resident memory. <a class="footnote-backref" href="#fnref:improvements" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:seek">
<p><a href="https://github.com/sozip/sozip-spec">SOZip</a> is a profile that enables fast random access in a compressed
file. However, Go’s <code>archive/zip</code> module does not support it. <a class="footnote-backref" href="#fnref:seek" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Akvorado 2.0</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2025-akvorado-2.0" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2025-akvorado-2.0#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-09-22T06:12:52Z</updated>
    <id>http://www.luffy.cx/en/blog/2025-akvorado-2.0.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="https://github.com/akvorado/akvorado/releases/tag/v2.0.0">Akvorado 2.0</a> was released today! <a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> collects network flows with
<abbr title="IP Flow Information Export">IPFIX</abbr> and sFlow. It enriches flows and stores them in a ClickHouse database.
Users can browse the data through a web console. This release introduces an
important architectural change and other smaller improvements. Let’s dive in! 🤿</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>diff<span class="w"> </span>--shortstat<span class="w"> </span>v1.11.5
<span class="go"> 493 files changed, 25015 insertions(+), 21135 deletions(-)</span>
</code></pre></div>

<div class="toc">
<ul>
<li><a href="#new-outlet-service">New “outlet” service</a></li>
<li><a href="#other-changes">Other changes</a><ul>
<li><a href="#observability">Observability</a></li>
<li><a href="#documentation">Documentation</a></li>
<li><a href="#continuous-integration">Continuous integration</a></li>
<li><a href="#docker">Docker</a></li>
<li><a href="#go">Go</a><ul>
<li><a href="#toolchain">Toolchain</a></li>
<li><a href="#testing">Testing</a></li>
<li><a href="#another-package-for-kafka">Another package for Kafka</a></li>
<li><a href="#improved-routing-table-for-bmp">Improved routing table for BMP</a></li>
<li><a href="#miscellaneous">Miscellaneous</a></li>
</ul>
</li>
<li><a href="#javascript">JavaScript</a></li>
</ul>
</li>
<li><a href="#next-steps">Next steps</a></li>
</ul>
</div>
<h1 id="new-outlet-service">New “outlet” service</h1>
<p>The major change in Akvorado 2.0 is <strong>splitting the inlet service</strong> into two
parts: the <strong>inlet</strong> and the <strong>outlet</strong>. Previously, the inlet handled all flow
processing: receiving, decoding, and enrichment. Flows were then sent to Kafka
for storage in ClickHouse:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 542px"><span class="lf-media-inner" style="padding-bottom: 40.037%"><img alt="Akvorado flow processing before the change: flows are received and processed&#10;by the inlet, sent to Kafka and stored in&#10;ClickHouse" src="https://media-bernat-ch.b-cdn.net/images/akvorado/before-outlet.d3c1c81ca52d28.svg" width="542" height="217" class="lf-media"/></span></div><figcaption>Akvorado flow processing before the introduction of the outlet service</figcaption></figure>
<p>Network flows reach the inlet service using UDP, an unreliable protocol. The
inlet must process them <strong>fast enough to avoid losing packets</strong>. To handle a
high number of flows, the inlet spawns several sets of workers to receive flows,
fetch metadata, and assemble enriched flows for Kafka. Many configuration
options existed for scaling, which increased complexity for users. The code
needed to <strong>avoid blocking at any cost</strong>, making the processing pipeline complex
and sometimes unreliable, particularly the <abbr title="BGP Monitoring Protocol">BMP</abbr> receiver.<sup id="fnref:bmp"><a class="footnote-ref" href="#fn:bmp">1</a></sup> Adding new
features became difficult without making the problem worse.<sup id="fnref:features"><a class="footnote-ref" href="#fn:features">2</a></sup></p>
<p>In Akvorado 2.0, the inlet receives flows and pushes them to Kafka without
decoding them. The new outlet service handles the remaining tasks:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 732px"><span class="lf-media-inner" style="padding-bottom: 29.645%"><img alt="Akvorado flow processing after the change: flows are received by the inlet,&#10;sent to Kafka, processed by the outlet and inserted in ClickHouse" src="https://media-bernat-ch.b-cdn.net/images/akvorado/after-outlet.9e9a485126fdf3.svg" width="732" height="217" class="lf-media"/></span></div><figcaption>Akvorado flow processing after the introduction of the outlet service</figcaption></figure>
<p>This change goes beyond a simple split:<sup id="fnref:size"><a class="footnote-ref" href="#fn:size">3</a></sup> the outlet now reads flows from
Kafka and pushes them to ClickHouse, two tasks that Akvorado did not handle
before. Flows are heavily <strong>batched to increase efficiency</strong> and reduce the load
on ClickHouse using <a href="https://github.com/ClickHouse/ch-go/" title="Low-level Go Client for ClickHouse">ch-go</a>, a low-level Go client for ClickHouse. When
batches are too small, asynchronous inserts are used (<a href="https://github.com/akvorado/akvorado/commit/e20645c92e38" title="outlet/metadata: synchronous fetching of metadata">e20645</a>). The number of
outlet <strong>workers scales dynamically</strong> (<a href="https://github.com/akvorado/akvorado/commit/e5a625aecf9e" title="outlet: make the number of Kafka workers dynamic">e5a625</a>) based on the target batch
size and latency (50,000 flows and 5 seconds by default).</p>
<p>This new architecture also allows us to <strong>simplify and optimize</strong> the code. The
outlet fetches metadata synchronously (<a href="https://github.com/akvorado/akvorado/commit/e20645c92e38" title="outlet/metadata: synchronous fetching of metadata">e20645</a>). The <abbr title="BGP Monitoring Protocol">BMP</abbr> component becomes
simpler by removing cooperative multitasking (<a href="https://github.com/akvorado/akvorado/commit/3b9486b000c0" title="outlet/routing: remove batching of peer removal">3b9486</a>). Reusing the same
<code>RawFlow</code> object to decode protobuf-encoded flows from Kafka reduces pressure on
the garbage collector (<a href="https://github.com/akvorado/akvorado/commit/8b580fd26bfc" title="outlet/core: reuse the same RawFlow object when decoding">8b580f</a>).</p>
<p>The effect on Akvorado’s overall performance was somewhat uncertain, but a
<a href="https://github.com/akvorado/akvorado/issues/1908#issuecomment-3233143146">user reported</a> <strong>35% lower CPU usage</strong> after migrating from the previous
version, plus resolution of the long-standing <abbr title="BGP Monitoring Protocol">BMP</abbr> component issue. 🥳</p>
<h1 id="other-changes">Other changes</h1>
<p>This new version includes many miscellaneous changes, such as completion for
source and destination ports (<a href="https://github.com/akvorado/akvorado/commit/f92d2ef9e2bd" title="console/filter: add autocomplete for src and dst ports">f92d2e</a>), and automatic restart of the
orchestrator service (<a href="https://github.com/akvorado/akvorado/commit/0f72ffd6761b" title="cmd: automatic restart of orchestrator on configuration change">0f72ff</a>) when configuration changes to avoid a common
pitfall for newcomers.</p>
<p>Let’s focus on some key areas for this release: <a href="#observability">observability</a>,
<a href="#documentation">documentation</a>, <a href="#continuous-integration"><abbr title="Continuous integration">CI</abbr></a>,
<a href="#docker">Docker</a>, <a href="#go">Go</a>, and <a href="#javascript">JavaScript</a>.</p>
<h2 id="observability">Observability</h2>
<p>Akvorado exposes metrics to provide visibility into the processing pipeline and
help troubleshoot issues. These are available through <strong>Prometheus HTTP metrics
endpoints</strong>, such as <a href="https://demo.akvorado.net/api/v0/inlet/metrics"><code>/api/v0/inlet/metrics</code></a>. With the introduction
of the outlet, many metrics moved. Some were also renamed (<a href="https://github.com/akvorado/akvorado/commit/4c0b15e1cd4e" title="inlet/outlet: rename a few metrics">4c0b15</a>) to match
<a href="https://prometheus.io/docs/practices/naming/">Prometheus best practices</a>. Kafka consumer lag was added as a new metric
(<a href="https://github.com/akvorado/akvorado/commit/e3a778552d6e" title="outlet/kafka: expose consumer lag as a prometheus metric">e3a778</a>).</p>
<p>If you do not have your own observability stack, the Docker Compose setup
shipped with Akvorado provides one. You can enable it by activating the profiles
introduced for this purpose (<a href="https://github.com/akvorado/akvorado/commit/529a8f69bebc" title="docker: use profiles to optionally enable Prometheus, Loki and Grafana">529a8f</a>).</p>
<p>The <code>prometheus</code> profile ships <a href="https://prometheus.io/" title="Prometheus: Open source metrics and monitoring">Prometheus</a> to <strong>store metrics</strong> and <a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Alloy</a>
to <strong>collect them</strong> (<a href="https://github.com/akvorado/akvorado/commit/2b3c46372985" title="docker: switch from prometheus to alloy for scrapping metrics">2b3c46</a>, <a href="https://github.com/akvorado/akvorado/commit/f81299ff2816" title="docker: group services together in alloy configuration">f81299</a>, and <a href="https://github.com/akvorado/akvorado/commit/8eb7cd63b1c0" title="docker: make Alloy configuration use only Docker labels">8eb7cd</a>). Redis and Kafka
metrics are collected through the exporter bundled with Alloy (<a href="https://github.com/akvorado/akvorado/commit/560113e3af24" title="docker: monitor Redis and Kafka using Alloy internal exporter">560113</a>).
Other metrics are exposed using Prometheus metrics endpoints and are
automatically fetched by Alloy with the help of some Docker labels, similar to
what is done to configure Traefik. <a href="https://github.com/google/cadvisor/" title="Analyzes resource usage and performance characteristics of running containers">cAdvisor</a> was also added (<a href="https://github.com/akvorado/akvorado/commit/83d8556d6ddb" title="docker: add cAdvisor for container monitoring">83d855</a>) to
provide some container-related metrics.</p>
<p>The <code>loki</code> profile ships <a href="https://grafana.com/docs/loki/latest/" title="Grafana Loki documentation">Loki</a> to <strong>store logs</strong> (<a href="https://github.com/akvorado/akvorado/commit/45c684dc6737" title="docker: add Loki to the observability stack">45c684</a>). While Alloy
can collect and ship logs to Loki, its parsing abilities are limited: I could
not find a way to preserve all metadata associated with structured logs produced
by many applications, including Akvorado. <a href="https://vector.dev/" title="Vector: lightweight, ultra-fast tool for building observability pipelines">Vector</a> replaces Alloy (<a href="https://github.com/akvorado/akvorado/commit/95e2011d0c67" title="docker: replace Alloy by Vector for parsing logs">95e201</a>)
and features a domain-specific language, <abbr title="Vector Remap Language">VRL</abbr>, to transform logs. Annoyingly,
Vector currently <a href="https://github.com/vectordotdev/vector/issues/7358" title="Support checkpointing and reading old logs with docker_logs source">cannot retrieve Docker logs from before it was
started</a>.</p>
<div class="admonition">
<p class="admonition-title">Update (2025-11)</p>
<p>Alloy can now preserve the fields from the structured logs into the metadata
using a regular expression. See <a href="https://github.com/grafana/alloy/pull/4629" title="feat(loki.process): add regex field to structured_metadata stage">PR #4629</a>.</p>
</div>
<p>Finally, the <code>grafana</code> profile ships <a href="https://grafana.com/docs/grafana/latest/" title="Grafana documentation">Grafana</a>, but the shipped dashboards are
broken. This is planned for a future version.</p>
<h2 id="documentation">Documentation</h2>
<p>The Docker Compose setup provided by Akvorado makes it easy to get the web
interface up and running quickly. However, Akvorado requires a few mandatory
steps to be functional. It ships with <a href="https://demo.akvorado.net/docs">comprehensive documentation</a>, including
a chapter about <a href="https://demo.akvorado.net/docs/troubleshooting">troubleshooting problems</a>. I hoped this documentation would
reduce the support burden. It is difficult to know if it works. Happy users
rarely report their success, while some users open discussions asking for help
without reading much of the documentation.</p>
<p>In this release, the <strong>documentation was significantly improved</strong>.</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>diff<span class="w"> </span>--shortstat<span class="w"> </span>v1.11.5<span class="w"> </span>--<span class="w"> </span>console/data/docs
<span class="go"> 10 files changed, 1873 insertions(+), 1203 deletions(-)</span>
</code></pre></div>

<p>The documentation was updated (<a href="https://github.com/akvorado/akvorado/commit/fc1028bfcdc5" title="doc: documentation update for inlet/outlet separation">fc1028</a>) to match Akvorado’s new architecture.
The troubleshooting section was rewritten (<a href="https://github.com/akvorado/akvorado/commit/17a272d0ba18" title="docs: update troubleshooting documentation">17a272</a>). Instructions on how to
improve ClickHouse performance when upgrading from versions earlier than 1.10.0
were added (<a href="https://github.com/akvorado/akvorado/commit/5f1e9a49c7d5" title="docs: document schema update for installations before 1.10.0">5f1e9a</a>). An <abbr title="Large Language Model">LLM</abbr> proofread the entire content (<a href="https://github.com/akvorado/akvorado/commit/06e3f334fdb3" title="docs: proofread the whole documentation">06e3f3</a>).
Developer-focused documentation was also improved (<a href="https://github.com/akvorado/akvorado/commit/548bbbfca161" title="docs: explain more how to develop on the frontend">548bbb</a>, <a href="https://github.com/akvorado/akvorado/commit/e41bae008b68" title="docs: make it a bit simpler to hack Akvorado">e41bae</a>, and
<a href="https://github.com/akvorado/akvorado/commit/871fc5981498" title="docs: make it even easier to hack on the console">871fc5</a>).</p>
<p>From a usability perspective, table of contents sections are now collapsible
(<a href="https://github.com/akvorado/akvorado/commit/c142e58110b3" title="console/frontend: make TOC collapsable in documentation">c142e5</a>). Admonitions help draw user attention to important points
(<a href="https://github.com/akvorado/akvorado/commit/8ac89407fd49" title="console: add admonitions to documentation markdown">8ac894</a>).</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 693px"><span class="lf-media-inner" style="padding-bottom: 67.677%"><img alt="Admonition in Akvorado documentation to ask a user not to open an issue or&#10;start a discussion before reading the documentation" src="https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@1x.1322ddc0bd4b5b.png" srcset="https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@1x.1322ddc0bd4b5b.png 871w,https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@2x.2ad1061da0399d.png 1386w" sizes="auto, (max-width: 693px) 100vw, 693px" width="693" height="469" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGMAAABDAQMAAACyWSngAAAAA1BMVEX5+PvrZTWkAAAAEUlEQVR42mNgGAWjYBQMfgAAA6oAASjPUCUAAAAASUVORK5CYII=)"/></span></div><figcaption>Example of use of admonitions in Akvorado's documentation</figcaption></figure>
<h2 id="continuous-integration">Continuous integration</h2>
<p>This release includes efforts to <strong>speed up</strong> continuous integration on GitHub.
Coverage and race tests run in parallel (<a href="https://github.com/akvorado/akvorado/commit/6af2163066aa" title="build: speed up CI by running coverage and race tests in parallel">6af216</a> and <a href="https://github.com/akvorado/akvorado/commit/fa9e48d16a90" title="build: run a third job for misc tests">fa9e48</a>). The Docker
image builds during the tests but gets tagged only after they succeed
(<a href="https://github.com/akvorado/akvorado/commit/8b0dcee14f60" title="build: build Docker image earlier but tag later">8b0dce</a>).</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1358px"><span class="lf-media-inner" style="padding-bottom: 36.672%"><img alt="GitHub workflow for CI with many jobs, some of them running in parallel, some&#10;not" src="https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@1x.d3576897765cd8.png" srcset="https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@1x.d3576897765cd8.png 1707w,https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@2x.085acee6fbcf28.png 2716w" sizes="auto, (max-width: 972px) 100vw, 900px" width="1358" height="498" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAqcAAAD5AQMAAAAgMA7pAAAAA1BMVEX19/mL6bEjAAAAK0lEQVR42u3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAAAAAAAAAADAuwFTpgABD31LnAAAAABJRU5ErkJggg==)"/></span></div><figcaption>GitHub workflow to test and build Akvorado</figcaption></figure>
<p>End-to-end tests (<a href="https://github.com/akvorado/akvorado/commit/883e19922eee" title="build: add end-to-end testing">883e19</a>) ensure the shipped Docker Compose setup works as
expected. <a href="https://hurl.dev/">Hurl</a> runs tests on various HTTP endpoints, particularly to verify
metrics (<a href="https://github.com/akvorado/akvorado/commit/42679b1f2db3" title="build: use hurl for end-to-end tests">42679b</a> and <a href="https://github.com/akvorado/akvorado/commit/169fa933af64" title="build: add more e2e tests by testing Prometheus metrics">169fa9</a>). For example:</p>
<div class="language-hurl codehilite"><pre><span/><code><span class="c1">## Test inlet has received NetFlow flows</span>
<span class="kr">GET</span><span class="w"> </span><span class="sx">http://127.0.0.1:8080/prometheus/api/v1/query</span>
<span class="nn">[Query]</span>
<span class="na">query</span><span class="p">:</span><span class="w"> </span>sum(akvorado_inlet_flow_input_udp_packets_total{job="akvorado-inlet",listener=":2055"})
<span class="k">HTTP</span><span class="w"> </span><span class="mi">200</span>
<span class="nn">[Captures]</span>
<span class="na">inlet_receivedflows</span><span class="p">:</span><span class="w"> </span>jsonpath "$.data.result[0].value[1]" toInt
<span class="nn">[Asserts]</span>
<span class="k">variable</span><span class="w"> </span><span class="s2">"inlet_receivedflows"</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">10</span>

<span class="c1">## Test inlet has sent them to Kafka</span>
<span class="kr">GET</span><span class="w"> </span><span class="sx">http://127.0.0.1:8080/prometheus/api/v1/query</span>
<span class="nn">[Query]</span>
<span class="na">query</span><span class="p">:</span><span class="w"> </span>sum(akvorado_inlet_kafka_sent_messages_total{job="akvorado-inlet"})
<span class="k">HTTP</span><span class="w"> </span><span class="mi">200</span>
<span class="nn">[Captures]</span>
<span class="na">inlet_sentflows</span><span class="p">:</span><span class="w"> </span>jsonpath "$.data.result[0].value[1]" toInt
<span class="nn">[Asserts]</span>
<span class="k">variable</span><span class="w"> </span><span class="s2">"inlet_sentflows"</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="p">{{</span><span class="w"> </span><span class="nv">inlet_receivedflows</span><span class="w"> </span><span class="p">}}</span>
</code></pre></div>

<h2 id="docker">Docker</h2>
<p>Akvorado ships with a comprehensive <a href="https://docs.docker.com/compose/" title="Docker Compose documentation">Docker Compose</a> setup to help users <strong>get
started quickly</strong>. It ensures a consistent deployment, eliminating many
configuration-related issues. It also serves as a living documentation of the
complete architecture.</p>
<p>This release brings some small enhancements around Docker:</p>
<ul>
<li>an example to configure Traefik for TLS with <a href="https://letsencrypt.org/">Let’s Encrypt</a> (<a href="https://github.com/akvorado/akvorado/commit/1a27bb1bc2a3" title="docker: add examples to enable authentication and TLS">1a27bb</a>),</li>
<li>HTTP compression (<a href="https://github.com/akvorado/akvorado/commit/bee9a5de6771" title="docker: enable HTTP compression for Traefik">bee9a5</a>), and</li>
<li>support of IPv6 (<a href="https://github.com/akvorado/akvorado/commit/a74a41a6a049" title="docker: enable IPv6 networking, use a specific IPv4 subnet">a74a41</a>).</li>
</ul>
<p>Previously, many Docker images were pulled from the <a href="https://github.com/bitnami/containers/">Bitnami Containers
library</a>. However, VMWare acquired Bitnami in 2019 and Broadcom acquired
VMWare in 2023. As a result, <a href="https://github.com/bitnami/containers/issues/83267" title="Upcoming changes to the Bitnami catalog (effective August 28th, 2025)">Bitnami images were deprecated</a> in less than a
month. This was not really a surprise.<sup id="fnref:broadcom"><a class="footnote-ref" href="#fn:broadcom">4</a></sup> Previous versions of Akvorado
had already started moving away from them. In this release, the Apache project’s
Kafka image replaces the Bitnami one (<a href="https://github.com/akvorado/akvorado/commit/1eb38226fc23" title="docker: switch to Apache Kafka">1eb382</a>). Thanks to the switch to KRaft
mode, Zookeeper is no longer needed (<a href="https://github.com/akvorado/akvorado/commit/0a2ea1b1a189" title="docker: remove zookeeper for ClickHouse cluster as well">0a2ea1</a>, <a href="https://github.com/akvorado/akvorado/commit/8a49ca40d31b" title="docker: remove Zookeeper from default setup">8a49ca</a>, and <a href="https://github.com/akvorado/akvorado/commit/f65d20443622" title="docker: remove use of Zookeeper for Kafka">f65d20</a>).</p>
<p>Akvorado’s Docker images were previously compiled with Nix. However, building
AArch64 images on x86-64 is slow because it relies on QEMU userland emulation.
The updated <code>Dockerfile</code> uses <a href="https://docs.docker.com/build/building/multi-stage/" title="Multi-stage builds">multi-stage</a> and <a href="https://docs.docker.com/build/building/multi-platform/" title="Multi-platform builds">multi-platform</a> builds: one
stage builds the JavaScript part on the host platform, one stage builds the Go
part cross-compiled on the host platform, and the final stage assembles the
image on top of a <a href="https://github.com/GoogleContainerTools/distroless" title="Language focused docker images, minus the operating system">slim distroless image</a> (<a href="https://github.com/akvorado/akvorado/commit/268e95ee1891" title="docker: switch away from Nix to build Docker images">268e95</a> and <a href="https://github.com/akvorado/akvorado/commit/d526ca451ce1" title="build: add a platform independant step into Makefile">d526ca</a>).</p>
<div class="language-docker codehilite"><pre><span/><code><span class="c"># This is a simplified version</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">--platform=$BUILDPLATFORM</span><span class="w"> </span>node:20-alpine<span class="w"> </span>AS<span class="w"> </span>build-js
<span class="k">RUN</span><span class="w"> </span>apk<span class="w"> </span>add<span class="w"> </span>--no-cache<span class="w"> </span>make
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">/build</span>
<span class="k">COPY</span><span class="w"> </span>console/frontend<span class="w"> </span>console/frontend
<span class="k">COPY</span><span class="w"> </span>Makefile<span class="w"> </span>.
<span class="k">RUN</span><span class="w"> </span>make<span class="w"> </span>console/data/frontend

<span class="k">FROM</span><span class="w"> </span><span class="s">--platform=$BUILDPLATFORM</span><span class="w"> </span>golang:alpine<span class="w"> </span>AS<span class="w"> </span>build-go
<span class="k">RUN</span><span class="w"> </span>apk<span class="w"> </span>add<span class="w"> </span>--no-cache<span class="w"> </span>make<span class="w"> </span>curl<span class="w"> </span>zip
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">/build</span>
<span class="k">COPY</span><span class="w"> </span>.<span class="w"> </span>.
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>build-js<span class="w"> </span>/build/console/data/frontend<span class="w"> </span>console/data/frontend
<span class="k">RUN</span><span class="w"> </span>go<span class="w"> </span>mod<span class="w"> </span>download
<span class="k">RUN</span><span class="w"> </span>make<span class="w"> </span>all-indep
<span class="hll"><span class="k">ARG</span><span class="w"> </span>TARGETOS<span class="w"> </span>TARGETARCH<span class="w"> </span>TARGETVARIANT<span class="w"> </span>VERSION
</span><span class="k">RUN</span><span class="w"> </span>make

<span class="k">FROM</span><span class="w"> </span><span class="s">gcr.io/distroless/static:latest</span>
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>build-go<span class="w"> </span>/build/bin/akvorado<span class="w"> </span>/usr/local/bin/akvorado
<span class="k">ENTRYPOINT</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"/usr/local/bin/akvorado"</span><span class="w"> </span><span class="p">]</span>
</code></pre></div>

<p>When building for multiple platforms with <code>--platform
linux/amd64,linux/arm64,linux/arm/v7</code>, the build steps until the highlighted
line execute only once for all platforms. This significantly speeds up the
build. 🚅</p>
<p>Akvorado now ships Docker images for these platforms: <code>linux/amd64</code>,
<code>linux/amd64/v3</code>, <code>linux/arm64</code>, and <code>linux/arm/v7</code>. When requesting
<code>ghcr.io/akvorado/akvorado</code>, Docker selects the best image for the current CPU.
On x86-64, there are two choices. If your <a href="https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels" title="X86-64: Microarchitecture levels">CPU is recent enough</a>, Docker
downloads <code>linux/amd64/v3</code>. This version contains additional optimizations and
should run faster than the <code>linux/amd64</code> version. It would be interesting to
ship an image for <code>linux/arm64/v8.2</code>, but Docker does not support the same
mechanism for AArch64 yet (<a href="https://github.com/akvorado/akvorado/commit/7928084fdef0" title="build: add some pointers to understand architecture variants">792808</a>).</p>
<h2 id="go">Go</h2>
<p>This release includes many changes related to Go but not visible to the users.</p>
<h3 id="toolchain">Toolchain</h3>
<p>In the past, Akvorado supported the two latest Go versions, preventing immediate
use of the latest enhancements. The goal was to allow users of stable
distributions to use Go versions shipped with their distribution to compile
Akvorado. However, this became frustrating when interesting features, like <code>go
tool</code>, were released. Akvorado 2.0 requires <strong>Go 1.25</strong> (<a href="https://github.com/akvorado/akvorado/commit/77306ddceee6" title="build: bump go.mod to Go 1.25 to use wg.Go()">77306d</a>) but can be
compiled with older toolchains by automatically downloading a newer one
(<a href="https://github.com/akvorado/akvorado/commit/94fb1c64837d" title="build: accept building with a not up-to-date toolchain">94fb1c</a>).<sup id="fnref:debian"><a class="footnote-ref" href="#fn:debian">5</a></sup> Users can still override <code>GOTOOLCHAIN</code> to revert this
decision. The recommended toolchain updates weekly through <abbr title="Continuous integration">CI</abbr> to ensure we get
the latest minor release (<a href="https://github.com/akvorado/akvorado/commit/5b11ecf29e4c" title="build: try updating Go toolchain weekly">5b11ec</a>). This change also simplifies updates to
newer versions: only <code>go.mod</code> needs updating.</p>
<p>Thanks to this change, Akvorado now uses <code>wg.Go()</code> (<a href="https://github.com/akvorado/akvorado/commit/77306ddceee6" title="build: bump go.mod to Go 1.25 to use wg.Go()">77306d</a>) and I have
started converting some unit tests to the new <code>test/synctest</code> package
(<a href="https://github.com/akvorado/akvorado/commit/bd787e654f93" title="common/daemon: experiment with the test/synctest package">bd787e</a>, <a href="https://github.com/akvorado/akvorado/commit/7016d899690b" title="demoexporter/flows: switch to testing/synctest package for clock">7016d8</a>, and <a href="https://github.com/akvorado/akvorado/commit/159085a0d3ac" title="outlet/metadata: switch to testing/synctest package for clock">159085</a>).</p>
<h3 id="testing">Testing</h3>
<p>When <strong>testing equality</strong>, I use a helper function <code>Diff()</code> to display the
differences when it fails:</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">got</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">Keys</span><span class="p">()</span>
<span class="nx">expected</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">}</span>
<span class="k">if</span><span class="w"> </span><span class="nx">diff</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">helpers</span><span class="p">.</span><span class="nx">Diff</span><span class="p">(</span><span class="nx">got</span><span class="p">,</span><span class="w"> </span><span class="nx">expected</span><span class="p">);</span><span class="w"> </span><span class="nx">diff</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s">""</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"Keys() (-got, +want):\n%s"</span><span class="p">,</span><span class="w"> </span><span class="nx">diff</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div>

<p>This function uses <a href="https://github.com/kylelemons/godebug"><code>kylelemons/godebug</code></a>. This package is
no longer maintained and has some shortcomings: for example, by default, it does
not compare struct private fields, which may cause unexpectedly successful
tests. I replaced it with <a href="https://github.com/google/go-cmp"><code>google/go-cmp</code></a>, which is stricter
and has better output (<a href="https://github.com/akvorado/akvorado/commit/e2f1df9add0d" title="tests: replace godebug by go-cmp for structure diffs">e2f1df</a>).</p>
<h3 id="another-package-for-kafka">Another package for Kafka</h3>
<p>Another change is the switch from <a href="https://github.com/IBM/sarama">Sarama</a> to <a href="https://github.com/twmb/franz-go">franz-go</a> to <strong>interact with
Kafka</strong> (<a href="https://github.com/akvorado/akvorado/commit/756e4a8fbd7a" title="/kafka: switch to franz-go">756e4a</a> and <a href="https://github.com/akvorado/akvorado/commit/2d26c5b9cab1" title="inlet/kafka: use kfake for functional tests with Kafka">2d26c5</a>). The main motivation for this change is to
get a better concurrency model. Sarama heavily relies on channels and it is
difficult to understand the lifecycle of an object handed to this package.
franz-go uses a more modern approach with callbacks<sup id="fnref:channels"><a class="footnote-ref" href="#fn:channels">6</a></sup> that is both more
performant and easier to understand. It also ships with a package to spawn fake
Kafka broker clusters, which is more convenient than the mocking functions
provided by Sarama.</p>
<h3 id="improved-routing-table-for-bmp">Improved routing table for <abbr title="BGP Monitoring Protocol">BMP</abbr></h3>
<p>To store its routing table, the <abbr title="BGP Monitoring Protocol">BMP</abbr> component used
<a href="https://github.com/kentik/patricia"><code>kentik/patricia</code></a>, an implementation of a patricia tree
focused on reducing garbage collection pressure.
<a href="https://github.com/gaissmai/bart"><code>gaissmai/bart</code></a> is a more recent alternative using an
adaptation of <a href="https://www.hariguchi.org/art/art.pdf" title="ART: Allotment Routing Table. A Fast Free Multibit Trie Based Routing Table">Donald Knuth’s ART algorithm</a> that promises <a href="https://github.com/gaissmai/iprbench" title="Benchmarks for Golang IP routing table">better
performance</a> and delivers it: <strong>90% faster lookups</strong> and <strong>27% faster
insertions</strong> (<a href="https://github.com/akvorado/akvorado/commit/92ee2e05b7c4" title="outlet/routing: use gaissmai/bart instead of kentik/patricia">92ee2e</a> and <a href="https://github.com/akvorado/akvorado/commit/fdb65c93a5fe" title="outlet/routing: store v4 routes into a v4 tree">fdb65c</a>).</p>
<p>Unlike <code>kentik/patricia</code>, <code>gaissmai/bart</code> does not help efficiently store values
attached to each prefix. I adapted the same approach as <code>kentik/patricia</code> to
store route lists for each prefix: store a 32-bit index for each prefix, and use
it to build a 64-bit index for looking up routes in a map. This leverages Go’s
efficient map structure.</p>
<p><code>gaissmai/bart</code> also supports a lockless routing table version, but this is not
simple because we would need to extend this to the map storing the routes and to
the interning mechanism. I also attempted to use Go’s new <code>unique</code> package to
replace the <code>intern</code> package included in Akvorado, but performance was
worse.<sup id="fnref:worse"><a class="footnote-ref" href="#fn:worse">7</a></sup></p>
<h3 id="miscellaneous">Miscellaneous</h3>
<p>Previous versions of Akvorado were using a <a href="/en/blog/2023-dynamic-protobuf-golang" title="Fast and dynamic encoding of Protocol Buffers in Go">custom Protobuf encoder</a> for
performance and flexibility. With the introduction of the outlet service,
Akvorado only needs a simple static schema, so this code was removed. However,
it is possible to enhance performance with
<a href="https://github.com/planetscale/vtprotobuf"><code>planetscale/vtprotobuf</code></a> (<a href="https://github.com/akvorado/akvorado/commit/e49a744a6d98" title="build: use vtprotobuf to speedup protobuf marshal/unmarshal">e49a74</a>, and <a href="https://github.com/akvorado/akvorado/commit/8b580fd26bfc" title="outlet/core: reuse the same RawFlow object when decoding">8b580f</a>).
Moreover, the dependency on <code>protoc</code>, a C++ program, was somewhat annoying.
Therefore, Akvorado now uses <a href="https://buf.build/">buf</a>, written in Go, to convert a Protobuf
schema into Go code (<a href="https://github.com/akvorado/akvorado/commit/f4c879e525a5" title="build: switch from protoc to buf to build protobuf">f4c879</a>).</p>
<p>Another small optimization to <strong>reduce the size of the Akvorado binary</strong> by
10 MB was to compress the static assets embedded in Akvorado in a ZIP file. It
includes the ASN database, as well as the SVG images for the documentation. A
small layer of code makes this change transparent (<a href="https://github.com/akvorado/akvorado/commit/b1d6382585dd" title="common/embed: replace all go:embed use by an embedded archive">b1d638</a> and <a href="https://github.com/akvorado/akvorado/commit/e69b919d1c78" title="build: remove embedded drawio diagrams from embedded SVG">e69b91</a>).</p>
<h2 id="javascript">JavaScript</h2>
<p>Recently, two large <strong>supply-chain attacks</strong> hit the JavaScript ecosystem: <a href="https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised" title="npm debug and chalk packages compromised">one
affecting the popular packages <code>chalk</code> and <code>debug</code></a> and <a href="https://sigh.dev/posts/ctrl-tinycolor-post-mortem/" title="@ctrl/tinycolor Supply Chain Attack Post-mortem">another
impacting the popular package <code>@ctrl/tinycolor</code></a>. These attacks also
exist in other ecosystems, but JavaScript is a prime target due to heavy use of
small third-party dependencies. The previous version of Akvorado relied on <strong>653
dependencies</strong>.</p>
<p><code>npm-run-all</code> was removed (<a href="https://github.com/akvorado/akvorado/commit/3424e8b8c9f2" title="console/frontend: don't use npm-run-all">3424e8</a>, 132 dependencies). <code>patch-package</code> was
removed (<a href="https://github.com/akvorado/akvorado/commit/6258050251a3" title="console/frontend: remove patch for sugar-date">625805</a> and <a href="https://github.com/akvorado/akvorado/commit/e85ff0ae31fa" title="console/frontend: remove patch for echarts">e85ff0</a>, 69 dependencies) by moving missing
TypeScript definitions to <code>env.d.ts</code>. <code>eslint</code> was replaced with <a href="https://oxc.rs/docs/guide/usage/linter">oxlint</a>, a
linter written in Rust (<a href="https://github.com/akvorado/akvorado/commit/97fd8ca2e479" title="console/frontend: replace eslint by oxlint">97fd8c</a>, 125 dependencies, including the plugins).</p>
<p>I switched from <code>npm</code> to <a href="https://pnpm.io/" title="Fast, disk space efficient package manager">Pnpm</a>, an alternative package manager (<a href="https://github.com/akvorado/akvorado/commit/fce383dbf46b" title="build: switch to pnpm">fce383</a>).
Pnpm does not run install scripts by default<sup id="fnref:install"><a class="footnote-ref" href="#fn:install">8</a></sup> and prevents installing
packages that are too recent. It is also significantly faster.<sup id="fnref:fast"><a class="footnote-ref" href="#fn:fast">9</a></sup> Node.js
does not ship Pnpm but it ships <a href="https://github.com/nodejs/corepack" title="Zero-runtime-dependency package acting as bridge between Node projects and their package managers">Corepack</a>, which allows us to use Pnpm
without installing it. Pnpm can also list licenses used by each dependency,
removing the need for <code>license-compliance</code> (<a href="https://github.com/akvorado/akvorado/commit/a35ca8bc07e0" title="build: replace license-compliance with pnpm">a35ca8</a>, 42 dependencies).</p>
<p>For additional speed improvements, beyond switching to Pnpm and Oxlint, <a href="https://vite.dev/">Vite</a>
was replaced with its faster <a href="https://voidzero.dev/posts/announcing-rolldown-vite" title="Announcing Rolldown-Vite">Rolldown version</a> (<a href="https://github.com/akvorado/akvorado/commit/4638272b7b6d" title="console/frontend: switch to rolldown-vite">463827</a>).</p>
<p>After these changes, Akvorado “only” pulls 225 dependencies. 😱</p>
<h1 id="next-steps">Next steps</h1>
<p>I would like to land three features in the next version of Akvorado:</p>
<ul>
<li>
<p>Add Grafana dashboards to complete the observability stack. See
  <a href="https://github.com/akvorado/akvorado/issues/1906" title="Ship some Grafana dashboards in docker compose setup">issue #1906</a> for details.</p>
</li>
<li>
<p>Integrate <a href="https://github.com/ovh/grafana-akvorado">OVH’s Grafana plugin</a> by providing a stable API for such
  integrations. Akvorado’s web console would still be useful for browsing
  results, but if you want to build and share dashboards, you should switch to
  Grafana. See <a href="https://github.com/akvorado/akvorado/issues/1895" title="Integration of OVH Grafana plugin">issue #1895</a>.</p>
</li>
<li>
<p>Move some work currently done in ClickHouse (custom dictionaries, GeoIP and IP
  enrichment) back into the outlet service. This should give more flexibility
  for adding features like the one requested in <a href="https://github.com/akvorado/akvorado/issues/1030" title="RegExp for custom dimension">issue #1030</a>. See <a href="https://github.com/akvorado/akvorado/issues/2006" title="Move back GeoIP and custom dictionaries into outlet">issue #2006</a>.</p>
</li>
</ul>
<hr/>
<p>I started working on splitting the inlet into two parts more than one year ago.
I found more motivation in recent months, partly thanks to <a href="https://code.claude.com/docs/en/overview" title="Claude Code overview">Claude Code</a>,
which I used as a <a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging" title="https://en.wikipedia.org/wiki/Rubber_duck_debugging">rubber duck</a>. Almost none of the produced code was
kept:<sup id="fnref:exceptions"><a class="footnote-ref" href="#fn:exceptions">10</a></sup> it is like an intern who never learns. 🦆</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:bmp">
<p>Many attempts were made to make the <abbr title="BGP Monitoring Protocol">BMP</abbr> component both performant and
not blocking. See for example <a href="https://github.com/akvorado/akvorado/pull/254" title="inlet/bmp: avoid long lock times when flushing peers">PR #254</a>, <a href="https://github.com/akvorado/akvorado/pull/255" title="“Lockless” RIB design">PR #255</a>, and <a href="https://github.com/akvorado/akvorado/pull/278" title="inlet/bmp: new lockless design">PR #278</a>.
Despite these efforts, this component remained problematic for most users.
See <a href="https://github.com/akvorado/akvorado/issues/1461" title="BMP crash: cannot read BMP body">issue #1461</a> as an example. <a class="footnote-backref" href="#fnref:bmp" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:features">
<p>Some features have been pushed to ClickHouse to avoid the
processing cost in the inlet. See for example <a href="https://github.com/akvorado/akvorado/pull/1059" title="Do geoip enrich in clickhouse instead of inlet">PR #1059</a>. <a class="footnote-backref" href="#fnref:features" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:size">
<p>This is the <a href="https://github.com/akvorado/akvorado/commit/ac68c5970e2c" title="inlet: split inlet into new inlet and outlet">biggest commit</a>:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>show<span class="w"> </span>--shortstat<span class="w"> </span>ac68c5970e2c<span class="w"> </span><span class="p">|</span><span class="w"> </span>tail<span class="w"> </span>-1
<span class="go">231 files changed, 6474 insertions(+), 3877 deletions(-)</span>
</code></pre></div>

<p><a class="footnote-backref" href="#fnref:size" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:broadcom">
<p>Broadcom is known for its user-hostile moves. Look at what happened
with VMWare. <a class="footnote-backref" href="#fnref:broadcom" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:debian">
<p>As a Debian developer, I dislike these mechanisms that circumvent
the distribution package manager. The final straw came when Go 1.25 spent one month in the <a href="https://ftp-master.debian.org/new.html">Debian NEW queue</a>, an arbitrary mechanism I
don’t like at all. <a class="footnote-backref" href="#fnref:debian" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:channels">
<p>In the early years of Go, channels were heavily promoted. Sarama
was designed during this period. A few years later, a more nuanced approach
emerged. See notably “<a href="https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad/" title="Go channels are bad and you should feel bad">Go channels are bad and you should feel bad</a>.” <a class="footnote-backref" href="#fnref:channels" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:worse">
<p>This should be investigated further, but my theory is that the
<code>intern</code> package uses 32-bit integers, while <code>unique</code> uses 64-bit pointers.
See commit <a href="https://github.com/akvorado/akvorado/commit/74e5aca41b71" title="outlet/routing: replace intern pool by unique.Hash">74e5ac</a>. <a class="footnote-backref" href="#fnref:worse" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:install">
<p>This is also possible with <code>npm</code>. See commit <a href="https://github.com/akvorado/akvorado/commit/dab2f7875171" title="build: do not run install scripts with npm">dab2f7</a>. <a class="footnote-backref" href="#fnref:install" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:fast">
<p>An even faster alternative is <a href="https://bun.sh/" title="Fast all-in-one JavaScript runtime">Bun</a>, but it is less available. <a class="footnote-backref" href="#fnref:fast" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:exceptions">
<p>The exceptions are part of the code for the admonition blocks,
the code for collapsing the table of contents, and part of the documentation. <a class="footnote-backref" href="#fnref:exceptions" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Building a SQL-like language to filter flows</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2023-sql-like-language-filter" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2023-sql-like-language-filter#isso-thread" rel="replies" type="text/html"/>
    <updated>2023-02-13T07:06:06Z</updated>
    <id>http://www.luffy.cx/en/blog/2023-sql-like-language-filter.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="/en/blog/2022-akvorado-flow-collector" title="Akvorado: a flow collector, enricher, and visualizer">Akvorado</a> collects network flows using <a href="https://www.rfc-editor.org/rfc/rfc7011" title="RFC 7011: Specification of the IP Flow Information Export (IPFIX) Protocol for the Exchange of Flow Information">IPFIX</a> or <a href="https://www.rfc-editor.org/rfc/rfc3176" title="RFC 3176: InMon Corporation's sFlow: A Method for Monitoring Traffic in Switched and Routed Networks">sFlow</a>. It stores them
in a <a href="https://clickhouse.com/" title="ClickHouse: OLAP DBMS">ClickHouse</a> database. A web console allows a user to query the data and
plot some graphs. A nice aspect of this console is how we can filter flows with
a SQL-like language:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 700px"><span class="lf-media-inner" style="padding-bottom: 60.000%"><video src="https://media-bernat-ch.b-cdn.net/images/akvorado/filter.b8c2bbe031362a.mp4" width="700" height="420" muted="" loop="" autoplay="" playsinline="" controls="" class="lf-media lf-opaque"/></span></div><figcaption>Filter editor in Akvorado console</figcaption></figure>
<p>Often, web interfaces expose a <a href="https://dabernathy89.github.io/vue-query-builder/" title="Vue Query Builder">query</a>
<a href="https://react-querybuilder.js.org/" title="React Query Builder">builder</a> to build such filters. I think combining a
SQL-like language with an editor supporting <strong>completion</strong>, <strong>syntax
highlighting</strong>, and <strong>linting</strong> is a better approach.<sup id="fnref:lazy"><a class="footnote-ref" href="#fn:lazy">1</a></sup></p>
<div class="toc">
<ul>
<li><a href="#language-parser">Language parser</a></li>
<li><a href="#integration-in-codemirror">Integration in CodeMirror</a><ul>
<li><a href="#syntax-highlighting">Syntax highlighting</a></li>
<li><a href="#linting">Linting</a></li>
<li><a href="#completion">Completion</a></li>
</ul>
</li>
</ul>
</div>
<p>The language parser is built with <a href="https://github.com/mna/pigeon" title="Command pigeon generates parsers in Go from a PEG grammar">pigeon</a> (<em>Go</em>) from a <a href="https://en.wikipedia.org/wiki/Parsing_expression_grammar" title="Parsing expression grammar on Wikipedia">parsing expression
grammar</a>—or <abbr title="Parsing Expression Grammar">PEG</abbr>. The editor component is <a href="https://codemirror.net/" title="CodeMirror: Extensible Code Editor">CodeMirror</a> (<em>TypeScript</em>).</p>
<h1 id="language-parser">Language parser</h1>
<p><abbr title="Parsing Expression Grammar">PEG</abbr> grammars are relatively recent<sup id="fnref:recent"><a class="footnote-ref" href="#fn:recent">2</a></sup> and are an alternative to
context-free grammars. They are easier to write and they can generate better
error messages. Python <a href="https://peps.python.org/pep-0617/" title="PEP 617 – New PEG parser for CPython">switched from an <abbr title="left-to-right, leftmost derivation">LL</abbr>(1)-based parser to a <abbr title="Parsing Expression Grammar">PEG</abbr>-based
parser</a> in Python 3.9.</p>
<p><a href="https://github.com/mna/pigeon" title="Command pigeon generates parsers in Go from a PEG grammar">pigeon</a> generates a parser for Go. A grammar is a set of rules. Each rule is
an identifier, with an optional user-friendly label for error messages, an
expression, and an action in Go to be executed on match. You can find the
complete grammar in <a href="https://github.com/akvorado/akvorado/blob/main/console/filter/parser.peg"><code>parser.peg</code></a>. Here is a simplified rule:</p>
<div class="language-pigeon codehilite"><pre><span/><code>ConditionIPExpr <span class="s2">"condition on IP"</span> <span class="o">←</span>
  <span class="nv">column</span>:<span class="p">(</span><span class="s2">"ExporterAddress"</span><span class="o">i</span> <span class="p">{</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="s">"ExporterAddress"</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">}</span>
        / <span class="s2">"SrcAddr"</span><span class="o">i</span> <span class="p">{</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="s">"SrcAddr"</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">}</span>
        / <span class="s2">"DstAddr"</span><span class="o">i</span> <span class="p">{</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="s">"DstAddr"</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">})</span> _ 
  <span class="nv">operator</span>:<span class="p">(</span><span class="s2">"="</span> / <span class="s2">"!="</span><span class="p">)</span> _ 
  <span class="nv">ip</span>:IP <span class="p">{</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Sprintf</span><span class="p">(</span><span class="s">"%s %s IPv6StringToNum(%s)"</span><span class="p">,</span>
<span class="w">      </span><span class="nx">toString</span><span class="p">(</span><span class="nx">column</span><span class="p">),</span><span class="w"> </span><span class="nx">toString</span><span class="p">(</span><span class="nx">operator</span><span class="p">),</span><span class="w"> </span><span class="nx">quote</span><span class="p">(</span><span class="nx">ip</span><span class="p">)),</span><span class="w"> </span><span class="kc">nil</span>
<span class="w">  </span><span class="p">}</span>
</code></pre></div>

<p>The rule identifier is <code>ConditionIPExpr</code>. It case-insensitively matches
<code>ExporterAddress</code>, <code>SrcAddr</code>, or <code>DstAddr</code>. For each of them, it returns the
column name with the proper case. That’s what is stored in the <code>column</code>
variable. Then, it matches one of the possible operators. As there is no code
block, it stores the matched string directly in the <code>operator</code> variable. Then,
it tries to match the <code>IP</code> rule, which is defined elsewhere in the grammar. If
it succeeds, it stores the result of the match in the <code>ip</code> variable and executes
the final action. The action turns the column, operator, and IP into a proper
expression for <em>ClickHouse</em>. For example, if we have <code>ExporterAddress =
203.0.113.15</code>, we get <code>ExporterAddress = IPv6StringToNum('203.0.113.15')</code>.</p>
<p>The <code>IP</code> rule uses a rudimentary regular expression but checks if the matched
address is correct in the action block, thanks to <code>netip.ParseAddr()</code>:</p>
<div class="language-pigeon codehilite"><pre><span/><code>IP <span class="s2">"IP address"</span> <span class="o">←</span> <span class="p">[</span><span class="s">0-9A-Fa-f:.</span><span class="p">]</span>+ <span class="p">{</span>
<span class="w">  </span><span class="nx">ip</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">ParseAddr</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">text</span><span class="p">))</span>
<span class="w">  </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="s">""</span><span class="p">,</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nx">New</span><span class="p">(</span><span class="s">"expecting an IP address"</span><span class="p">)</span>
<span class="w">  </span><span class="p">}</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="nx">ip</span><span class="p">.</span><span class="nx">String</span><span class="p">(),</span><span class="w"> </span><span class="kc">nil</span>
<span class="p">}</span>
</code></pre></div>

<p>Our parser safely turns the filter into a <code>WHERE</code> clause accepted by
<em>ClickHouse</em>:<sup id="fnref:ast"><a class="footnote-ref" href="#fn:ast">3</a></sup></p>
<div class="language-sql codehilite"><pre><span/><code><span class="k">WHERE</span><span class="w"> </span><span class="n">InIfBoundary</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'external'</span><span class="w"> </span>
<span class="k">AND</span><span class="w"> </span><span class="n">ExporterRegion</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'france'</span><span class="w"> </span>
<span class="k">AND</span><span class="w"> </span><span class="n">InIfConnectivity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'transit'</span><span class="w"> </span>
<span class="k">AND</span><span class="w"> </span><span class="n">SrcAS</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">15169</span><span class="w"> </span>
<span class="k">AND</span><span class="w"> </span><span class="n">DstAddr</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="n">toIPv6</span><span class="p">(</span><span class="s1">'2a01:e0f:ffff::'</span><span class="p">)</span><span class="w"> </span>
<span class="w">                </span><span class="k">AND</span><span class="w"> </span><span class="n">toIPv6</span><span class="p">(</span><span class="s1">'2a01:e0f:ffff:ffff:ffff:ffff:ffff:ffff'</span><span class="p">)</span>
</code></pre></div>

<h1 id="integration-in-codemirror">Integration in CodeMirror</h1>
<p><a href="https://codemirror.net/" title="CodeMirror: Extensible Code Editor">CodeMirror</a> is a versatile code editor that can be easily integrated into
JavaScript projects. In <em>Akvorado</em>, the <a href="https://vuejs.org/" title="Vue.js: the Progressive JavaScript Framework">Vue.js</a> component,
<a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/components/InputFilter.vue"><code>InputFilter</code></a>, uses <em>CodeMirror</em> as its foundation and
leverages features such as syntax highlighting, linting, and completion. The
source code for these capabilities can be found in the
<a href="https://github.com/akvorado/akvorado/tree/main/console/frontend/src/codemirror/lang-filter"><code>codemirror/lang-filter/</code></a> directory.</p>
<h2 id="syntax-highlighting">Syntax highlighting</h2>
<p>The <abbr title="Parsing Expression Grammar">PEG</abbr> grammar for Go cannot be utilized directly<sup id="fnref:pegjs"><a class="footnote-ref" href="#fn:pegjs">4</a></sup> and the requirements
for parsers for editors are distinct: they should be error-tolerant and operate
incrementally, as code is typically updated character by character. <em>CodeMirror</em>
offers a solution through its own parser generator, <a href="https://lezer.codemirror.net/" title="The Lezer Parser System">Lezer</a>.</p>
<p>We don’t need this additional parser to fully understand the filter language.
Only the basic structure is needed: column names, comparison and logic
operators, quoted and unquoted values. The <a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/codemirror/lang-filter/syntax.grammar">grammar</a> is therefore quite short
and does not need to be updated often:</p>
<div class="language-lezer codehilite"><pre><span/><code><span class="kt">@top</span> <span class="ss">Filter</span> <span class="p">{</span>
  expression
<span class="p">}</span>

<span class="ss">expression</span> <span class="p">{</span>
 Not expression |
 <span class="s2">"("</span> expression <span class="s2">")"</span> |
 <span class="s2">"("</span> expression <span class="s2">")"</span> And expression |
 <span class="s2">"("</span> expression <span class="s2">")"</span> Or expression |
 comparisonExpression And expression |
 comparisonExpression Or expression |
 comparisonExpression
<span class="p">}</span>
<span class="ss">comparisonExpression</span> <span class="p">{</span>
 Column Operator Value
<span class="p">}</span>

<span class="ss">Value</span> <span class="p">{</span>
  String | Literal | ValueLParen ListOfValues ValueRParen
<span class="p">}</span>
<span class="ss">ListOfValues</span> <span class="p">{</span>
  ListOfValues ValueComma <span class="p">(</span>String | Literal<span class="p">)</span> |
  String | Literal
<span class="p">}</span>

<span class="c1">// […]</span>
<span class="kt">@tokens</span> <span class="p">{</span>
  <span class="c1">// […]</span>
  <span class="ss">Column</span> <span class="p">{</span> std.asciiLetter <span class="p">(</span>std.asciiLetter|std.digit<span class="p">)</span>* <span class="p">}</span>
  <span class="ss">Operator</span> <span class="p">{</span> $<span class="p">[</span><span class="s">a-zA-Z!=&gt;&lt;</span><span class="p">]</span>+ <span class="p">}</span>

  <span class="ss">String</span> <span class="p">{</span>
    <span class="s1">'"'</span> <span class="p">(</span>!<span class="p">[</span><span class="s">\\\n"</span><span class="p">]</span> | <span class="s2">"\\"</span> _<span class="p">)</span>* <span class="s1">'"'</span>? |
    <span class="s2">"'"</span> <span class="p">(</span>!<span class="p">[</span><span class="s">\\\n'</span><span class="p">]</span> | <span class="s2">"\\"</span> _<span class="p">)</span>* <span class="s2">"'"</span>?
  <span class="p">}</span>
  <span class="ss">Literal</span> <span class="p">{</span> <span class="p">(</span>std.digit | std.asciiLetter | $<span class="p">[</span><span class="s">.:/</span><span class="p">])</span>+ <span class="p">}</span>
  <span class="c1">// […]</span>
<span class="p">}</span>
</code></pre></div>

<p>The expression <code>SrcAS = 12322 AND (DstAS = 1299 OR SrcAS = 29447)</code> is parsed to:</p>
<div class="language-text-only codehilite"><pre><span/><code>Filter(Column, Operator, Value(Literal),
  And, Column, Operator, Value(Literal),
  Or, Column, Operator, Value(Literal))
</code></pre></div>

<p>The last step is to teach <em>CodeMirror</em> how to map each token to a highlighting
tag:</p>
<div class="language-typescript codehilite"><pre><span/><code><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">FilterLanguage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">LRLanguage</span><span class="p">.</span><span class="nx">define</span><span class="p">({</span>
<span class="w">  </span><span class="nx">parser</span><span class="o">:</span><span class="w"> </span><span class="kt">parser.configure</span><span class="p">({</span>
<span class="w">    </span><span class="nx">props</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w">      </span><span class="nx">styleTags</span><span class="p">({</span>
<span class="w">        </span><span class="nx">Column</span><span class="o">:</span><span class="w"> </span><span class="kt">t.propertyName</span><span class="p">,</span>
<span class="w">        </span><span class="nx">String</span><span class="o">:</span><span class="w"> </span><span class="kt">t.string</span><span class="p">,</span>
<span class="w">        </span><span class="nx">Literal</span><span class="o">:</span><span class="w"> </span><span class="kt">t.literal</span><span class="p">,</span>
<span class="w">        </span><span class="nx">LineComment</span><span class="o">:</span><span class="w"> </span><span class="kt">t.lineComment</span><span class="p">,</span>
<span class="w">        </span><span class="nx">BlockComment</span><span class="o">:</span><span class="w"> </span><span class="kt">t.blockComment</span><span class="p">,</span>
<span class="w">        </span><span class="nx">Or</span><span class="o">:</span><span class="w"> </span><span class="kt">t.logicOperator</span><span class="p">,</span>
<span class="w">        </span><span class="nx">And</span><span class="o">:</span><span class="w"> </span><span class="kt">t.logicOperator</span><span class="p">,</span>
<span class="w">        </span><span class="nx">Not</span><span class="o">:</span><span class="w"> </span><span class="kt">t.logicOperator</span><span class="p">,</span>
<span class="w">        </span><span class="nx">Operator</span><span class="o">:</span><span class="w"> </span><span class="kt">t.compareOperator</span><span class="p">,</span>
<span class="w">        </span><span class="s2">"( )"</span><span class="o">:</span><span class="w"> </span><span class="nx">t</span><span class="p">.</span><span class="nx">paren</span><span class="p">,</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">    </span><span class="p">],</span>
<span class="w">  </span><span class="p">}),</span>
<span class="p">});</span>
</code></pre></div>

<h2 id="linting">Linting</h2>
<p>We offload linting to the original parser in Go. The
<code>/api/v0/console/filter/validate</code> endpoint accepts a filter and returns a JSON
structure with the errors that were found:</p>
<div class="language-json codehilite"><pre><span/><code><span class="p">{</span>
<span class="w">  </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"at line 1, position 12: string literal not terminated"</span><span class="p">,</span>
<span class="w">  </span><span class="nt">"errors"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span>
<span class="w">    </span><span class="nt">"line"</span><span class="p">:</span><span class="w">    </span><span class="mi">1</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"column"</span><span class="p">:</span><span class="w">  </span><span class="mi">12</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"offset"</span><span class="p">:</span><span class="w">  </span><span class="mi">11</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string literal not terminated"</span><span class="p">,</span>
<span class="w">  </span><span class="p">}]</span>
<span class="p">}</span>
</code></pre></div>

<p>The <a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/codemirror/lang-filter/linter.ts">linter source</a> for <em>CodeMirror</em> queries the API and turns each error into
a <a href="https://codemirror.net/docs/ref/#lint.Diagnostic">diagnostic</a>.</p>
<h2 id="completion">Completion</h2>
<p>The completion system takes a hybrid approach. It splits the work between the
frontend and the backend to offer useful suggestions for completing filters.</p>
<p>The <a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/codemirror/lang-filter/complete.ts">frontend</a> uses the parser built with <em>Lezer</em> to determine the context of
the completion: do we complete a column name, an operator, or a value? It also
extracts the column name if we are completing something else. It forwards the
result to the backend through the <code>/api/v0/console/filter/complete</code> endpoint.
Walking the syntax tree was not as easy as I thought, but <a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/codemirror/lang-filter/complete.test.ts">unit tests</a> helped
a lot.</p>
<p>The <a href="https://github.com/akvorado/akvorado/blob/9eee46caded6fa6cc2dfb90d1941b2ef05d15e9f/console/filter.go#L76">backend</a> uses the parser generated by <em>pigeon</em> to complete a column name
or a comparison operator. For values, the completions are either static or
extracted from the <em>ClickHouse</em> database. A user can complete an <abbr title="Autonomous System">AS</abbr> number from
an organization name thanks to the following snippet:</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">results</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">Label</span><span class="w">  </span><span class="kt">string</span><span class="w"> </span><span class="s">`ch:"label"`</span>
<span class="w">  </span><span class="nx">Detail</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="s">`ch:"detail"`</span>
<span class="p">}{}</span>
<span class="nx">columnName</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">"DstAS"</span>
<span class="nx">sqlQuery</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Sprintf</span><span class="p">(</span><span class="s">`</span>
<span class="s"> SELECT concat('AS', toString(%s)) AS label, dictGet('asns', 'name', %s) AS detail</span>
<span class="s"> FROM flows</span>
<span class="s"> WHERE TimeReceived &gt; date_sub(minute, 1, now())</span>
<span class="s"> AND detail != ''</span>
<span class="s"> AND positionCaseInsensitive(detail, $1) &gt;= 1</span>
<span class="s"> GROUP BY label, detail</span>
<span class="s"> ORDER BY COUNT(*) DESC</span>
<span class="s"> LIMIT 20</span>
<span class="s">`</span><span class="p">,</span><span class="w"> </span><span class="nx">columnName</span><span class="p">,</span><span class="w"> </span><span class="nx">columnName</span><span class="p">)</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Select</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">results</span><span class="p">,</span><span class="w"> </span><span class="nx">sqlQuery</span><span class="p">,</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">Prefix</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">c</span><span class="p">.</span><span class="nx">r</span><span class="p">.</span><span class="nx">Err</span><span class="p">(</span><span class="nx">err</span><span class="p">).</span><span class="nx">Msg</span><span class="p">(</span><span class="s">"unable to query database"</span><span class="p">)</span>
<span class="w">  </span><span class="k">break</span>
<span class="p">}</span>
<span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">result</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">results</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">completions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">completions</span><span class="p">,</span><span class="w"> </span><span class="nx">filterCompletion</span><span class="p">{</span>
<span class="w">    </span><span class="nx">Label</span><span class="p">:</span><span class="w">  </span><span class="nx">result</span><span class="p">.</span><span class="nx">Label</span><span class="p">,</span>
<span class="w">    </span><span class="nx">Detail</span><span class="p">:</span><span class="w"> </span><span class="nx">result</span><span class="p">.</span><span class="nx">Detail</span><span class="p">,</span>
<span class="w">    </span><span class="nx">Quoted</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="w">  </span><span class="p">})</span>
<span class="p">}</span>
</code></pre></div>

<p>In my opinion, the completion system is a major factor in making the field
editor an efficient way to select flows. While a query builder may have been
more beginner-friendly, the completion system’s ease of use and functionality
make it more enjoyable to use once you become familiar.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:lazy">
<p>Moreover, building a query builder did not seem like a fun task for me. <a class="footnote-backref" href="#fnref:lazy" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:recent">
<p>They were introduced in 2004 in “<a href="https://bford.info/pub/lang/peg.pdf">Parsing Expression Grammars: A
Recognition-Based Syntactic Foundation</a>.” <abbr title="left-to-right">LR</abbr> parsers were
introduced in <a href="https://web.archive.org/web/20120315152151/http://www.cs.dartmouth.edu/~mckeeman/cs48/mxcom/doc/knuth65.pdf" title="On the Translation of Languages from Left to Right">1965</a>, <abbr title="lookahead left-to-right">LALR</abbr> parsers in 1969, and <abbr title="left-to-right, leftmost derivation">LL</abbr> parsers in the
1970s. <a href="https://en.wikipedia.org/wiki/Yacc" title="Yacc article on Wikipedia">Yacc</a>, a popular parser generator, was written in 1975. <a class="footnote-backref" href="#fnref:recent" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:ast">
<p>The parser returns a string. It does not generate an intermediate <abbr title="Abstract Syntax Tree">AST</abbr>.
This makes it simpler and it currently fits our needs. <a class="footnote-backref" href="#fnref:ast" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:pegjs">
<p>It could be manually translated to JavaScript with <a href="https://peggyjs.org/" title="Peggy: Parser Generator for JavaScript">Peggy</a>. <a class="footnote-backref" href="#fnref:pegjs" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
</feed>