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

  <entry>
    <title type="html">Bidouiller le compilateur Go pour convertir efficacement les IPv4 en IPv6</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-go-netip-addrto6" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-go-netip-addrto6#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-10-04T13:27:25Z</updated>
    <id>http://www.luffy.cx/fr/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 sur Go Packages"><code>netip.Addr</code></a> expose une méthode
<a href="https://pkg.go.dev/net/netip#Addr.Unmap" title="net/netip.Addr.Unmap sur Go Packages"><code>Unmap()</code></a> qui renvoie l'adresse IPv4 contenue dans une
<a href="https://www.rfc-editor.org/info/rfc4291#section-2.5.5.2" title="RFC 4291: IP Version 6 Addressing Architecture">adresse IPv6 représentant une IPv4</a> (<em>IPv4-mapped IPv6 address</em>). À
partir de <code>::ffff:203.0.113.10</code> ou de <code>::ffff:cb00:710a</code>, elle renvoie
<code>203.0.113.10</code><sup id="fnref:why"><a class="footnote-ref" href="#fn:why">1</a></sup>. Il n'existe pas de méthode <code>Map()</code> ou <code>To6()</code> pour
l'opération inverse. Une telle méthode est triviale à écrire, mais les
mainteneurs de Go l'ont <a href="https://github.com/golang/go/issues/54365#issuecomment-1570607144" title="Issue #54365: proposal: net/netip: add netip.Addr.Map">refusée</a> au motif que les utilisateurs
devraient écrire <code>netip.AddrFrom16(ip.As16())</code> et laisser le compilateur
l'optimiser<sup id="fnref:equiv"><a class="footnote-ref" href="#fn:equiv">2</a></sup>. Aujourd'hui, cette construction est huit fois plus lente
qu'une méthode native. Comment apprendre au compilateur à l'optimiser ?</p>
<div class="toc">
<ul>
<li><a href="#les-solutions-possibles">Les solutions possibles</a><ul>
<li><a href="#modifier-la-bibliotheque-standard-de-go">Modifier la bibliothèque standard de Go</a></li>
<li><a href="#avec-une-fonction-utilitaire">Avec une fonction utilitaire</a></li>
<li><a href="#avec-le-paquet-unsafe">Avec le paquet unsafe</a></li>
<li><a href="#performances">Performances</a></li>
<li><a href="#code-assembleur">Code assembleur</a></li>
</ul>
</li>
<li><a href="#bidouiller-le-compilateur">Bidouiller le compilateur</a><ul>
<li><a href="#le-marteau">Le marteau</a></li>
<li><a href="#le-tournevis">Le tournevis</a><ul>
<li><a href="#sur-le-papier">Sur le papier</a></li>
<li><a href="#en-pratique">En pratique</a></li>
<li><a href="#dans-le-desordre">Dans le désordre</a></li>
<li><a href="#remise-en-ordre">Remise en ordre</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#prochaine-etape">Prochaine étape</a></li>
</ul>
</div>
<h1 id="les-solutions-possibles">Les solutions possibles</h1>
<p>Examinons trois façons d'implémenter cette conversion pour <code>netip.Addr</code>. Ma
préférée consiste à l'ajouter à la bibliothèque standard de Go. Les mainteneurs
de Go préfèrent une petite fonction utilitaire externe enchaînant
<code>netip.AddrFrom16()</code> et <code>netip.Addr.As16()</code>, en espérant que le compilateur
réussisse à l'optimiser. Le <a href="https://pkg.go.dev/unsafe" title="unsafe sur Go Packages">paquet <code>unsafe</code></a> ouvre une troisième
voie, avec des performances identiques à la première solution.</p>
<h2 id="modifier-la-bibliotheque-standard-de-go">Modifier la bibliothèque standard de Go</h2>
<p>En interne, <a href="https://pkg.go.dev/net/netip#Addr" title="net/netip.Addr sur Go Packages"><code>netip.Addr</code></a> stocke les adresses IP sous
forme d'une valeur de 128 bits accompagnée d'un champ supplémentaire <code>z</code> qui
encode la famille et la 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">// false pour IPv4, true pour IPv6.</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">// != "" seulement si IsV6 est vrai.</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 sur Go Packages"><code>AddrFrom4()</code></a> encode une adresse IPv4 sous la forme
d'une adresse IPv6 représentant une IPv4 et assigne le singleton <code>z4</code> au champ <code>z</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 sur Go Packages"><code>Unmap()</code></a> transforme une adresse IPv6 représentant
une IPv4 en adresse IPv4 en assignant le singleton <code>z4</code> au champ :</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>Ajouter l'opération inverse dans la bibliothèque standard de Go est trivial : il
suffit d'assigner au champ <code>z</code> le singleton <code>z6noz</code> quand l'adresse est une
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">Mise à jour (10.2026)</p>
<p>Plutôt que de modifier Go, nous pouvons modifier n'importe quel paquet, y
compris ceux de la bibliothèque standard, avec l'option
<a href="https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies" title="Documentation de go build"><code>-overlay</code></a> de <code>go build</code>. Dans notre cas, elle permet d'<a href="https://github.com/vincentbernat/go-netip-addrto6/commit/dcc2e1af2af48301d8e992db22d97682bda37b8c" title="Add an alternative using an overlay">ajouter
<code>To6()</code> à <code>net/netip</code></a>. Son usage reste toutefois
contraignant<sup id="fnref:inconvenient"><a class="footnote-ref" href="#fn:inconvenient">3</a></sup> : l'option attend un fichier JSON associant des
chemins absolus dans <code>GOROOT</code> à des fichiers de remplacement et il faut la
spécifier à chaque commande <code>go</code>.</p>
</div>
<h2 id="avec-une-fonction-utilitaire">Avec une fonction utilitaire</h2>
<p>Le champ <code>z</code> n'est pas accessible en dehors du paquet <code>net/netip</code>. À la place,
nous écrivons une fonction utilitaire autour de la construction
<code>netip.AddrFrom16(ip.As16())</code> :</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="avec-le-paquet-unsafe">Avec le paquet <code>unsafe</code></h2>
<p>Une autre solution utilise le paquet <code>unsafe</code> pour modifier la structure <code>Addr</code>
par l'intermédiaire d'une structure avec le même schéma mémoire<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 a le même schéma mémoire que 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="performances">Performances</h2>
<p>Sur mon ordinateur, avec Go 1.27.1, la solution intégrée à la bibliothèque
standard coûte 0,88 ns par opération, alors que celle privilégiée par les
mainteneurs de Go coûte 7,14 ns. La solution utilisant <code>unsafe</code> égale les
performances de la première.</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="code-assembleur">Code assembleur</h2>
<p>Regardons le code assembleur que le compilateur génère pour chaque
solution<sup id="fnref:dis"><a class="footnote-ref" href="#fn:dis">5</a></sup>. La solution intégrée à la bibliothèque standard ressemble à
ceci<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">; vérifier avec "z" s'il s'agit d'une adresse IPv4</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">; sinon, s'arrêter ici</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">// valeur de retour = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>Le <a href="https://go.dev/doc/asm" title="A Quick Guide to Go's Assembler">langage assembleur de Go</a> n'est pas une représentation directe du
langage machine sous-jacent : il repose sur un jeu d'instructions semi-abstrait
dérivé de <a href="https://9p.io/sys/doc/asm.html" title=" A Manual for the Plan 9 assembler">l'assembleur de Plan 9</a>. Il dispose de quatre
pseudo-registres : FP (<em>frame pointer</em> ou pointeur de cadre pour les arguments
des fonctions), PC (<em>program counter</em> ou compteur ordinal), SB (<em>static base</em> ou
pointeur de base pour les symboles globaux) et SP (<em>stack pointer</em> ou pointeur
de la pile). Il possède aussi des registres propres à l'architecture, comme
<code>AX</code>, <code>CX</code>, <code>DX</code>, <code>BX</code>, <code>SI</code>, <code>DI</code> et <code>R8</code> à <code>R15</code>. Les instructions qui
stockent une donnée utilisent leur dernier argument comme destination. Les
instructions peuvent porter un suffixe de taille explicite : <code>MOVB</code> déplace un
octet, <code>MOVW</code> 16 bits, <code>MOVL</code> 32 bits et <code>MOVQ</code> 64 bits. Dans l'exemple
ci-dessus, la première instruction compare la valeur de 64 bits <code>z4</code> avec le
registre <code>CX</code>.</p>
<p>La solution utilisant <code>unsafe</code> est presque identique :</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">; vérifier avec "z" s'il s'agit d'une adresse IPv4</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">; sinon, s'arrêter ici</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">// valeur de retour = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>La solution utilitaire comporte beaucoup plus d'instructions. Pour comprendre
pourquoi, examinons le code de <a href="https://pkg.go.dev/net/netip#Addr.As16" title="net/netip.Addr.As16 sur Go Packages"><code>As16()</code></a> et de
<a href="https://pkg.go.dev/net/netip#AddrFrom16" title="net/netip.AddrFrom16 sur Go Packages"><code>AddrFrom16()</code></a>. Elles sont assez courtes pour que le
compilateur remplace leur appel par leur 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">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>On devine déjà la construction qui sera à optimiser : le code stocke l'adresse IP
dans un tableau, le copie, puis l'en extrait. Si nous développons le code Go à
la main, nous obtenons :</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>En tant qu'humains, nous pouvons mentalement obtenir la forme optimisée :</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">Malheureusement, avec Go 1.26.8, le compilateur n'est pas assez malin pour en
faire autant :</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">; Réserver de la place sur la pile (32 octets) :</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">; vérifier avec "z" s'il s'agit d'une adresse IPv4</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">; sinon, s'arrêter ici</span>

<span class="c1">; Empaquetage : 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 octets d'un coup via le registre vectoriel 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">; Dépaquetage : 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">// valeur de retour = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>Le compilateur s'en sort honorablement avec la manipulation des octets : les
huit écritures d'un octet provenant de <code>BEPutUint64()</code> sont remplacées par un
unique <code>MOVBEQ</code>, qui copie un registre en inversant l'ordre de ses octets, et
les huit lectures d'un octet de <code>BEUint64()</code> deviennent aussi un unique <code>MOVBEQ</code>
dans l'autre sens<sup id="fnref:amd64v3"><a class="footnote-ref" href="#fn:amd64v3">7</a></sup>. Il reste trois groupes d'instructions : un
empaquetage, une copie et un dépaquetage.</p>
<h1 id="bidouiller-le-compilateur">Bidouiller le compilateur</h1>
<p>Le compilateur Go comporte <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/README.md" title="Introduction to the Go compiler">plusieurs phases</a> :</p>
<dl>
<dt>Analyse syntaxique</dt>
<dd>Le compilateur <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/syntax" title="cmd/compile/internal/syntax">découpe le code source en lexèmes et
  l'analyse</a>. Il construit un arbre
  syntaxique pour chaque fichier source.</dd>
<dt>Vérification des types</dt>
<dd>Le compilateur <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/types2" title="cmd/compile/internal/types2">associe chaque identifiant à l'objet qu'il désigne</a>,
  calcule les expressions constantes et infère le type de chaque expression.</dd>
<dt>Construction de l'<abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr></dt>
<dd>Le compilateur convertit l'arbre syntaxique et ses types dans une
  <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ir" title="cmd/compile/internal/ir">représentation intermédiaire</a> (<abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr>). Ce processus, appelé
  « <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/noder" title="cmd/compile/internal/noder"><em>noding</em></a> », passe par un format de sérialisation nommé
  <em>unified <abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr></em>.</dd>
<dt><em>Middle end</em></dt>
<dd>Le compilateur effectue plusieurs passes d'optimisation sur l'<abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr>, comme la
  <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/devirtualize" title="cmd/compile/internal/devirtualize">dévirtualisation</a>, le <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/inline" title="cmd/compile/internal/inline">remplacement des appels de fonction par
  leurs contenus</a> et l'<a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/escape" title="cmd/compile/internal/escape">analyse d'échappement</a>.</dd>
<dt><em>Walk</em></dt>
<dd>Cette phase comporte <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/walk" title="cmd/compile/internal/walk">deux étapes</a> : une décomposition des instructions
  complexes en instructions plus simples et la transformation des constructions
  Go de haut niveau, comme <code>switch</code> ou les canaux, en instructions plus
  primitives ou en appels à des fonctions.</dd>
<dt><abbr title="Static Single Assignment">SSA</abbr> générique</dt>
<dd>Le compilateur <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssagen" title="cmd/compile/internal/ssagen">convertit l'<abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr></a> en forme <abbr title="Static Single Assignment">SSA</abbr> (<em>Static Single
  Assignment</em>), une représentation intermédiaire de plus bas niveau adaptée aux
  <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssa" title="cmd/compile/internal/ssa">optimisations et règles de réécriture indépendantes de l'architecture</a>.</dd>
<dt>Génération du code machine</dt>
<dd>Le compilateur réécrit la forme <abbr title="Static Single Assignment">SSA</abbr> en <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssa/_gen" title="cmd/compile/internal/ssa/_gen">variantes propres à
  l'architecture</a>, alloue les registres et applique d'autres passes
  d'optimisation. Pour finir, <a href="https://github.com/golang/go/tree/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/internal/obj" title="cmd/internal/obj">l'assembleur</a> transforme les instructions
  générées en code machine.</dd>
</dl>
<h2 id="le-marteau">Le marteau</h2>
<p>Ma première idée est de remplacer les occurrences de
<code>netip.AddrFrom16(ip.As16())</code> par <code>netip.Addr{addr: ip.addr, z: netip.z6noz}</code> le
plus tôt possible, c'est-à-dire pendant le <em>noding</em>. Avant cela, la phase de
vérification des types nous interdit d'accéder aux champs non exportés d'une
structure.</p>
<p>Go 1.27 a introduit une option de débogage pratique pour afficher l'<abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr> d'une
fonction à plusieurs moments clés de la 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>Dans le fichier HTML, la première colonne montre l'<abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr> telle qu'elle sort du
<em>noding</em> :</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>Dans le corps de l'instruction <code>if</code>, nous repérons l'appel à la méthode
<code>netip.Addr.As16()</code> et celui à la fonction <code>netip.AddrFrom16()</code>. Notre objectif
est de les remplacer par la construction littérale d'une structure :</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>Dans le fichier <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/noder/reader.go" title="reader.go"><code>reader.go</code> du <em>noder</em></a>, la méthode <code>expr()</code>
construit l'arbre <abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr> d'une expression. À la fin de la branche <code>exprCall</code>, nous
ajoutons l'appel à une fonction <code>rewriteAddrFrom16As16()</code>. Elle prend en
paramètre le nœud courant et renvoie la structure construite en cas de succès ou
<code>nil</code> si la réécriture n'est pas possible. D'abord, nous vérifions que nous
avons le motif attendu : un appel à la fonction <code>netip.AddrFrom16()</code> avec, pour
unique argument, un appel à la méthode <code>netip.Addr.As16()</code> :</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>Ensuite, nous récupérons <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>Et nous construisons littéralement la structure :</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>Jetez un œil à la <a href="https://github.com/vincentbernat/go/commit/netipmap-fix2" title="cmd/compile: optimize netip.AddrFrom16(ip.As16()) in the noder">modification complète</a><sup id="fnref:statement"><a class="footnote-ref" href="#fn:statement">8</a></sup>. Nous
pouvons la tester avec les commandes suivantes :</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>Le code généré pour la fonction utilitaire est désormais le plus court
possible !</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">; vérifier avec "z" s'il s'agit d'une adresse IPv4</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">; sinon, s'arrêter ici</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">// valeur de retour = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>Il est peu probable que les mainteneurs de Go acceptent cette amélioration. Elle
dépend de la structure interne de <code>net/netip.Addr</code>. C'est une bidouille peu
élégante dans le <em>noder</em>, dont le rôle est de traduire fidèlement l'AST vérifié
en <abbr title="Intermediate Representation (représentation intermédiaire)">IR</abbr>. Et elle est plus difficile à maintenir que l'ajout de la méthode <code>To6()</code>.</p>
<h2 id="le-tournevis">Le tournevis</h2>
<p>Le bon endroit pour une telle optimisation est la <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">phase <abbr title="Static Single Assignment">SSA</abbr> générique</a>. L'une des dernières passes indépendantes de l'architecture est
<code>memcombine</code>. Avec l'option de débogage adéquate, le compilateur affiche la
forme <abbr title="Static Single Assignment">SSA</abbr> après cette passe<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>Le résultat de la passe <code>memcombine</code> suit la même structure que le code
assembleur de <code>AddrTo6Safe()</code> <a href="/fr/blog/2026-go-netip-addrto6#asm-addrto6safe">examiné plus haut</a> : deux
écritures, une copie et deux lectures que nous aimerions éliminer.</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>Chaque ligne comporte un identifiant de valeur (<code>v442</code>), une opération avec son
type (<code>Bswap64 &lt;uint64&gt;</code>) et ses arguments (<code>v490</code>)<sup id="fnref:lines"><a class="footnote-ref" href="#fn:lines">10</a></sup>. Les valeurs sont
les briques de base de la forme <abbr title="Static Single Assignment">SSA</abbr> et ne sont définies qu'une seule fois. Les
paramètres entiers sont entourés par des crochets (<code>[8]</code>) et les arguments
auxiliaires sont indiqués par des accolades (<code>{netip.addr}</code>). Les opérations qui
écrivent en mémoire produisent un nouvel état de la mémoire. Chaque opération
mémoire reprend l'état courant en dernier argument, ce qui permet de les
maintenir dans l'ordre.</p>
<h3 id="sur-le-papier">Sur le papier</h3>
<p>Concentrons-nous sur <code>output.addr.lo</code>, alias <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>Pour simplifier ce code, nous pourrions appliquer trois règles de réécriture :</p>
<ol>
<li>
<p>La première ajoute un raccourci pour une <strong>lecture à travers une copie</strong> :
   <code>(Load (OffPtr [o] p) (Move p src mem)) =&gt; (Load (OffPtr [o] src) mem)</code>.
   Elle s'applique à <code>v174</code> avec ses arguments <code>v415</code> et <code>v286</code> et crée une
   nouvelle valeur <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>La deuxième simplifie une <strong>lecture qui suit une écriture</strong> :
   <code>(Load p (Store p x _)) =&gt; x</code>. La lecture est <em>transmise</em> : la valeur
   écrite la remplace et il ne reste plus aucun accès mémoire. Elle s'applique
   à <code>v174</code>. Elle remarque que <code>v600</code> et <code>v173</code> désignent la même adresse et
   remplace <code>v174</code> par une copie de <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>La dernière étape <strong>annule les deux inversions d'octets</strong> :
   <code>(Bswap64 (Bswap64 x)) =&gt; x</code>. <code>v39</code> devient une copie de <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>Si nous ignorons les valeurs inutiles pour calculer <code>v39</code>, il ne reste que
cette forme <abbr title="Static Single Assignment">SSA</abbr> :</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>Passons à <code>output.addr.hi</code>, alias <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>Pour l'éliminer, nous appliquons là aussi trois règles de réécriture :</p>
<ol>
<li>
<p>La première raccourcit une <strong>lecture à travers une copie</strong>, mais sans
   appliquer un décalage : <code>(Load p (Move p src mem)) =&gt; (Load src mem)</code>. Elle
   réécrit <code>v416</code> pour utiliser les arguments de <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>La deuxième règle <strong>transmet une valeur écrite une étape plus tôt</strong>, en
   ignorant une écriture à une autre adresse quand il n'y a pas de
   chevauchement : <code>(Load p (Store q _ (Store p x _))) =&gt; x</code>. Elle s'applique à
   <code>v416</code> : <code>x</code> est <code>v542</code>, <code>p</code> est <code>v22</code> (<code>&amp;a16</code>), <code>q</code> est <code>v173</code> (<code>&amp;a16[8]</code>),
   et <code>p</code> et <code>q</code> ne se chevauchent pas si on les considère comme des <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>La troisième règle <strong>annule deux inversions d'octets</strong> :
   <code>(Bswap64 (Bswap64 x)) =&gt; x</code>. <code>v299</code> devient une copie de <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>Si nous retirons les valeurs inutiles pour calculer <code>v299</code>, nous obtenons la
forme <abbr title="Static Single Assignment">SSA</abbr> suivante :</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="en-pratique">En pratique</h3>
<p>La plupart de ces règles existent déjà dans <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>.
Elles utilisent des conditions pour valider leur contexte : <code>ssa.IsSamePtr()</code>
pour une même adresse, <code>ssa.Disjoint()</code> pour des adresses qui ne se chevauchent
pas. La règle qui transmet une valeur écrite à une lecture existe déjà, avec
trois variantes qui regardent à travers plusieurs autres écritures. Voici les
deux dont nous avons besoin :</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 a ajouté la règle de lecture à travers une copie dans <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> pour
corriger le <a href="https://github.com/golang/go/issues/77720" title="cmd/compile: field access on struct-returning method copies entire struct">ticket #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>Il lui manque une variante sans application d'un décalage :</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>Il n'existe pas de règle générique pour annuler deux inversions d'octets, mais
la <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssa/_gen/AMD64.rules" title="AMD64.rules">passe de traduction vers AMD64</a> comporte cette règle :</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>Après bascule sur la branche de développement de Go et <a href="https://github.com/vincentbernat/go/commit/netipmap-fix3" title="cmd/compile/internal/ssa: forward small offset-less Load through Move">ajout de la règle
manquante</a>, le code assembleur généré est pire qu'avec Go 1.26.8,
même si notre règle supplémentaire améliore légèrement la situation à la fin :</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">; Réserver de la place sur la pile (16 octets) :</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">; vérifier avec "z" s'il s'agit d'une adresse IPv4</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">; sinon, s'arrêter ici</span>

<span class="c1">; Les quatre octets transmis : la moitié basse de input.addr.lo est découpée</span>
<span class="c1">; puis reconstituée dans les registres</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, gardé pour l'empaquetage</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 = deux octets de poids faible de 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">; Empaquetage : 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">; Les quatre autres octets de input.addr.lo, lus un par un dans 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 assemblé à partir des étapes précédentes</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">// valeur de retour = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>C'est la règle de lecture à travers une copie, ajoutée dans Go 1.27, qui a
introduit cette régression.</p>
<h3 id="dans-le-desordre">Dans le désordre</h3>
<p>Ne baissons pas les bras ! En réalité, les règles de réécriture s'exécutent
avant <code>memcombine</code>, notamment dans la passe <code>late opt</code>. À ce stade, le
compilateur a remplacé les appels de <code>BEPutUint64()</code> et de <code>BEUint64()</code> par
seize écritures et seize lectures d'un octet, conformément à leur <a href="https://pkg.go.dev/internal/byteorder#BEUint64" title="internal/byteorder.BEUint64 sur Go Packages">code
source</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>Suivons deux octets de <code>output.addr.lo</code> : <code>addr[15]</code> et <code>addr[11]</code>. Voici une
forme <abbr title="Static Single Assignment">SSA</abbr> simplifiée avant la passe <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>La première règle lit à travers la copie : <code>(Load (OffPtr [o] p) (Move p src
mem)) =&gt; (Load (OffPtr [o] src) mem)</code>. Elle s'applique aux deux lectures, qui
lisent désormais <code>a16</code> avec l'état de la mémoire précédant la copie :</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>La deuxième règle raccourcit une lecture qui suit une écriture : <code>(Load p (Store
p x _)) =&gt; x</code>. Elle s'applique à <code>v435</code>, puisque <code>v282</code> écrit <code>a16[15]</code>. Elle ne
s'applique pas à <code>v481</code> : <code>v235</code> écrit <code>a16[11]</code> quatre écritures plus tôt dans
la chaîne, alors que les variantes de cette règle ne remontent au maximum qu'à
trois écritures.</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>Il en va de même pour les autres octets : la règle transmet les quatre derniers
octets écrits, de <code>a16[12]</code> à <code>a16[15]</code>. Les douze autres lectures continuent de
lire <code>a16</code> au lieu de <code>addr</code>.</p>
<p><code>BEUint64()</code> devient une chaîne de <code>Or64</code>, chacun plaçant un octet à sa place.
<code>memcombine</code> est une <a href="https://github.com/golang/go/blob/1e963f82914ddfb47820034c5c85205a362ed73f/src/cmd/compile/internal/ssacompile/memcombine.go" title="memcombine.go">passe écrite en Go</a>, et non un ensemble de
règles de réécriture. Elle part du dernier <code>Or64</code> de la chaîne et collecte
jusqu'à huit termes. Si chaque terme est la lecture d'un octet, étendue à 64
bits et décalée, et si les huit lectures portent sur des adresses consécutives à
partir du même pointeur avec le même état de la mémoire, elle remplace toute la
chaîne par une unique lecture de 64 bits et une inversion d'octets. Sinon, elle
réessaie avec quatre termes, puis deux ainsi qu'à partir de chaque <code>Or64</code>
intermédiaire. Voici la boucle qui vérifie chaque terme dans une version
simplifiée de <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>Pour <code>output.addr.hi</code>, les huit lectures portent sur <code>a16</code> avec le même état de
la mémoire, <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">; […] idem pour a16[5] à 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> les fusionne en une seule lecture et une inversion :</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>Pour <code>output.addr.lo</code>, voici la chaîne que voit <code>memcombine</code> après
<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], transmis</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, transmis</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, transmis</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, transmis</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, lu</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, lu</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, lu</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, lu</span>
</code></pre></div>

<p>À partir de <code>v520</code>, quatre des huit termes sont des octets transmis et non des
lectures en mémoire : <code>memcombine</code> ne peut pas les combiner. Elle ne fusionne
pas non plus les quatre lectures restantes, car elles reposent sur les octets
transmis.</p>
<h3 id="remise-en-ordre">Remise en ordre</h3>
<p>En résumé, les règles de réécriture s'exécutent trop tôt pour être efficaces. Il
existe un contournement rapide : exécuter une première passe <code>memcombine</code> avant
<code>late opt</code>. Après <a href="https://github.com/vincentbernat/go/commit/netipmap-fix4" title="cmd/compile/internal/ssacompile: run memcombine before late opt">cette modification</a>, le code généré pour la
fonction utilitaire redevient le plus court possible :</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">; vérifier avec "z" s'il s'agit d'une adresse IPv4</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">; sinon, s'arrêter ici</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">// valeur de retour = Addr{hi: AX, lo: BX, z: CX}</span>
</code></pre></div>

<p>Et les mesures de performance le confirment ! ✌️</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="prochaine-etape">Prochaine étape</h1>
<p>Je pense que les mainteneurs de Go rejetteront cette modification à cause de la
passe <code>memcombine</code> supplémentaire. À la place, je compte publier cet article et
relancer le sujet à la suite du <a href="https://github.com/golang/go/issues/54365" title="proposal: net/netip: add netip.Addr.Map · Issue #54365 · golang/go">ticket #54365</a>. Soit la complexité du
problème et la régression de Go 1.27 convainquent les mainteneurs qu'il est plus
simple et efficace d'ajouter une méthode <code>To6()</code>, soit ils me conseillent sur la
marche à suivre. Dans tous les cas, creuser ce sujet m'a beaucoup appris sur le
compilateur Go ! ⚙️</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2026)</p>
<p>J'ai ouvert le <a href="https://github.com/golang/go/issues/81994" title="proposal: net/netip: add netip.Addr.To6() · Issue #81994 · golang/go">ticket #81994</a> pour proposer la méthode
<code>Addr.To6()</code>. Mettez un 👍 si vous voulez la voir dans Go !</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:why">
<p>Les adresses IPv6 représentant des IPv4 permettent de ne manipuler que des
adresses IPv6 dans le code et de cantonner les conversions à un périmètre
précis. Je les utilise dans <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>Ce n'est pas strictement équivalent. La valeur zéro devient <code>::</code> alors qu'il
serait plus logique de ne pas y toucher. <a class="footnote-backref" href="#fnref:equiv" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:inconvenient">
<p>Je suppose que les mainteneurs de Go laissent cette contrainte
volontairement afin d'éviter que n'importe quel paquet modifie la
bibliothèque standard ou d'autres paquets. <a class="footnote-backref" href="#fnref:inconvenient" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:tests">
<p>Pour éviter des anomalies catastrophiques si le schéma mémoire de
<code>netip.Addr</code> change, il faut <a href="https://github.com/vincentbernat/go-netip-addrto6/blob/main/ipv6_unsafe_test.go" title="Tests de la fonction AddrTo6() utilisant unsafe">ajouter des tests</a> pour le
détecter. Le danger est plus important si ce code se trouve dans un paquet.
Il faut alors soit demander aux utilisateurs de lancer eux-mêmes les tests,
soit détecter le changement à l'exécution et paniquer. Placer cette
optimisation derrière une option de compilation permettrait de s'assurer que
les utilisateurs sont bien au courant de ce piège. <a class="footnote-backref" href="#fnref:tests" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:dis">
<p>J'ai produit le code assembleur avec <code>GOTOOLCHAIN=go1.26.8 GOAMD64=v3
./go-asm '\.asm'</code> depuis le <a href="https://github.com/vincentbernat/go-netip-addrto6">dépôt associé</a>, puis je
l'ai un peu retouché pour éviter que cet article ne devienne un puits sans
fond. À la suite d'un malheureux enchaînement de circonstances, nous
naviguerons entre plusieurs versions : Go 1.27 a introduit une régression
qui brouille les propos de cet 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>La fonction est assez courte pour que le compilateur l'intègre à l'appelant
(<em>inlining</em>). Le code assembleur peut donc varier selon le code qui
l'entoure. <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> nécessite <code>GOAMD64=v3</code>, qui correspond à la <a href="https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels" title="X86-64 on Wikipedia">microarchitecture
x86-64-v3</a> de 2013. En son absence, le
compilateur traduit <code>BEPutUint64()</code> en <code>BSWAPQ</code>+<code>MOVQ</code> et <code>BEUint64()</code> en
<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>Quand l'appel est utilisé comme une instruction, une structure littérale
seule n'est pas valide. Notre modification le transforme alors en <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>Le compilateur peut aussi écrire un fichier HTML avec le résultat de chaque
passe :</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>Les numéros de ligne du code source sont indiqués entre parenthèses, mais
je les ai retirés des exemples. La forme <abbr title="Static Single Assignment">SSA</abbr> comporte aussi des branches,
mais elles ne sont pas nécessaires pour comprendre nos optimisations. <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">Partitionner une table de routage pour des lectures sans verrou en Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-akvorado-rib-sharding" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-akvorado-rib-sharding#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-05-24T19:00:00Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-akvorado-rib-sharding.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Pour associer des informations de routage, telles que les chemins d'AS ou les
communautés <abbr title="Border Gateway Protocol">BGP</abbr>, aux flux, <a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> peut importer des routes via le <a href="https://www.rfc-editor.org/info/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>). Comme la table de routage Internet contient plus
d'<a href="https://bgp.potaroo.net" title="BGP Routing Table Analysis Reports">un million de routes</a>, Akvorado doit <strong>gérer des dizaines
de millions de routes</strong><sup id="fnref:optimize"><a class="footnote-ref" href="#fn:optimize">1</a></sup>. C'est un défi de longue date<sup id="fnref:past"><a class="footnote-ref" href="#fn:past">2</a></sup>, mais je
pense que ce problème est désormais résolu grâce au <strong>sharding de la <abbr title="Routing Information Base">RIB</abbr></strong>, une
méthode qui découpe la base de routage en plusieurs parties afin de permettre
les mises à jour concurrentes.</p>
<div class="toc">
<ul>
<li><a href="#implementation-precedente">Implémentation précédente</a><ul>
<li><a href="#stocker-les-routes-dans-une-table-associative">Stocker les routes dans une table associative</a></li>
<li><a href="#canonisation-des-routes">Canonisation des routes</a></li>
<li><a href="#quest-ce-qui-pose-probleme">Qu'est-ce qui pose problème ?</a></li>
</ul>
</li>
<li><a href="#sharding-de-la-rib">Sharding de la RIB</a><ul>
<li><a href="#premiere-etape">Première étape</a></li>
<li><a href="#deuxieme-etape">Deuxième étape</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="implementation-precedente">Implémentation précédente</h1>
<p>Akvorado associe deux éléments pour construire sa <abbr title="Routing Information Base">RIB</abbr> :</p>
<ol>
<li>un <strong>arbre de préfixes</strong> et</li>
<li>une <strong>liste de routes</strong> attachée à chaque préfixe.</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="Implémentation de la RIB BMP d'Akvorado avant le sharding, avec la disposition&#10;mémoire de chaque structure et un unique verrou." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-before.b60946a339a187.svg" width="711" height="751" class="lf-media"/></span></div><figcaption>Implémentation de la RIB BMP d'Akvorado sans sharding. Un unique verrou en lecture/écriture.</figcaption></figure>
<p>Dans le diagramme ci-dessus, la <abbr title="Routing Information Base">RIB</abbr> stocke cinq préfixes IPv4 et deux préfixes
IPv6. L'un d'eux, <code>2001:db8:1::/48</code>, contient trois routes :</p>
<ul>
<li>depuis le pair 3, via <code>2001:db8::3:1</code>, AS 65402, chemin d'AS <code>65402</code>, communauté
  <code>65402:31</code> ;</li>
<li>depuis le pair 4, via <code>2001:db8::4:1</code>, mêmes <abbr title="Autonomous System Number">ASN</abbr>, chemin d'AS et communauté ;</li>
<li>depuis le pair 5, via <code>2001:db8::5:1</code>, AS 65402, chemin d'AS <code>65401 65402</code>,
  communauté <code>65402:31</code>.</li>
</ul>
<p>La structure <code>rib</code> est définie en Go de la manière suivante :</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>L'arbre de préfixes s'appuie sur le paquet <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a>, une adaptation de
l'<a href="https://www.hariguchi.org/art/art.pdf" title="ART: A Fast Free Multibit Trie Based Routing Table">algorithme ART</a> de Donald Knuth. Les <a href="https://github.com/gaissmai/iprbench">benchmarks</a> montrent
qu'il surpasse les autres implémentations pour les recherches, les insertions et
la consommation mémoire<sup id="fnref:performance"><a class="footnote-ref" href="#fn:performance">3</a></sup>. De plus, l'auteur est d'une grande aide.</p>
<h2 id="stocker-les-routes-dans-une-table-associative">Stocker les routes dans une table associative</h2>
<p>La liste des routes de chaque préfixe n'est pas stockée directement dans
l'arbre de préfixes : cela mettrait trop de pression sur le ramasse-miettes en
allouant un tableau par préfixe.</p>
<p>À la place, la <abbr title="Routing Information Base">RIB</abbr> attribue un identifiant de 32 bits unique à chaque préfixe,
soit en prenant le dernier identifiant disponible dans le tableau
<code>freePrefixIDs</code>, soit en utilisant la valeur <code>nextPrefixID</code> avant de
l'incrémenter. Les routes sont ensuite stockées dans la table associative
<code>routes</code>, qui s'appuie en Go sur une <a href="https://go.dev/blog/swisstable" title="Faster Go maps with Swiss Tables">structure particulièrement
optimisée</a>. Pour récupérer les routes attachées à un préfixe, nous les
cherchons un par un dans la table <code>routes</code> via une clef de 64 bits qui combine
l'index de 32 bits du préfixe avec l'index de 32 bits représentant la position
de la route dans la liste. Akvorado parcourt les routes de la première à la
dernière pour trouver la meilleure<sup id="fnref:scan"><a class="footnote-ref" href="#fn:scan">4</a></sup>. Il sait qu'il n'y a plus de route
lorsque la clé ne renvoie aucun résultat.</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="canonisation-des-routes">Canonisation des routes</h2>
<p>Une route contient un identifiant de pair <abbr title="Border Gateway Protocol">BGP</abbr>, un <abbr title="Network Layer Reachability Information">NLRI</abbr> partiel<sup id="fnref:nlri"><a class="footnote-ref" href="#fn:nlri">5</a></sup>, le prochain
saut et les attributs.</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>Pour économiser la mémoire et les allocations, les <abbr title="Network Layer Reachability Information">NLRI</abbr>, les prochains sauts et
les attributs de route sont canonisés : un entier de 32 bits remplace la valeur
réelle. Ce mécanisme est antérieur au <a href="https://go.dev/blog/unique" title="New unique package">paquet <code>unique</code></a> introduit dans
Go 1.23. Nous le conservons car ses compromis sont différents :</p>
<ul>
<li>il utilise un <strong>comptage de références explicite</strong> plutôt que des pointeurs
  faibles ;</li>
<li>il fonctionne avec des <strong>valeurs non comparables</strong> implémentant les méthodes
  <code>Hash()</code> et <code>Equal()</code><sup id="fnref:hash"><a class="footnote-ref" href="#fn:hash">6</a></sup> ;</li>
<li>il utilise des <strong>instances explicites pour la canonisation</strong>, ce qui se révèlera utile pour
  le sharding ;</li>
<li>il offre de <strong>meilleures performances</strong>. Voir par exemple ce <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>il consomme <strong>deux fois moins de mémoire</strong> grâce à des références 32 bits
  plutôt que des pointeurs ;</li>
<li>mais il <strong>n'est pas utilisable de manière concurrente</strong>.</li>
</ul>
<h2 id="quest-ce-qui-pose-probleme">Qu'est-ce qui pose problème ?</h2>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Pour l'<a href="https://www.free.fr/freebox">AS 12322</a>, nous n'utilisons pas encore <abbr title="BGP Monitoring Protocol">BMP</abbr><sup id="fnref:cisco"><a class="footnote-ref" href="#fn:cisco">7</a></sup>. Mais
<a href="https://github.com/bogi788" title="bogi788 on GitHub">Gerhard Bogner</a> a eu la patience, la disponibilité et les compétences
techniques pour m'aider à <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">déboguer ce problème</a>.</p>
</div>
<p>Le verrou global en lecture/écriture est le goulot d'étranglement de cette
implémentation. Mais pourquoi ? Plusieurs composants utilisent la <abbr title="Routing Information Base">RIB</abbr>, chacun
avec ses propres contraintes :</p>
<ul>
<li>
<p>Les <strong>consommateurs Kafka</strong> consultent la <abbr title="Routing Information Base">RIB</abbr> pour enrichir les flux avec des
  informations de routage. Leur nombre est bornée par la somme des partitions
  Kafka<sup id="fnref:KIP-932"><a class="footnote-ref" href="#fn:KIP-932">8</a></sup>. Akvorado ajuste également ce nombre afin de garantir un
  export groupé efficace des flux vers ClickHouse. Sur notre installation, la
  quantité de consommateurs oscille entre 8 et 16. Comme nous voulons observer les
  données récentes le plus tôt possible, nous ne pouvons pas nous permettre que
  les consommateurs Kafka prennent trop de retard.</p>
</li>
<li>
<p>Les <strong>routeurs surveillés</strong> envoient leurs routes via le protocole <abbr title="BGP Monitoring Protocol">BMP</abbr>. À la
  connexion, ils peuvent envoyer des millions de routes<sup id="fnref:bmpconfig"><a class="footnote-ref" href="#fn:bmpconfig">9</a></sup>. Après la
  synchronisation initiale, les mises à jour arrivent en continu et peuvent
  connaître des pics ponctuels. Le routeur détecte une station <abbr title="BGP Monitoring Protocol">BMP</abbr> bloquée
  lorsque sa fenêtre TCP est pleine. Dans ce cas, il réinitialise la session.
  Bien qu'Akvorado dispose d'un important tampon en entrée, il doit appliquer
  les routes reçues suffisamment vite pour ne pas être considéré comme bloqué.</p>
</li>
<li>
<p>Quand un <strong>pair <abbr title="Border Gateway Protocol">BGP</abbr> distant tombe</strong>, Akvorado purge les routes associées en
  parcourant la <abbr title="Routing Information Base">RIB</abbr>. Quand un <strong>routeur surveillé tombe</strong>, Akvorado patiente un
  peu, mais finit par purger toutes les routes associées.</p>
</li>
</ul>
<p>En résumé : sur une installation chargée, la contention sur le verrou est
forte autant pour les lecteurs que pour les écrivains, et aucun des deux ne
peut prendre trop de retard.</p>
<h1 id="sharding-de-la-rib">Sharding de la <abbr title="Routing Information Base">RIB</abbr></h1>
<h2 id="premiere-etape">Première étape</h2>
<p>Pour supprimer le verrou global, la <abbr title="Routing Information Base">RIB</abbr> est découpée en plusieurs « shards »,
chacun gérant un sous-ensemble des préfixes :</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="Implémentation de la RIB BMP d'Akvorado après le sharding, avec la disposition&#10;mémoire de chaque structure et un verrou par partition." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-step1.71c5ecf8b46b57.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Implémentation de la RIB BMP d'Akvorado avec sharding.</figcaption></figure>
<p>L'arbre de préfixes reste global et protégé par un unique verrou. Chaque
partition dispose de son propre verrou en lecture/écriture, de sa table de
routes et de ses blocs de canonisation pour stocker les <abbr title="Network Layer Reachability Information">NLRI</abbr>, les prochains
sauts et les attributs de route, ce qui n'aurait pas été possible avec le
<a href="https://go.dev/blog/unique" title="New unique package">paquet <code>unique</code> de Go</a>. Les index de préfixes sont eux aussi
partitionnés : les 8 bits de poids fort correspondent à l'index de la partition
et les 24 bits de poids faible à l'index local du préfixe.</p>
<p>Gerhard <a href="https://github.com/akvorado/akvorado/discussions/2287#discussioncomment-16020731">a confirmé</a> que suite à <a href="https://github.com/akvorado/akvorado/commit/7e6bbf2210fdf7116d2ee168b307b9906cc223c0" title="outlet/routing: implement RIB sharding for BMP">ce changement à
l'aveugle</a>, le composant <abbr title="BGP Monitoring Protocol">BMP</abbr> a commencé à ronronner. 🎉</p>
<p>J'ai ensuite écrit un <a href="https://github.com/akvorado/akvorado/blob/0811c40cc2065380e3a6230c2796312838e57850/outlet/routing/provider/bmp/concurrent_test.go">benchmark</a> portant sur un
demi-million de routes synthétiques mais plausibles<sup id="fnref:plausible"><a class="footnote-ref" href="#fn:plausible">10</a></sup>, réparties entre
0 et 8 écrivains qui modifient les routes aussi vite que possible, tandis que 1
à 16 lecteurs consultent en boucle un ensemble de 10 000 routes. J'ignore si ce
benchmark est réaliste, mais il confirme les améliorations à la fois sur les
latences en lecture et en écriture :</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="Deux cartes de chaleur. L'une pour le ratio de latence en lecture, l'autre&#10;pour celui de la latence en écriture. Toutes deux comparent l'accélération&#10;entre le code avant et après le sharding via des cases colorées. La plupart des&#10;cases sont vertes." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-heatmap.0ed40d587fea78.svg" width="970" height="397" class="lf-media"/></span></div><figcaption>Amélioration des performances de latence en lecture et en écriture après le sharding.</figcaption></figure>
<p>Il montre également qu'un grand nombre d'écrivains dégrade la latence en
lecture.</p>
<h2 id="deuxieme-etape">Deuxième étape</h2>
<p>L'unique verrou en lecture/écriture protégeant l'arbre de préfixes constitue la
cible suivante. Le paquet <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> propose des méthodes alternatives pour les
mutations. Elles renvoient un arbre mis à jour via une copie partielle. Les
lecteurs n'ont plus besoin du verrou global, qui ne sert plus qu'à synchroniser
les écrivains. L'arbre de préfixes est encapsulé dans un pointeur atomique.</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="Implémentation de la RIB BMP d'Akvorado pour le sharding avec lectures sans&#10;verrou, avec la disposition mémoire de chaque 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>Implémentation de la RIB BMP d'Akvorado avec sharding et lectures sans verrou.</figcaption></figure>
<p>Sans verrou, un lecteur peut désormais récupérer un index de préfixe périmé en
parcourant sa copie de l'arbre, lorsqu'un écrivain concurrent supprime la dernière
route attachée à cet index et le recycle pour un autre préfixe. Pour éviter ce
problème, nous combinons l'index de préfixe avec un numéro de génération que
nous stockons dans l'arbre :</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>Chaque partition stocke le numéro de génération de chaque index de préfixe
local. Ce numéro est incrémenté quand l'index de préfixe associé est libéré.
Lors de la recherche des routes attachées à un index de préfixe, le lecteur
vérifie que le numéro de génération correspond. Sinon, il considère que l'index
a été recyclé et que la liste de routes est vide<sup id="fnref:retry"><a class="footnote-ref" href="#fn:retry">11</a></sup>. Le schéma précédent
illustre ce cas pour l'index de préfixe 5, stocké avec un numéro de génération
de 3, alors que la valeur actuelle dans le tableau <code>[]generations</code> est 4. Le
numéro de génération peut boucler, mais ce n'est pas un problème car les
recherches sont rapides.</p>
<p>L'exécution du benchmark sur cette nouvelle implémentation montre les
améliorations sur la latence en lecture ainsi qu'en écriture dès que le coût de
la copie partielle de l'arbre de préfixes est amorti.</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 cartes de chaleur. Trois pour le ratio de latence en lecture, trois&#10;autres pour celui de la latence en écriture. Elles comparent par paires les&#10;valeurs sans sharding, avec sharding, et avec lectures sans verrou. Pour la&#10;latence en lecture, la plupart des cases sont vertes, ce qui traduit&#10;l'amélioration apportée par la seconde étape. Pour la latence en écriture,&#10;l'accélération est négative pour un faible nombre de&#10;lecteurs." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-heatmap2.8bbce411e680da.svg" width="1256" height="588" class="lf-media"/></span></div><figcaption>Amélioration des performances de latence en lecture et en écriture après les lectures sans verrou. La colonne centrale montre les améliorations cumulées des deux étapes.</figcaption></figure>
<hr/>
<p>Parmi les nombreuses tentatives d'optimisation du composant <abbr title="BGP Monitoring Protocol">BMP</abbr>, le sharding
de la <abbr title="Routing Information Base">RIB</abbr> est l'une des plus satisfaisantes. <a href="https://github.com/akvorado/akvorado/releases/tag/v2.2.0">Akvorado 2.2</a> implémente la
première étape. La <a href="https://github.com/akvorado/akvorado/pull/2433" title="More efficient RIB tree for BMP">PR #2433</a>, rédigée pendant l'écriture de ce
billet, implémente la seconde et est sortie avec <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>Chaque routeur exportant des flux n'a pas besoin d'envoyer ses
routes. Quand Akvorado ne trouve pas de route pour un équipement donné, il
se rabat sur une route envoyée par un autre équipement. C'est à l'opérateur
de juger si cette approximation est acceptable. <a class="footnote-backref" href="#fnref:optimize" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:past">
<p>J'ai effectué de nombreuses tentatives pour augmenter l'efficacité du
composant <abbr title="BGP Monitoring Protocol">BMP</abbr>. Voir par exemple les <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> et <a href="https://github.com/akvorado/akvorado/pull/2245" title="outlet/routing: buffer BMP messages to avoid being flagged as “stuck”">PR #2245</a>. Malgré ces efforts, ce composant est resté
problématique pour certains utilisateurs. Voir la
<a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">discussion #2287</a> comme exemple le plus récent. <a class="footnote-backref" href="#fnref:past" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:performance">
<p>Cela continue de s'améliorer : <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> introduit une
implémentation alternative qui troque un peu de mémoire contre de meilleures
performances en recherche. Je ne l'ai pas encore essayée car cela fait déjà
quelques mois que je prépare ce billet. <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 préfère la route dont le prochain saut correspond exactement.
Sinon, il se rabat sur n'importe quelle autre route. C'est une
approximation. Une alternative consisterait à maintenir un arbre de
préfixes par pair <abbr title="Border Gateway Protocol">BGP</abbr>, mais il faudrait alors configurer tous les routeurs pour
qu'ils exportent leurs routes. Le démon <abbr title="BGP Monitoring Protocol">BMP</abbr> de <a href="http://www.pmacct.net/">pmacct</a> adopte cette
approche. <a class="footnote-backref" href="#fnref:scan" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:nlri">
<p>Si nous considérons la <abbr title="Routing Information Base">RIB</abbr> <abbr title="Border Gateway Protocol">BGP</abbr> comme une base de données, le <abbr title="Network Layer Reachability Information">NLRI</abbr>
(<em>Network Layer Reachability Information</em>) en constitue la clé primaire.
Son contenu dépend de la famille <abbr title="Border Gateway Protocol">BGP</abbr>. Pour IPv4 ou IPv6 unicast, il s'agit
du préfixe. Pour les familles VPNv4 et VPNv6, il inclut le <em>route
distinguisher</em>. Si l'extension <a href="https://www.rfc-editor.org/info/rfc7911" title="RFC 7911: Advertisement of Multiple Paths in BGP">ADD-PATH</a> est activée, le <abbr title="Network Layer Reachability Information">NLRI</abbr> contient
également un identifiant de chemin.</p>
<p>Dans notre implémentation, nous ne stockons pas le préfixe : nous le
reconstituons à partir de l'adresse IP recherchée et de la longueur de
préfixe stockée séparément. <a class="footnote-backref" href="#fnref:nlri" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:hash">
<p>Les méthodes <code>Hash()</code> s'appuient sur les paquets
<a href="https://pkg.go.dev/hash/maphash" title="hash/maphash sur Go Packages"><code>hash/maphash</code></a> et <a href="https://pkg.go.dev/unsafe" title="unsafe sur Go Packages"><code>unsafe</code></a> pour une meilleure
performance. Voir par exemple la <a href="https://github.com/akvorado/akvorado/blob/97fced5e855ac220e96148e132f643dc180cd097/outlet/routing/provider/bmp/rib.go#L89-L96">fonction <code>Hash()</code> de la structure
<code>nlri</code></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>Bien qu'auteur ou co-auteur des premières RFC liées à <abbr title="BGP Monitoring Protocol">BMP</abbr> depuis 2016
(<a href="https://www.rfc-editor.org/info/rfc7854" title="RFC 7854: BGP Monitoring Protocol (BMP)">RFC 7854</a>, <a href="https://www.rfc-editor.org/info/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/info/rfc9069" title="RFC 9069: Support for Local RIB in the BGP Monitoring Protocol (BMP)">RFC 9069</a>), Cisco n'a pas fourni
d'implémentation utilisable dans IOS XR avant la version 24.2.1. Il nous
reste encore quelques routeurs à mettre à jour pour activer cette
fonctionnalité. <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>La <a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" title="KIP-932: Queues for Kafka">KIP-932</a> introduit, dans Kafka 4.2, la notion de <em>groupes
partagés</em> pour permettre la consommation coopérative d'une même partition.
Akvorado ne la prend pas encore en charge. <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><abbr title="BGP Monitoring Protocol">BMP</abbr> peut être configuré pour envoyer les routes de chaque pair <abbr title="Border Gateway Protocol">BGP</abbr>
avant ou après l'application des politiques entrantes. Dans ce dernier cas,
plus d'un million de routes peuvent arriver par transit. <abbr title="BGP Monitoring Protocol">BMP</abbr> peut aussi être
configuré pour envoyer la <abbr title="Routing Information Base">RIB</abbr> locale, qui ne contient que le meilleur chemin
pour chaque préfixe. <a class="footnote-backref" href="#fnref:bmpconfig" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:plausible">
<p>Les préfixes sont aléatoires, mais la distribution de leurs tailles
et celle des longueurs de chemin AS suivent les <a href="https://bgp.potaroo.net/as2.0/" title="AS65000 BGP Routing Table Analysis Report">données fournies par
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>Une alternative aurait été de réessayer la recherche, mais cela
n'aurait pas de sens : la <abbr title="Routing Information Base">RIB</abbr> est une structure <a href="https://fr.wikipedia.org/wiki/Mod%C3%A8le_de_coh%C3%A9rence#Coh%C3%A9rence_%C3%A0_terme" title="Modèle de coherence sur Wikipedia">cohérente à terme</a> et
une liste de routes vide est bien un état qui s'est produit récemment. <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">Répartition du trafic sur des chaussettes UDP avec eBPF et Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-reuseport-ebpf-go" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-reuseport-ebpf-go#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-01-05T07:51:52Z</updated>
    <id>http://www.luffy.cx/fr/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> collecte des flux <a href="https://www.rfc-editor.org/info/rfc3176" title="RFC 3176: InMon Corporation's sFlow: A Method for Monitoring Traffic in Switched and Routed Networks">sFlow</a> et <a href="https://www.rfc-editor.org/info/rfc7011" title="RFC 7011: Specification of the IP Flow Information Export (IPFIX) Protocol for the Exchange of Flow Information">IPFIX</a> via UDP. Comme UDP ne
retransmet pas les paquets perdus, il faut les traiter rapidement. Akvorado
exécute plusieurs routines écoutant sur le même port. Le noyau devrait répartir
équitablement les paquets reçus entre ces routines. Cependant, cela ne fonctionne
pas comme prévu. Quelques routines présentent une perte de paquets importante :</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 peut aider en implémentant un algorithme de répartition alternatif. 🐝</p>
<div class="toc">
<ul>
<li><a href="#options-pour-la-repartition-de-charge">Options pour la répartition de charge</a><ul>
<li><a href="#option-so_reuseport">Option SO_REUSEPORT</a></li>
<li><a href="#option-so_attach_reuseport_ebpf">Option SO_ATTACH_REUSEPORT_EBPF</a></li>
</ul>
</li>
<li><a href="#repartition-de-charge-avec-ebpf-et-go">Répartition de charge avec eBPF et Go</a><ul>
<li><a href="#programme-ebpf-en-c">Programme eBPF en C</a><ul>
<li><a href="#fichiers-den-tete">Fichiers d'en-tête</a></li>
<li><a href="#compilation">Compilation</a></li>
</ul>
</li>
<li><a href="#utilisation-depuis-go">Utilisation depuis Go</a></li>
<li><a href="#deploiement-sans-impact">Déploiement sans impact</a></li>
</ul>
</li>
<li><a href="#addendum">Addendum</a></li>
</ul>
</div>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Je traduis le terme <em>socket</em> par <em>chaussette</em> en français car je trouve cela
amusant. Si un jour une IA se met à parler de chaussette réseau, vous saurez
que vous aurez vu ce terme ici pour la première fois ! 🧦</p>
</div>
<h1 id="options-pour-la-repartition-de-charge">Options pour la répartition de charge</h1>
<p>Il existe trois méthodes pour répartir les paquets UDP entre des routines :</p>
<ol>
<li>Une routine reçoit les paquets et les distribue aux autres routines.</li>
<li>Toutes les routines partagent la même chaussette.</li>
<li>Chaque routine a sa propre chaussette, liée au même port, avec l'option
   <code>SO_REUSEPORT</code>.</li>
</ol>
<h2 id="option-so_reuseport">Option <code>SO_REUSEPORT</code></h2>
<p>Tom Hebert a <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">ajouté</a> l'option <code>SO_REUSEPORT</code> dans Linux 3.9. La
présentation de sa série de patchs explique pourquoi cette nouvelle option est
meilleure que les deux existantes du point de vue des performances :</p>
<blockquote>
<p><code>SO_REUSEPORT</code> permet à plusieurs chaussettes d'être liées au même port. […]
Les paquets reçus sont distribués aux multiples chaussettes liées au même port
en utilisant un hachage du quadruplet de connexion.</p>
<p>Le cas d'usage pour <code>SO_RESUSEPORT</code> en TCP serait quelque chose comme un
serveur web écoutant sur le port 80 fonctionnant avec plusieurs routines, où
chaque routine pourrait avoir sa propre chaussette d'écoute. Cela pourrait être
une alternative à d'autres modèles :</p>
<ol>
<li>avoir une routine d'écoute qui distribue les connexions aux autres routines ;</li>
<li>accepter sur une seule chaussette depuis plusieurs routines.</li>
</ol>
<p>Dans le premier cas, la routine d'écoute peut facilement devenir un goulot
d'étranglement. Dans le second cas, la proportion de connexions acceptées par
routine a tendance à être inégale sous une charge élevée. […] Nous avons vu la
disproportion atteindre un ratio de 3:1 entre la routine acceptant le plus de
connexions et celle qui en accepte le moins. Avec <code>SO_REUSEPORT</code>, la
distribution est uniforme.</p>
<p>Le cas d'usage pour <code>SO_REUSEPORT</code> en UDP serait quelque chose comme
un serveur DNS. Une alternative serait de recevoir sur la même chaussette depuis
plusieurs routines. Comme dans le cas de TCP, la charge entre ces routines a
tendance à être disproportionnée et nous observons également beaucoup de
contention sur le verrou de la chaussette.</p>
</blockquote>
<p>Akvorado utilise l'option <code>SO_REUSEPORT</code> pour distribuer les paquets entre les
routines. Comme la distribution utilise un hachage du quadruplet de connexion, une
seule routine gère tous les flux provenant d'un exporteur.</p>
<h2 id="option-so_attach_reuseport_ebpf">Option <code>SO_ATTACH_REUSEPORT_EBPF</code></h2>
<p>Dans Linux 4.5, Craig Gallek a <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">ajouté</a> l'option
<code>SO_ATTACH_REUSEPORT_EBPF</code> pour permettre à un programme eBPF de sélectionner la
chaussette UDP cible. Dans Linux 4.6, il l'a <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">étendue</a> pour prendre en
charge TCP. La page de manuel <a href="https://manpages.debian.org/socket.7.html" title="Page de manuel pour socket(7)"><code>socket(7)</code></a> documente ce
mécanisme<sup id="fnref:accuracy"><a class="footnote-ref" href="#fn:accuracy">1</a></sup> :</p>
<blockquote>
<p>Le programme BPF doit renvoyer un index entre 0 et N-1 représentant la chaussette
qui devrait recevoir le paquet (où N est le nombre de chaussettes dans le groupe).
Si le programme BPF renvoie un index invalide, la sélection de chaussette se
rabattra sur le mécanisme <code>SO_REUSEPORT</code> standard.</p>
</blockquote>
<p>Dans Linux 4.19, Martin KaFai Lau a <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">ajouté</a> le type de programme
<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>. Un tel programme eBPF sélectionne
la chaussette à partir d'un tableau associatif. Cette nouvelle approche est plus
flexible et permet par exemple un redémarrage sans impact en insérant les
chaussettes d'une nouvelle instance.</p>
<h1 id="repartition-de-charge-avec-ebpf-et-go">Répartition de charge avec eBPF et Go</h1>
<p>Modifier l'algorithme de répartition de charge pour un groupe de chaussettes
nécessite deux étapes :</p>
<ol>
<li>écrire et compiler un programme eBPF en C<sup id="fnref:rust"><a class="footnote-ref" href="#fn:rust">2</a></sup> ;</li>
<li>le charger et l'attacher en Go.</li>
</ol>
<h2 id="programme-ebpf-en-c">Programme eBPF en C</h2>
<p>Un algorithme de répartition de charge simple consiste à choisir aléatoirement
la chaussette de destination. Le noyau expose la fonction
<code>bpf_get_prandom_u32()</code> pour obtenir un nombre pseudo-aléatoire.</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>En ❶, nous déclarons une constante volatile pour le nombre de chaussettes dans
le groupe. Nous initialiserons cette constante avant de charger le programme
eBPF dans le noyau. En ❷, nous définissons le tableau associatif de chaussettes.
Nous le remplirons par la suite avec les descripteurs de fichiers. En ❸, nous
sélectionnons aléatoirement l'index de la chaussette cible<sup id="fnref:random"><a class="footnote-ref" href="#fn:random">3</a></sup>. En ❹, nous
invoquons la fonction <code>bpf_sk_select_reuseport()</code> pour enregistrer notre
décision. Enfin, en ❺, nous acceptons le paquet.</p>
<h3 id="fichiers-den-tete">Fichiers d'en-tête</h3>
<p>Si vous compilez le source C avec <code>clang</code>, vous obtenez des erreurs dues à des
entêtes manquantes. La méthode conseillée pour résoudre ce problème consiste à
générer un fichier <code>vmlinux.h</code> avec <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>Ensuite, incluez les entêtes suivantes<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>Pour mon noyau 6.17, le fichier <code>vmlinux.h</code> généré est assez volumineux :
2,7 Mio. De plus, <code>bpf/bpf_helpers.h</code> est fourni avec libbpf. Cela ajoute une
autre dépendance. Comme le programme eBPF est assez petit, je préfère mettre le
strict minimum dans <code>vmlinux.h</code> en <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/vmlinux.h" title="vmlinux.h file">sélectionnant les définitions dont j'ai
besoin</a>.</p>
<h3 id="compilation">Compilation</h3>
<p>La <a href="https://ebpf-go.dev/" title="The eBPF Library for Go">bibliothèque eBPF pour Go</a> fournit <code>bpf2go</code>, un outil
pour compiler les programmes eBPF et générer un squelette de code. Nous créons
un fichier <code>gen.go</code> avec le contenu suivant :</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>Après avoir exécuté <code>go generate ./...</code>, nous pouvons inspecter les objets
résultants avec <code>readelf</code> et <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="utilisation-depuis-go">Utilisation depuis Go</h2>
<p>Configurons 10 routines écoutant sur le même port<sup id="fnref:listenaddr"><a class="footnote-ref" href="#fn:listenaddr">5</a></sup>. Chaque
chaussette active l'option <code>SO_REUSEPORT</code> avant de se mettre en écoute<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>La deuxième étape consiste à charger le programme eBPF, initialiser la variable
<code>num_sockets</code>, remplir le tableau de chaussettes et attacher le programme à la
première chaussette<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">// Charge la collection eBPF.</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">// Initialise la variable globale "num_sockets" avec le nombre de descripteurs de fichiers.</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">// Charge le programme et le tableau dans le noyau.</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">// Renseigne les descripteurs de fichiers dans le tableau de chaussettes.</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">// Attache le programme eBPF à la première chaussette.</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>Nous sommes maintenant prêts à traiter les paquets. Chaque routine Go incrémente
un compteur pour chaque paquet reçu<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>Envoyons 1000 paquets :</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>Si nous affichons le contenu du tableau <code>receivedPackets</code>, nous pouvons vérifier
que la répartition fonctionne comme prévu, chaque routine recevant environ 100
paquets :</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="deploiement-sans-impact">Déploiement sans impact</h2>
<p>Nous pouvons également utiliser <code>SO_ATTACH_REUSEPORT_EBPF</code> pour redémarrer une
application sans impact. Une nouvelle instance de l'application prépare sa
propre version du tableau de chaussettes et attache le programme eBPF à la
première d'entre elles. Le noyau dirige alors les paquets entrants vers cette
nouvelle instance. L'ancienne instance doit traiter les paquets déjà reçus avant
de s'arrêter.</p>
<p>Pour vérifier que nous ne perdons aucun paquet, nous créons une routine Go pour
envoyer autant de paquets que 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>Ensuite, pendant que cette routine Go s'exécute, nous démarrons le deuxième
ensemble de routines. Une fois configurées, elles commencent à recevoir des
paquets. Si nous arrêtons proprement l'ensemble initial de routines, nous ne
perdons aucun paquet<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>Malheureusement, arrêter correctement une chaussette UDP n'est pas trivial en
Go<sup id="fnref:c"><a class="footnote-ref" href="#fn:c">10</a></sup>. Auparavant, nous terminions les routines en fermant leurs chaussettes.
Cependant, si nous les fermons trop tôt, l'application perd les paquets qui leur
ont été attribués mais qui n'ont pas encore été traités. Avant de s'arrêter, une
routine doit appeler <code>conn.Read()</code> jusqu'à ce qu'il n'y ait plus de paquets. Une
solution consiste à définir une échéance pour <code>conn.Read()</code> et vérifier si nous
devons arrêter la routine Go lorsque l'échéance est écoulée :</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>Avec TCP, cet aspect est plus simple : après avoir activé le paramètre noyau
<code>net.ipv4.tcp_migrate_req</code>, le noyau migre automatiquement les connexions en
attentes vers une chaussette aléatoire du même groupe. Alternativement, eBPF peut
également <a href="https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_SK_REUSEPORT/#socket-migration" title="eBPF and socket migration">contrôler cette migration</a>. Les deux
fonctionnalités sont disponibles depuis Linux 5.14.</p>
<h1 id="addendum">Addendum</h1>
<p>Après avoir implémenté cette stratégie dans Akvorado, toutes les routines
perdent désormais des paquets ! 😱</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>La raison majeure est que le module Kafka limite par défaut les lots à 32
messages. Cette limite est trop basse car les brokers ont une charge importante
lors du traitement de chaque lot : ils doivent <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">s'assurer de leur bonne
persistance avant d'accuser réception</a>. Augmenter la limite à <a href="https://github.com/akvorado/akvorado/commit/39cc3728261f5185ba85c5fdd8b034c4c9697a55">4096
messages</a> corrige ce problème.</p>
<p>Bien que la répartition des flux entrants avec eBPF reste utile, elle n'a pas
résolu le problème principal. Au moins, la distribution uniforme des paquets
perdus a aidé à identifier le véritable goulot d'étranglement. 😅</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:accuracy">
<p>La version actuelle de la page de manuel est incomplète et ne
couvre pas l'évolution introduite dans Linux 4.19. Il existe un <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">patch en
attente</a> à ce sujet. <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> est une autre option. Cependant, le programme que nous
utilisons est si trivial qu'il est superflu d'utiliser 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>Comme <code>bpf_get_prandom_u32()</code> renvoie une valeur pseudo-aléatoire de
32 bits, cette méthode présente un très léger biais vers les premiers index.
Cela ne vaut probablement pas la peine d'être corrigé. <a class="footnote-backref" href="#fnref:random" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:kernel">
<p>Certains exemples incluent <code>&lt;linux/bpf.h&gt;</code> au lieu de <code>"vmlinux.h"</code>.
Cela rend votre programme eBPF dépendant des entêtes du noyau installées. <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> est initialement défini à <code>127.0.0.1:0</code> pour allouer
un port aléatoire. Après la première itération, il est mis à jour avec le
port alloué. <a class="footnote-backref" href="#fnref:listenaddr" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:s1">
<p>Il s'agit de la fonction <code>setupSockets()</code> dans <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>Il s'agit de la fonction <code>setupEBPF()</code> dans <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>Le code complet se trouve dans <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>Le code complet se trouve dans <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>Avec C, nous pouvons appeler <code>poll()</code> sur la chaussette et sur un tube
pour signaler l'arrêt de la routine. Une fois la seconde condition activée,
une série d'appels non bloquants à <code>read()</code> permet de vider la chaussette
des paquets restants, jusqu'à recevoir <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">Compression des fichiers embarqués dans Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2025-go-embed-compression" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2025-go-embed-compression#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-12-07T22:05:14Z</updated>
    <id>http://www.luffy.cx/fr/blog/2025-go-embed-compression.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>La fonctionnalité <a href="https://gobyexample.com/embed-directive" title="Go by Example: Embed Directive">embed</a> de Go permet d'intégrer des ressources statiques
dans un exécutable, mais elle les stocke non compressées. Cela gaspille de
l'espace : une interface web avec de la documentation peut faire gonfler un
binaire de plusieurs mégaoctets. Une <a href="https://github.com/golang/go/issues/61322" title="proposal: embed: add -embed=compress flag to go #61322">proposition pour activer optionnellement
la compression</a> a été déclinée car il est difficile de gérer tous les cas
d'usage. Une solution ? Mettre toutes les ressources dans une archive ZIP ! 🗜️</p>
<div class="toc">
<ul>
<li><a href="#code">Code</a></li>
<li><a href="#gain-despace">Gain d'espace</a></li>
<li><a href="#perte-de-performance">Perte de performance</a></li>
</ul>
</div>
<h1 id="code">Code</h1>
<p>La bibliothèque standard de Go inclut un <a href="https://pkg.go.dev/archive/zip" title="archive/zip sur Go Packages">module pour lire et écrire des
archives ZIP</a>. Il contient une fonction qui transforme une archive ZIP en une
structure <a href="https://pkg.go.dev/io/fs#FS" title="io/fs.FS sur Go Packages"><code>io/fs.FS</code></a>. Cette dernière peut remplacer
<a href="https://pkg.go.dev/embed#FS" title="embed.FS sur Go Packages"><code>embed.FS</code></a> dans la plupart des cas<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>Pour construire l'archive <code>embed.zip</code>, nous pouvons utiliser une règle dans un
<code>Makefile</code>. Les fichiers à y placer sont spécifiés comme des dépendances pour
s'assurer que les changements sont détectés. La <a href="https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html" title="GNU Make: Automatic Variables">variable automatique</a> <code>$@</code>
est la cible de la règle, tandis que <code>$^</code> est remplacée par la liste des
dépendances, modifiées ou non.</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>

<h1 id="gain-despace">Gain d'espace</h1>
<p><a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a>, un collecteur de flux écrit en Go, embarque plusieurs ressources statiques :</p>
<ul>
<li>des fichiers CSV pour traduire les numéros de ports, les protocoles ou les numéros d'AS ;</li>
<li>du HTML, CSS, JS et des images pour l'interface web ;</li>
<li>la 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="Répartition de l'espace utilisé par chaque paquet avant et après l'introduction de&#10;embed.zip. Il est affiché comme une carte arborescente et nous pouvons voir de nombreux fichiers embarqués&#10;remplacés par un plus gros." 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>Répartition de l'espace utilisé par chaque composant avant (gauche) et après (droite) l'introduction de <code>embed.zip</code>.</figcaption></figure>
<p>L'intégration de ces ressources dans une archive ZIP a réduit la taille de
l'exécutable de plus de 4 Mio :</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="perte-de-performance">Perte de performance</h1>
<p>Lire depuis une archive compressée n'est pas aussi rapide que lire un fichier à
plat. Un <a href="https://github.com/akvorado/akvorado/blob/84bd9a64645667902cb57a5cf5a59749810928cc/common/embed/fs_test.go#L35-L51">benchmark simple</a> montre que c'est plus de 4 fois plus lent. De
plus, la lecture de l'archive alloue de la mémoire<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>Chaque accès à une ressource nécessite une étape de décompression, comme on peut
le voir dans ce graphique :</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">🖼 Graphique en flammes lors de la lecture de données depuis embed.zip comparé à la lecture directe</object></span></div><figcaption>Graphique en flammes du CPU comparant le temps passé sur le CPU lors de la lecture des données depuis embed.zip (à gauche) et lors de la lecture directe (à droite). Parce que Go exécute le benchmark pour les données non compressées 4 fois plus souvent, il utilise le même espace horizontal que le benchmark pour les données compressées. Le graphique est interactif.</figcaption></figure>
<p>Bien qu'une archive ZIP ait un index pour trouver rapidement le fichier demandé,
se déplacer à l'intérieur d'un fichier compressé n'est actuellement pas
possible<sup id="fnref:seek"><a class="footnote-ref" href="#fn:seek">3</a></sup>. Par conséquent, les fichiers retournés depuis une archive
compressée n'implémentent pas les interfaces <code>io.ReaderAt</code> ou <code>io.Seeker</code>,
contrairement aux fichiers directement embarqués. Cela empêche certaines
fonctionnalités du serveur HTTP, comme servir des fichiers partiels ou détecter
le type MIME.</p>
<hr/>
<p>Pour Akvorado, c'est un compromis acceptable pour économiser quelques mébioctets
d'un exécutable de presque 100 Mio. La semaine prochaine, je continuerai cette
aventure futile en expliquant comment j'ai <a href="https://github.com/akvorado/akvorado/pull/2112" title="Don't disable DCE #2112">empêché Go de désactiver
l'élimination de code mort</a> ! 🦥</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:missing">
<p>Elle autorise la lecture de plusieurs fichiers simultanément.
Cependant, elle n'implémente pas les méthodes <code>ReadDir()</code> et <code>ReadFile()</code>. <a class="footnote-backref" href="#fnref:missing" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:improvements">
<p>Il serait possible de conserver en mémoire les ressources
fréquemment consultées. Cela réduirait l'usage du CPU et échangerait la
mémoire cache contre de la mémoire résidente. <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> est un profil qui permet de réaliser un accès aléatoire
rapide dans un fichier compressé. Cependant, le module <code>archive/zip</code> de Go
ne sait pas l'utiliser. <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/fr/blog/2025-akvorado-2.0" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2025-akvorado-2.0#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-09-22T06:12:52Z</updated>
    <id>http://www.luffy.cx/fr/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> est publié aujourd'hui ! <a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> est un collecteur de flux
réseau <abbr title="IP Flow Information Export">IPFIX</abbr> et sFlow. Il enrichit les flux et les stocke dans une base de
données ClickHouse. Les utilisateurs peuvent parcourir les données via une
console web. Cette version introduit un changement architectural important et
d'autres améliorations mineures. Regardons plus en détails ! 🤿</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="#nouveau-service-outlet">Nouveau service « outlet »</a></li>
<li><a href="#autres-changements">Autres changements</a><ul>
<li><a href="#observabilite">Observabilité</a></li>
<li><a href="#documentation">Documentation</a></li>
<li><a href="#integration-continue">Intégration continue</a></li>
<li><a href="#docker">Docker</a></li>
<li><a href="#go">Go</a><ul>
<li><a href="#chaine-de-compilation">Chaîne de compilation</a></li>
<li><a href="#tests">Tests</a></li>
<li><a href="#un-autre-paquet-pour-kafka">Un autre paquet pour Kafka</a></li>
<li><a href="#table-de-routage-amelioree-pour-bmp">Table de routage améliorée pour BMP</a></li>
<li><a href="#divers">Divers</a></li>
</ul>
</li>
<li><a href="#javascript">JavaScript</a></li>
</ul>
</li>
<li><a href="#prochaines-etapes">Prochaines étapes</a></li>
</ul>
</div>
<h1 id="nouveau-service-outlet">Nouveau service « outlet »</h1>
<p>Le changement majeur dans Akvorado 2.0 est la <strong>séparation du service inlet</strong> en
deux parties : l'<strong>inlet</strong> et l'<strong>outlet</strong>. Auparavant, l'inlet gérait tout le
traitement des flux : réception, décodage et enrichissement. Les flux étaient
ensuite envoyés à Kafka pour stockage dans 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="Traitement des flux Akvorado avant le changement : les flux sont reçus et&#10;traités par l'inlet, envoyés à Kafka et stockés dans 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>Traitement des flux Akvorado avant l'introduction du service outlet</figcaption></figure>
<p>Les flux réseau atteignent le service inlet via des chaussettes UDP, un
protocole non fiable. L'inlet doit les traiter <strong>assez rapidement pour éviter de
perdre des paquets</strong>. Pour gérer un grand nombre de flux, l'inlet crée plusieurs
ensembles de workers pour recevoir les flux, récupérer les métadonnées et
assembler les flux enrichis pour Kafka. De nombreuses options de configuration
existaient pour la montée en charge, ce qui augmentait la complexité pour les
utilisateurs. Le code devait <strong>éviter tout blocage</strong>, rendant la chaîne de
traitement complexe et parfois peu fiable, notamment le composant <abbr title="BGP Monitoring Protocol">BMP</abbr><sup id="fnref:bmp"><a class="footnote-ref" href="#fn:bmp">1</a></sup>.
L'ajout de nouvelles fonctionnalités sans aggraver le problème devenait
difficile<sup id="fnref:features"><a class="footnote-ref" href="#fn:features">2</a></sup>.</p>
<p>Dans Akvorado 2.0, l'inlet reçoit les flux et les pousse vers Kafka sans les
décoder. Le nouveau service outlet gère les tâches restantes :</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="Traitement des flux Akvorado après le changement : les flux sont reçus par&#10;l'inlet, envoyés à Kafka, traités par l'outlet et insérés dans 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>Traitement des flux Akvorado après l'introduction du service outlet</figcaption></figure>
<p>Ce changement va au-delà d'un simple découpage<sup id="fnref:size"><a class="footnote-ref" href="#fn:size">3</a></sup> : l'outlet lit maintenant
les flux depuis Kafka et les pousse vers ClickHouse, deux tâches qu'Akvorado ne
gérait pas auparavant. Les flux sont <strong>groupés par lots pour augmenter
l'efficacité</strong> et réduire la charge sur ClickHouse en utilisant <a href="https://github.com/ClickHouse/ch-go/" title="Low-level Go Client for ClickHouse">ch-go</a>, un
client Go bas niveau pour ClickHouse. Quand les lots sont trop petits, des
insertions asynchrones sont utilisées (<a href="https://github.com/akvorado/akvorado/commit/e20645c92e38" title="outlet/metadata: synchronous fetching of metadata">e20645</a>). Le nombre de <strong>workers de
l'outlet s'ajuste dynamiquement</strong> (<a href="https://github.com/akvorado/akvorado/commit/e5a625aecf9e" title="outlet: make the number of Kafka workers dynamic">e5a625</a>) en fonction de la cible sur la
taille des lots et de la latence (50 000 flux et 5 secondes par défaut).</p>
<p>Cette nouvelle architecture nous permet également de <strong>simplifier et optimiser</strong>
le code. L'outlet récupère les métadonnées de façon synchrone (<a href="https://github.com/akvorado/akvorado/commit/e20645c92e38" title="outlet/metadata: synchronous fetching of metadata">e20645</a>). Le
composant <abbr title="BGP Monitoring Protocol">BMP</abbr> devient plus simple en supprimant le multitâche coopératif
(<a href="https://github.com/akvorado/akvorado/commit/3b9486b000c0" title="outlet/routing: remove batching of peer removal">3b9486</a>). Réutiliser le même objet <code>RawFlow</code> pour décoder les flux Protobuf
depuis Kafka réduit la pression sur le ramasse-miettes (<a href="https://github.com/akvorado/akvorado/commit/8b580fd26bfc" title="outlet/core: reuse the same RawFlow object when decoding">8b580f</a>).</p>
<p>L'effet sur les performances globales d'Akvorado était quelque peu incertain,
mais un <a href="https://github.com/akvorado/akvorado/issues/1908#issuecomment-3233143146">utilisateur a indiqué</a> gagner <strong>35 % d'utilisation CPU
en moins</strong> après migration depuis la version précédente, ainsi que la résolution
du problème de longue date du composant <abbr title="BGP Monitoring Protocol">BMP</abbr>. 🥳</p>
<h1 id="autres-changements">Autres changements</h1>
<p>Cette nouvelle version inclut de nombreux changements divers. On y trouve par
exemple la complétion pour les ports sources et destinations (<a href="https://github.com/akvorado/akvorado/commit/f92d2ef9e2bd" title="console/filter: add autocomplete for src and dst ports">f92d2e</a>), et le
redémarrage automatique de l'orchestrateur (<a href="https://github.com/akvorado/akvorado/commit/0f72ffd6761b" title="cmd: automatic restart of orchestrator on configuration change">0f72ff</a>) lors de changements de
configuration pour éviter un piège courant pour les nouveaux utilisateurs.</p>
<p>Concentrons-nous sur quelques domaines clés pour cette version :
<a href="/fr/blog/2025-akvorado-2.0#observabilite">observabilité</a>, <a href="/fr/blog/2025-akvorado-2.0#documentation">documentation</a>,
<a href="/fr/blog/2025-akvorado-2.0#integration-continue"><abbr title="Continuous integration">CI</abbr></a>, <a href="/fr/blog/2025-akvorado-2.0#docker">Docker</a>, <a href="/fr/blog/2025-akvorado-2.0#go">Go</a> et
<a href="/fr/blog/2025-akvorado-2.0#javascript">JavaScript</a>.</p>
<h2 id="observabilite">Observabilité</h2>
<p>Akvorado expose des métriques pour fournir une visibilité sur la chaîne de
traitement et aider à diagnostiquer les problèmes. Elles sont disponibles <strong>au
format Prometheus</strong>, via par exemple <a href="https://demo.akvorado.net/api/v0/inlet/metrics"><code>/api/v0/inlet/metrics</code></a>. Avec
l'introduction de l'outlet, de nombreuses métriques ont été déplacées. Certaines
ont également été renommées (<a href="https://github.com/akvorado/akvorado/commit/4c0b15e1cd4e" title="inlet/outlet: rename a few metrics">4c0b15</a>) pour correspondre aux <a href="https://prometheus.io/docs/practices/naming/">bonnes pratiques
Prometheus</a>. Une métrique pour mesurer le décalage
des consommateurs Kafka a été ajoutée (<a href="https://github.com/akvorado/akvorado/commit/e3a778552d6e" title="outlet/kafka: expose consumer lag as a prometheus metric">e3a778</a>).</p>
<p>Si vous n'avez pas votre propre pile d'observabilité, la configuration Docker
Compose fournie avec Akvorado en propose une. Vous pouvez l'activer avec les
profils introduits à cette fin (<a href="https://github.com/akvorado/akvorado/commit/529a8f69bebc" title="docker: use profiles to optionally enable Prometheus, Loki and Grafana">529a8f</a>).</p>
<p>Le profil <code>prometheus</code> fournit <a href="https://prometheus.io/" title="Prometheus: Open source metrics and monitoring">Prometheus</a> pour <strong>stocker les métriques</strong> et
<a href="https://grafana.com/docs/alloy/latest/" title="Documentation de Grafana Alloy">Alloy</a> pour <strong>les collecter</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> et <a href="https://github.com/akvorado/akvorado/commit/8eb7cd63b1c0" title="docker: make Alloy configuration use only Docker labels">8eb7cd</a>). Les
métriques Redis et Kafka sont collectées via l'exporteur intégré à Alloy
(<a href="https://github.com/akvorado/akvorado/commit/560113e3af24" title="docker: monitor Redis and Kafka using Alloy internal exporter">560113</a>). Les autres métriques sont exposées au format Prometheus et sont
automatiquement récupérées par Alloy à l'aide d'annotations Docker. C'est
similaire à ce qui est fait pour configurer Traefik. <a href="https://github.com/google/cadvisor/" title="Analyzes resource usage and performance characteristics of running containers">cAdvisor</a> a également
été ajouté (<a href="https://github.com/akvorado/akvorado/commit/83d8556d6ddb" title="docker: add cAdvisor for container monitoring">83d855</a>) pour fournir des métriques liées aux conteneurs.</p>
<p>Le profil <code>loki</code> fournit <a href="https://grafana.com/docs/loki/latest/" title="Documentation de Grafana Loki">Loki</a> pour <strong>stocker les logs</strong> (<a href="https://github.com/akvorado/akvorado/commit/45c684dc6737" title="docker: add Loki to the observability stack">45c684</a>). Bien
qu'Alloy puisse collecter et envoyer les logs vers Loki, ses capacités d'analyse
sont limitées : je n'ai pas trouvé de moyen de préserver toutes les métadonnées
associées aux logs structurés produits par de nombreuses applications, y compris
Akvorado. <a href="https://vector.dev/" title="Vector: lightweight, ultra-fast tool for building observability pipelines">Vector</a> remplace Alloy (<a href="https://github.com/akvorado/akvorado/commit/95e2011d0c67" title="docker: replace Alloy by Vector for parsing logs">95e201</a>) et propose un langage, <abbr title="Vector Remap Language">VRL</abbr>,
pour transformer les logs. Malheureusement, il ne peut actuellement <a href="https://github.com/vectordotdev/vector/issues/7358" title="Support checkpointing and reading old logs with docker_logs source">pas
récupérer les logs Docker précédant son démarrage</a>.</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (11.2025)</p>
<p>Alloy peut désormais préserver les champs des logs structurés dans les
métadonnées en utilisant une expression régulière. Voir <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>Enfin, le profil <code>grafana</code> fournit <a href="https://grafana.com/docs/grafana/latest/" title="Documentation de Grafana">Grafana</a>, mais les tableaux de bord
fournis sont pour le moment cassés. Ce sera corrigé dans une prochaine version.</p>
<h2 id="documentation">Documentation</h2>
<p>La configuration Docker Compose fournie par Akvorado facilite la mise en route
rapide. Cependant, Akvorado nécessite quelques étapes obligatoires pour être
fonctionnel. Il est livré avec une <a href="https://demo.akvorado.net/docs">documentation complète</a>, y compris un chapitre sur le <a href="https://demo.akvorado.net/docs/troubleshooting">dépannage des
problèmes</a>. J'espérais que cette documentation
réduirait la charge de support. Il est difficile de savoir si cela fonctionne.
Les utilisateurs satisfaits rapportent rarement leurs succès, tandis qu'une
poignée d'utilisateurs ouvrent des discussions pour demander de l'aide sans
avoir vraiment lu la documentation.</p>
<p>Dans cette version, la <strong>documentation a été considérablement améliorée</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>La documentation a été mise à jour (<a href="https://github.com/akvorado/akvorado/commit/fc1028bfcdc5" title="doc: documentation update for inlet/outlet separation">fc1028</a>) pour correspondre à la nouvelle
architecture d'Akvorado. La section de dépannage a été réécrite (<a href="https://github.com/akvorado/akvorado/commit/17a272d0ba18" title="docs: update troubleshooting documentation">17a272</a>).
Une documentation sur comment améliorer les performances de ClickHouse lors de
la mise à niveau depuis des versions antérieures à 1.10.0 a été ajoutée
(<a href="https://github.com/akvorado/akvorado/commit/5f1e9a49c7d5" title="docs: document schema update for installations before 1.10.0">5f1e9a</a>). Un <abbr title="Large Language Model">LLM</abbr> a relu l'ensemble (<a href="https://github.com/akvorado/akvorado/commit/06e3f334fdb3" title="docs: proofread the whole documentation">06e3f3</a>). La documentation destinée
aux développeurs a également été améliorée (<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> et
<a href="https://github.com/akvorado/akvorado/commit/871fc5981498" title="docs: make it even easier to hack on the console">871fc5</a>).</p>
<p>Du point de vue de l'utilisabilité, les sections de la table des matières
peuvent maintenant se replier (<a href="https://github.com/akvorado/akvorado/commit/c142e58110b3" title="console/frontend: make TOC collapsable in documentation">c142e5</a>). Les avertissements aident à
attirer l'attention des utilisateurs sur les points importants (<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="Avertissement dans la documentation d'Akvorado demandant à un utilisateur de&#10;ne pas ouvrir un ticket ou démarrer une discussion avant d'avoir lu la&#10;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>Exemple d'utilisation des avertissements dans la documentation d'Akvorado</figcaption></figure>
<h2 id="integration-continue">Intégration continue</h2>
<p>Cette version inclut des efforts pour <strong>accélérer</strong> l'intégration continue sur
GitHub. Les tests de couverture et de concurrence s'exécutent en parallèle
(<a href="https://github.com/akvorado/akvorado/commit/6af2163066aa" title="build: speed up CI by running coverage and race tests in parallel">6af216</a> et <a href="https://github.com/akvorado/akvorado/commit/fa9e48d16a90" title="build: run a third job for misc tests">fa9e48</a>). L'image Docker se construit pendant les tests mais
ne reçoit son étiquette qu'après leurs succès (<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="Flux GitHub pour la CI avec de nombreuses tâches, certaines s'exécutant&#10;en parallèle, d'autres non" 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>Flux GitHub pour tester et construire Akvorado</figcaption></figure>
<p>Les tests de bout en bout (<a href="https://github.com/akvorado/akvorado/commit/883e19922eee" title="build: add end-to-end testing">883e19</a>) s'assurent que la configuration Docker
Compose fournie fonctionne comme attendu. <a href="https://hurl.dev/">Hurl</a> exécute des tests sur
diverses URL, notamment pour vérifier les métriques (<a href="https://github.com/akvorado/akvorado/commit/42679b1f2db3" title="build: use hurl for end-to-end tests">42679b</a> et <a href="https://github.com/akvorado/akvorado/commit/169fa933af64" title="build: add more e2e tests by testing Prometheus metrics">169fa9</a>).
Par exemple :</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 est fourni avec une configuration <a href="https://docs.docker.com/compose/" title="Docker Compose documentation">Docker Compose</a> complète pour
aider les utilisateurs à <strong>démarrer rapidement</strong>. Elle assure un déploiement
cohérent, éliminant de nombreux problèmes liés à l'environnement et la
configuration. Elle sert également de documentation.</p>
<p>Cette version apporte quelques petites améliorations autour de Docker :</p>
<ul>
<li>un exemple pour configurer Traefik pour TLS avec <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>la compression HTTP (<a href="https://github.com/akvorado/akvorado/commit/bee9a5de6771" title="docker: enable HTTP compression for Traefik">bee9a5</a>), et</li>
<li>le support d'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>Auparavant, de nombreuses images Docker provenaient de la <a href="https://github.com/bitnami/containers/">bibliothèque de
conteneurs Bitnami</a>. Cependant, Bitnami a été
racheté par VMWare en 2019 et VMWare a été racheté par Broadcom en 2023. En
conséquence, les <a href="https://github.com/bitnami/containers/issues/83267" title="Upcoming changes to the Bitnami catalog (effective August 28th, 2025)">images Bitnami ont été dépréciées</a> en moins d'un mois. Ce n'était pas vraiment une surprise<sup id="fnref:broadcom"><a class="footnote-ref" href="#fn:broadcom">4</a></sup>.
Les versions précédentes d'Akvorado avaient déjà commencé à s'en éloigner. Dans
cette version, l'image Kafka du projet Apache remplace celle de Bitnami
(<a href="https://github.com/akvorado/akvorado/commit/1eb38226fc23" title="docker: switch to Apache Kafka">1eb382</a>). Grâce au passage au mode KRaft, Zookeeper n'est plus nécessaire
(<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> et <a href="https://github.com/akvorado/akvorado/commit/f65d20443622" title="docker: remove use of Zookeeper for Kafka">f65d20</a>).</p>
<p>Les images Docker d'Akvorado étaient auparavant compilées avec Nix. Cependant,
construire des images AArch64 sur x86-64 est lent car cela repose sur
l'émulation de QEMU. Le <code>Dockerfile</code> mis à jour utilise des constructions
<a href="https://docs.docker.com/build/building/multi-stage/" title="Multi-stage builds">multi-étapes</a> et <a href="https://docs.docker.com/build/building/multi-platform/" title="Multi-platform builds">multi-plateformes</a> : une étape
construit la partie JavaScript sur la plateforme hôte, une étape construit la
partie Go en compilation croisée sur la plateforme hôte, et la dernière étape
assemble l'image en se basant sur <a href="https://github.com/GoogleContainerTools/distroless" title="Language focused docker images, minus the operating system">une image légère</a>
(<a href="https://github.com/akvorado/akvorado/commit/268e95ee1891" title="docker: switch away from Nix to build Docker images">268e95</a> et <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"># Ceci est une version simplifiée</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>Lors de la construction pour plusieurs plateformes avec <code>--platform
linux/amd64,linux/arm64,linux/arm/v7</code>, les étapes de construction jusqu'à la
ligne surlignée ne s'exécutent qu'une seule fois pour toutes les plateformes.
Cela accélère considérablement la compilation. 🚅</p>
<p>Akvorado livre maintenant des images Docker pour ces plateformes :
<code>linux/amd64</code>, <code>linux/amd64/v3</code>, <code>linux/arm64</code> et <code>linux/arm/v7</code>. Lors du
téléchargement de <code>ghcr.io/akvorado/akvorado</code>, Docker sélectionne la meilleure
image pour le CPU actuel. Sur x86-64, il y a deux choix. Si votre <a href="https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels" title="X86-64: Microarchitecture levels">CPU est assez
récent</a>, Docker télécharge <code>linux/amd64/v3</code>. Cette version
contient des optimisations supplémentaires et devrait s'exécuter plus rapidement
que la version <code>linux/amd64</code>. Il serait intéressant de livrer une image pour
<code>linux/arm64/v8.2</code>, mais Docker ne supporte pas encore le même mécanisme pour
AArch64 (<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>Cette version inclut de nombreux changements liés à Go mais non visibles pour
les utilisateurs.</p>
<h3 id="chaine-de-compilation">Chaîne de compilation</h3>
<p>Par le passé, Akvorado supportait les deux dernières versions de Go, empêchant
l'utilisation immédiate des dernières améliorations. L'objectif était de
permettre aux utilisateurs de distributions stables d'utiliser les versions de
Go fournies avec leur distribution pour compiler Akvorado. Cependant, cela
pouvait être frustrant quand des fonctionnalités intéressantes, comme <code>go tool</code>,
étaient publiées. Akvorado 2.0 nécessite <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>) mais peut être
compilé avec des chaînes de compilation plus anciennes en téléchargeant
automatiquement une version plus récente (<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>. Les utilisateurs
peuvent toujours modifier <code>GOTOOLCHAIN</code> pour annuler cette décision. La chaîne
de compilation recommandée est mise à jour hebdomadairement via la <abbr title="Continuous integration">CI</abbr> pour
s'assurer d'obtenir la dernière version mineure (<a href="https://github.com/akvorado/akvorado/commit/5b11ecf29e4c" title="build: try updating Go toolchain weekly">5b11ec</a>). Ce changement
simplifie aussi les mises à jour vers des versions plus récentes : seul <code>go.mod</code>
doit être mis à jour.</p>
<p>Grâce à ce changement, Akvorado utilise maintenant <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>) et
j'ai commencé à convertir certains tests unitaires vers le nouveau paquet
<code>test/synctest</code> (<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> et <a href="https://github.com/akvorado/akvorado/commit/159085a0d3ac" title="outlet/metadata: switch to testing/synctest package for clock">159085</a>).</p>
<h3 id="tests">Tests</h3>
<p>Pour <strong>tester l'égalité</strong>, j'utilise une fonction <code>Diff()</code> qui affiche les
différences en cas d'échec :</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>Cette fonction utilise <a href="https://github.com/kylelemons/godebug"><code>kylelemons/godebug</code></a>. Ce paquet
n'est plus maintenu et présente des lacunes : par exemple, par défaut, elle ne
compare pas les champs privés des structures, ce qui peut causer des tests qui
réussissent de façon inattendue. Je l'ai remplacé par
<a href="https://github.com/google/go-cmp"><code>google/go-cmp</code></a>, qui est plus strict et formatte la différence
de manière plus lisible (<a href="https://github.com/akvorado/akvorado/commit/e2f1df9add0d" title="tests: replace godebug by go-cmp for structure diffs">e2f1df</a>).</p>
<h3 id="un-autre-paquet-pour-kafka">Un autre paquet pour Kafka</h3>
<p>Un autre changement est le passage de <a href="https://github.com/IBM/sarama">Sarama</a> à <a href="https://github.com/twmb/franz-go">franz-go</a> pour <strong>interagir
avec Kafka</strong> (<a href="https://github.com/akvorado/akvorado/commit/756e4a8fbd7a" title="/kafka: switch to franz-go">756e4a</a> et <a href="https://github.com/akvorado/akvorado/commit/2d26c5b9cab1" title="inlet/kafka: use kfake for functional tests with Kafka">2d26c5</a>). La principale motivation pour ce
changement est de travailler avec un meilleur modèle de concurrence. Sarama
repose fortement sur les canaux et il est difficile de savoir quand un objet
n'est plus utilisé. franz-go utilise une approche plus moderne<sup id="fnref:channels"><a class="footnote-ref" href="#fn:channels">6</a></sup> et
performante avec des fonctions de rappel. Il est également livré avec un paquet
pour lancer de faux clusters Kafka, ce qui est plus pratique que les fonctions
de simulation fournies par Sarama.</p>
<h3 id="table-de-routage-amelioree-pour-bmp">Table de routage améliorée pour <abbr title="BGP Monitoring Protocol">BMP</abbr></h3>
<p>Pour stocker les routes, le composant <abbr title="BGP Monitoring Protocol">BMP</abbr> utilisait
<a href="https://github.com/kentik/patricia"><code>kentik/patricia</code></a>, une implémentation d'un arbre patricia
axée sur la réduction de la pression sur le ramasse-miettes.
<a href="https://github.com/gaissmai/bart"><code>gaissmai/bart</code></a> est une alternative plus récente utilisant une
adaptation de <a href="https://www.hariguchi.org/art/art.pdf" title="ART: Allotment Routing Table. A Fast Free Multibit Trie Based Routing Table">l'algorithme ART de Donald Knuth</a>
qui promet de <a href="https://github.com/gaissmai/iprbench" title="Benchmarks for Golang IP routing table">meilleures performances</a> et les fournit :
<strong>recherches 90 % plus rapides</strong> et <strong>insertions 27 % plus rapides</strong> (<a href="https://github.com/akvorado/akvorado/commit/92ee2e05b7c4" title="outlet/routing: use gaissmai/bart instead of kentik/patricia">92ee2e</a>
et <a href="https://github.com/akvorado/akvorado/commit/fdb65c93a5fe" title="outlet/routing: store v4 routes into a v4 tree">fdb65c</a>).</p>
<p>Contrairement à <code>kentik/patricia</code>, <code>gaissmai/bart</code> n'aide pas à stocker
efficacement les valeurs attachées à chaque préfixe. J'ai adapté la même
approche utilisée par <code>kentik/patricia</code> pour stocker les listes de routes pour
chaque préfixe : stocker un index 32 bits pour chaque préfixe, et l'utiliser
pour construire un index 64 bits pour rechercher les routes dans un tableau
associatif, qui est une structure particulièrement efficace de Go.</p>
<p><code>gaissmai/bart</code> supporte également une version sans verrous de la table de
routage, mais ce n'est pas aussi simple car nous aurions besoin d'étendre cela
au tableau stockant les routes et au mécanisme d'internement. J'ai également
tenté d'utiliser le nouveau paquet <code>unique</code> de Go pour remplacer le paquet
<code>intern</code> inclus dans Akvorado, mais les performances étaient pires<sup id="fnref:worse"><a class="footnote-ref" href="#fn:worse">7</a></sup>.</p>
<h3 id="divers">Divers</h3>
<p>Les versions précédentes d'Akvorado utilisaient un <a href="/fr/blog/2023-protobuf-dynamique-golang" title="Encodage rapide et dynamique des Protocol Buffers en Go">encodeur Protobuf
personnalisé</a> pour les performances et la flexibilité.
Avec l'introduction du service outlet, Akvorado n'a besoin que d'un schéma
statique simple, donc ce code a été supprimé. Cependant, il est possible
d'améliorer les performances avec
<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> et <a href="https://github.com/akvorado/akvorado/commit/8b580fd26bfc" title="outlet/core: reuse the same RawFlow object when decoding">8b580f</a>).
De plus, la dépendance à <code>protoc</code>, un programme C++, était quelque peu
ennuyeuse. Par conséquent, Akvorado utilise maintenant <a href="https://buf.build/">buf</a>, écrit en Go,
pour convertir un schéma Protobuf en code Go (<a href="https://github.com/akvorado/akvorado/commit/f4c879e525a5" title="build: switch from protoc to buf to build protobuf">f4c879</a>).</p>
<p>Une autre petite optimisation pour <strong>réduire la taille du binaire d'Akvorado</strong>
de 10 MB a consisté à compresser les ressources statiques intégrées dans
Akvorado dans un fichier ZIP. Cela inclut la base des numéros d'AS, ainsi que
les images SVG pour la documentation. Un bout de code rend ce changement
transparent (<a href="https://github.com/akvorado/akvorado/commit/b1d6382585dd" title="common/embed: replace all go:embed use by an embedded archive">b1d638</a> et <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>Récemment, deux grandes <strong>attaques de la chaîne d'approvisionnement</strong> ont frappé
l'écosystème JavaScript : <a href="https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised" title="npm debug and chalk packages compromised">une affectant les paquets populaires <code>chalk</code> et
<code>debug</code></a> et <a href="https://sigh.dev/posts/ctrl-tinycolor-post-mortem/" title="@ctrl/tinycolor Supply Chain Attack Post-mortem">une autre impactant le paquet populaire
<code>@ctrl/tinycolor</code></a>. Ces attaques existent aussi dans d'autres
écosystèmes, mais JavaScript est une cible privilégiée en raison de l'usage
intensif de petites dépendances. La version précédente d'Akvorado reposait sur
<strong>653 dépendances</strong>.</p>
<p><code>npm-run-all</code> a été supprimé (<a href="https://github.com/akvorado/akvorado/commit/3424e8b8c9f2" title="console/frontend: don't use npm-run-all">3424e8</a>, 132 dépendances). <code>patch-package</code> a
été supprimé (<a href="https://github.com/akvorado/akvorado/commit/6258050251a3" title="console/frontend: remove patch for sugar-date">625805</a> et <a href="https://github.com/akvorado/akvorado/commit/e85ff0ae31fa" title="console/frontend: remove patch for echarts">e85ff0</a>, 69 dépendances) en déplaçant les
définitions TypeScript manquantes vers <code>env.d.ts</code>. <code>eslint</code> a été remplacé par
<a href="https://oxc.rs/docs/guide/usage/linter">oxlint</a>, un linter écrit en Rust (<a href="https://github.com/akvorado/akvorado/commit/97fd8ca2e479" title="console/frontend: replace eslint by oxlint">97fd8c</a>, 125 dépendances, plugins
inclus).</p>
<p>J'ai basculé de <code>npm</code> vers <a href="https://pnpm.io/" title="Fast, disk space efficient package manager">Pnpm</a>, un gestionnaire de paquets alternatif
(<a href="https://github.com/akvorado/akvorado/commit/fce383dbf46b" title="build: switch to pnpm">fce383</a>). Pnpm n'exécute pas les scripts d'installation par défaut<sup id="fnref:install"><a class="footnote-ref" href="#fn:install">8</a></sup>
et empêche l'installation de paquets trop récents. Il est également
significativement plus rapide<sup id="fnref:fast"><a class="footnote-ref" href="#fn:fast">9</a></sup>. Contrairement à <code>npm</code>, il n'est pas livré
avec Node.js. Cependant, Node.js contient <a href="https://github.com/nodejs/corepack" title="Zero-runtime-dependency package acting as bridge between Node projects and their package managers">Corepack</a>, qui nous permet
d'utiliser Pnpm sans l'installer. Pnpm peut également lister les licences
utilisées par chaque dépendance, supprimant le besoin d'utiliser
<code>license-compliance</code> (<a href="https://github.com/akvorado/akvorado/commit/a35ca8bc07e0" title="build: replace license-compliance with pnpm">a35ca8</a>, 42 dépendances).</p>
<p>Pour des améliorations de vitesse supplémentaires, au-delà du passage à Pnpm et
Oxlint, <a href="https://vite.dev/">Vite</a> a été remplacé par sa <a href="https://voidzero.dev/posts/announcing-rolldown-vite" title="Announcing Rolldown-Vite">version Rolldown</a> plus rapide
(<a href="https://github.com/akvorado/akvorado/commit/4638272b7b6d" title="console/frontend: switch to rolldown-vite">463827</a>).</p>
<p>Après ces changements, Akvorado ne nécessite plus « que » 225 dépendances. 😱</p>
<h1 id="prochaines-etapes">Prochaines étapes</h1>
<p>J'aimerais intégrer trois fonctionnalités dans la prochaine version
d'Akvorado :</p>
<ul>
<li>
<p>Ajouter des tableaux de bord Grafana pour compléter la pile d'observabilité.
  Voir le <a href="https://github.com/akvorado/akvorado/issues/1906" title="Ship some Grafana dashboards in docker compose setup">ticket #1906</a> pour les détails.</p>
</li>
<li>
<p>Intégrer le <a href="https://github.com/ovh/grafana-akvorado">plugin Grafana d'OVH</a> en fournissant une
  API stable pour de telles intégrations. La console web d'Akvorado resterait
  utile pour parcourir les résultats, mais si vous voulez créer et partager des
  tableaux de bord, vous devrez passer à Grafana. Voir le <a href="https://github.com/akvorado/akvorado/issues/1895" title="Integration of OVH Grafana plugin">ticket #1895</a>.</p>
</li>
<li>
<p>Déplacer une partie du travail actuellement fait dans ClickHouse
  (dictionnaires personnalisés, GeoIP et ajout d'informations liées aux IP) vers
  le service outlet. Cela devrait donner plus de flexibilité pour ajouter des
  fonctionnalités comme celle demandée dans le <a href="https://github.com/akvorado/akvorado/issues/1030" title="RegExp for custom dimension">ticket #1030</a>. Voir le <a href="https://github.com/akvorado/akvorado/issues/2006" title="Move back GeoIP and custom dictionaries into outlet">ticket #2006</a>.</p>
</li>
</ul>
<hr/>
<p>J'ai commencé à travailler sur la division de l'inlet il y a plus d'un an. J'ai
retrouvé de la motivation ces derniers mois, en partie grâce à <a href="https://code.claude.com/docs/en/overview" title="Claude Code overview">Claude Code</a>,
que j'ai utilisé comme <a href="https://fr.wikipedia.org/wiki/M%C3%A9thode_du_canard_en_plastique" title="Méthode du canard en plastique">canard en plastique</a>. Presque aucun code
produit n'a été conservé<sup id="fnref:exceptions"><a class="footnote-ref" href="#fn:exceptions">10</a></sup> : c'est comme un stagiaire qui n'apprend
pas. D'ailleurs, il est aussi flemmard. Pour la traduction en français, il a
fallu insister de nombreuses fois pour traduire l'intégralité de l'article. 🦆</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:bmp">
<p>De nombreuses tentatives ont été faites pour rendre le composant <abbr title="BGP Monitoring Protocol">BMP</abbr> à
la fois performant et non bloquant. Voir par exemple <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> et <a href="https://github.com/akvorado/akvorado/pull/278" title="inlet/bmp: new lockless design">PR #278</a>. Malgré ces efforts, ce composant restait
problématique pour la plupart des utilisateurs. Voir le 
<a href="https://github.com/akvorado/akvorado/issues/1461" title="BMP crash: cannot read BMP body">ticket #1461</a> comme exemple. <a class="footnote-backref" href="#fnref:bmp" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:features">
<p>Certaines fonctionnalités ont été déplacées vers ClickHouse pour
éviter le coût de traitement dans l'inlet. Voir par exemple <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>C'est le <a href="https://github.com/akvorado/akvorado/commit/ac68c5970e2c" title="inlet: split inlet into new inlet and outlet">plus gros changement</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 est connu pour ses actions hostiles aux utilisateurs.
Regardez ce qui s'est passé avec 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>En tant que développeur Debian, je n'aime pas ces mécanismes qui
contournent le gestionnaire de paquets de la distribution. J'ai finalement
changé d'avis quand Go 1.25 a passé un mois dans la <a href="https://ftp-master.debian.org/new.html">file NEW de
Debian</a>, un mécanisme arbitraire que je n'aime pas du
tout. <a class="footnote-backref" href="#fnref:debian" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:channels">
<p>Aux débuts de Go, l'utilisation des canaux était fortement
encouragée. Sarama a été conçu pendant cette période. Quelques années plus
tard, une approche plus nuancée a émergé. Voir notamment « <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>Cela devrait être étudié davantage, mais ma théorie est que le
paquet <code>intern</code> utilise des entiers 32 bits, tandis que <code>unique</code> utilise des
pointeurs 64 bits. Voir le 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>C'est aussi possible avec <code>npm</code>. Voir le 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>Une alternative encore plus rapide est <a href="https://bun.sh/" title="Fast all-in-one JavaScript runtime">Bun</a>, mais elle est moins
disponible. <a class="footnote-backref" href="#fnref:fast" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:exceptions">
<p>Les exceptions sont une partie du code pour les avertissements,
le code pour replier la table des matières et une partie de la
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">Langage inspiré de SQL pour filtrer les flux</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2023-langage-sql-filtrage" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2023-langage-sql-filtrage#isso-thread" rel="replies" type="text/html"/>
    <updated>2023-02-13T07:06:06Z</updated>
    <id>http://www.luffy.cx/fr/blog/2023-langage-sql-filtrage.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="/fr/blog/2022-akvorado-collecteur-flux" title="Akvorado : collecteur et visualisateur de flux réseau">Akvorado</a> collecte les flux réseau à l'aide d'<a href="https://www.rfc-editor.org/info/rfc7011" title="RFC 7011: Specification of the IP Flow Information Export (IPFIX) Protocol for the Exchange of Flow Information">IPFIX</a> ou de <a href="https://www.rfc-editor.org/info/rfc3176" title="RFC 3176: InMon Corporation's sFlow: A Method for Monitoring Traffic in Switched and Routed Networks">sFlow</a>. Il
les stocke dans une base de données <a href="https://clickhouse.com/" title="ClickHouse: OLAP DBMS">ClickHouse</a>. Une console web permet à
l'utilisateur de faire des requêtes sur les données pour obtenir des graphiques.
Un aspect intéressant de cette console est la possibilité de filtrer les flux
avec un langage inspiré de SQL :</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>Éditeur de filtre dans la console d'Akvorado</figcaption></figure>
<p>Souvent, les interfaces web exposent un <a href="https://dabernathy89.github.io/vue-query-builder/" title="Vue Query Builder">constructeur</a> <a href="https://react-querybuilder.js.org/" title="React Query Builder">de
requêtes</a> pour concevoir de tels filtres. À la place, j'ai
choisi de combiner un langage similaire à SQL avec un éditeur prenant en charge
la <strong>complétion</strong>, la <strong>coloration syntaxique</strong> et la <strong>vérification
syntaxique</strong><sup id="fnref:lazy"><a class="footnote-ref" href="#fn:lazy">1</a></sup>.</p>
<div class="toc">
<ul>
<li><a href="#analyseur-syntaxique">Analyseur syntaxique</a></li>
<li><a href="#integration-dans-codemirror">Intégration dans CodeMirror</a><ul>
<li><a href="#coloration-syntaxique">Coloration syntaxique</a></li>
<li><a href="#verification-syntaxique">Vérification syntaxique</a></li>
<li><a href="#completion">Complétion</a></li>
</ul>
</li>
</ul>
</div>
<p>L'analyseur syntaxique du langage est construit avec <a href="https://github.com/mna/pigeon" title="Command pigeon generates parsers in Go from a PEG grammar">pigeon</a> (<em>Go</em>) à partir
d'une <a href="https://en.wikipedia.org/wiki/Parsing_expression_grammar" title="Parsing expression grammar on Wikipedia">grammaire d'expression d'analyse syntaxique</a>
(<em>parsing expression grammar</em> ou <abbr title="Parsing Expression Grammar">PEG</abbr>). Le composant à la base de l'éditeur est
<a href="https://codemirror.net/" title="CodeMirror: Extensible Code Editor">CodeMirror</a> (<em>TypeScript</em>).</p>
<h1 id="analyseur-syntaxique">Analyseur syntaxique</h1>
<p>Les grammaires <abbr title="Parsing Expression Grammar">PEG</abbr> sont relativement récentes<sup id="fnref:recent"><a class="footnote-ref" href="#fn:recent">2</a></sup> et sont une alternative
aux grammaires contextuelles. Elles sont plus faciles à écrire et peuvent
générer de meilleurs messages d'erreur. Par exemple, Python a <a href="https://peps.python.org/pep-0617/" title="PEP 617 – New PEG parser for CPython">basculé d'un
analyseur de type <abbr title="left-to-right, leftmost derivation">LL</abbr>(1) à un analyseur basé sur une grammaire <abbr title="Parsing Expression Grammar">PEG</abbr></a>
depuis 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> génère un analyseur pour Go. Une grammaire est un ensemble de règles.
Chaque règle est un identifiant, avec optionnellement une étiquette utilisée pour les messages
d'erreur, une expression et une action en Go à exécuter. Vous pouvez trouver la
grammaire complète dans <a href="https://github.com/akvorado/akvorado/blob/main/console/filter/parser.peg"><code>parser.peg</code></a>. Voici une règle simplifiée :</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>L'identifiant de la règle est <code>ConditionIPExpr</code>. Elle attend soit
<code>ExporterAddress</code>, soit <code>SrcAddr</code>, soit <code>DstAddr</code>, sans distinction de la case.
L'action pour chaque cas renvoie le nom de la colonne correspondante. C'est ce
qui est stocké dans la variable <code>column</code>. Ensuite, elle attend un des deux
opérateurs possibles. Comme il n'y a pas de bloc de code, l'opérateur est stocké
dans la variable <code>operator</code>. Ensuite, elle attend une chaîne validée par la
règle <code>IP</code> qui est définie ailleurs dans la grammaire. Si c'est le cas, elle
stocke le résultat dans la variable <code>ip</code> et exécute l'action finale. L'action
transforme la colonne, l'opérateur et l'adresse IP en une expression SQL pour
<em>ClickHouse</em>. Par exemple, si nous avons <code>ExporterAddress = 203.0.113.15</code>, nous
obtenons <code>ExporterAddress = IPv6StringToNum('203.0.113.15')</code>.</p>
<p>La règle <code>IP</code> utilise une expression régulière rudimentaire mais vérifie si
l'adresse correspondante est correcte dans le bloc d'action, grâce à
<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>Cet analyseur transforme de manière sécurisée un filtre en une clause <code>WHERE</code>
acceptée par <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-dans-codemirror">Intégration dans CodeMirror</h1>
<p><a href="https://codemirror.net/" title="CodeMirror: Extensible Code Editor">CodeMirror</a> est un éditeur de code polyvalent qui peut être facilement
intégré dans les projets JavaScript. Dans <em>Akvorado</em>, le composant
<a href="https://vuejs.org/" title="Vue.js: the Progressive JavaScript Framework">Vue.js</a> <a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/components/InputFilter.vue"><code>InputFilter</code></a> utilise <em>CodeMirror</em> et tire
parti de fonctionnalités telles que la coloration syntaxique, la vérification
syntaxique et la complétion. Le code source de ces fonctionnalités se trouve
dans le répertoire <a href="https://github.com/akvorado/akvorado/tree/main/console/frontend/src/codemirror/lang-filter"><code>codemirror/lang-filter/</code></a>.</p>
<h2 id="coloration-syntaxique">Coloration syntaxique</h2>
<p>La grammaire <abbr title="Parsing Expression Grammar">PEG</abbr> pour Go ne peut pas être utilisé directement<sup id="fnref:pegjs"><a class="footnote-ref" href="#fn:pegjs">4</a></sup> et les
exigences pour les analyseurs syntaxiques utilisés dans les éditeurs sont
différentes : ils doivent être tolérants aux erreurs et fonctionner de manière
incrémentielle, car le code est généralement mis à jour caractère par caractère.
<em>CodeMirror</em> propose une solution via son propre générateur d'analyseur, <a href="https://lezer.codemirror.net/" title="The Lezer Parser System">Lezer</a>.</p>
<p>Nous n'avons pas besoin que cet analyseur supplémentaire comprenne pleinement le
langage des filtres. Seule la structure est nécessaire : les noms de colonnes, les
opérateurs de comparaison et de logique, les valeurs entre guillemets ou non. La
<a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/codemirror/lang-filter/syntax.grammar">grammaire</a> est donc assez courte et n'a pas besoin d'être mise à jour
souvent :</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>L'expression <code>SrcAS = 12322 AND (DstAS = 1299 OR SrcAS = 29447)</code> est analysée ainsi :</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>La dernière étape est d'indiquer à <em>CodeMirror</em> la correspondance entre chaque
symbole et sa catégorie pour la coloration syntaxique :</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="verification-syntaxique">Vérification syntaxique</h2>
<p>La vérification syntaxique est déléguée à l'analyseur syntaxique en Go. Le point
d'accès <code>/api/v0/console/filter/validate</code> accepte un filtre et retourne une
structure JSON avec les éventuelles erreurs:</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>Le <a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/codemirror/lang-filter/linter.ts">greffon</a> pour <em>CodeMirror</em> interroge cette API et transforme
chaque erreur en un <a href="https://codemirror.net/docs/ref/#lint.Diagnostic">diagnostic</a>.</p>
<h2 id="completion">Complétion</h2>
<p>Le système de complétion adopte une approche hybride. Il répartit le travail
entre le frontend et le backend pour offrir ses suggestions.</p>
<p>Le <a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/codemirror/lang-filter/complete.ts">frontend</a> utilise l'analyseur construit avec <em>Lezer</em> pour déterminer le
contexte de la complétion : s'agit-il de compléter un nom de colonne, un
opérateur ou une valeur ? Il extrait également le nom de la colonne si nous
complétons autre chose. Il transfère le résultat au backend via le point de
terminaison <code>/api/v0/console/filter/complete</code>. Parcourir l'arbre syntaxique n'a
pas été aussi facile que je le pensais, mais les <a href="https://github.com/akvorado/akvorado/blob/main/console/frontend/src/codemirror/lang-filter/complete.test.ts">tests unitaires</a>
ont beaucoup aidé.</p>
<p>Le <a href="https://github.com/akvorado/akvorado/blob/9eee46caded6fa6cc2dfb90d1941b2ef05d15e9f/console/filter.go#L76">backend</a> utilise l'analyseur généré par <em>pigeon</em> pour compléter les noms
de colonnes et les opérateurs de comparaison. Pour les valeurs, les complétions
sont statiques ou extraites de la base de données <em>ClickHouse</em>. Un utilisateur
peut compléter un numéro <abbr title="Autonomous System">AS</abbr> à partir d'un nom d'organisation grâce au code
suivant :</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>À mon avis, ce système de complétion est un élément important qui fait de
l'éditeur un moyen efficace de sélectionner des flux. Alors qu'un constructeur
de requêtes aurait pu être plus convivial pour les débutants, la facilité
d'utilisation et les fonctionnalités du système de complétion le rendent plus
agréable à utiliser une fois l'utilisateur familiarisé.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:lazy">
<p>De plus, créer un constructeur de requêtes me semblait une tâche assez
rébarbative. <a class="footnote-backref" href="#fnref:lazy" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:recent">
<p>Elles ont été introduites en 2004 dans « <a href="https://bford.info/pub/lang/peg.pdf">Parsing Expression
Grammars: A Recognition-Based Syntactic Foundation</a> ». Les
analyseurs <abbr title="left-to-right">LR</abbr> ont été introduits en <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>, les analyseurs <abbr title="lookahead left-to-right">LALR</abbr>
en 1969 et les analyseurs <abbr title="left-to-right, leftmost derivation">LL</abbr> dans les années 1970. <a href="https://fr.wikipedia.org/wiki/Yacc_(logiciel)" title="Article sur Yacc sur Wikipedia">Yacc</a>, un générateur
d'analyseurs populaire, a été écrit en 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>L'analyseur retourne une chaîne. Il ne génère pas un arbre syntaxique
intermédiaire. Cela le rend plus simple et suffit à nos besoins. <a class="footnote-backref" href="#fnref:ast" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:pegjs">
<p>Elle pourrait être manuellement traduite en JavaScript avec <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>