<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>Mauro Pinto - go</title>
      <link>https://pintomau.dev</link>
      <description>Software and stuff</description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://pintomau.dev/tags/go/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Sun, 19 Jul 2026 00:00:00 +0000</lastBuildDate>
      <item>
          <title>A Benchmark Smelled Funny</title>
          <pubDate>Sun, 19 Jul 2026 00:00:00 +0000</pubDate>
          <author>Mauro Pinto</author>
          <link>https://pintomau.dev/blog/a-benchmark-smelled-funny/</link>
          <guid>https://pintomau.dev/blog/a-benchmark-smelled-funny/</guid>
          <description xml:base="https://pintomau.dev/blog/a-benchmark-smelled-funny/">&lt;p&gt;I&#x27;ve been building &lt;a class=&quot;external&quot; rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pintomau&#x2F;go-spmc-ring&quot;&gt;go-spmc-ring&lt;&#x2F;a&gt;, a single-writer, multiple-reader ring
buffer in Go. The whole point of the project is mechanical sympathy: every design decision is supposed to have a
benchmark behind it. I came across a benchmark that I didn&#x27;t quite understand, and went digging deeper.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-accusation&quot;&gt;The accusation&lt;a class=&quot;post-anchor&quot; href=&quot;#the-accusation&quot; aria-label=&quot;Anchor link for: the-accusation&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The ring buffer has two ways to publish an event. You can hand it a value:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;rb.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;Publish&lt;&#x2F;span&gt;&lt;span&gt;(event)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Or you can hand it a callback that fills the destination slot in place:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;rb.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;PublishFunc&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;func&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;object&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    slot.x[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;})&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;My payload is &lt;code&gt;object{ x [64]byte }&lt;&#x2F;code&gt;, which is exactly one cache line. And my benchmark table said this:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Benchmark&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;ns&#x2F;op&lt;&#x2F;th&gt;&lt;th&gt;Notes&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Publish&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;5.19&lt;&#x2F;td&gt;&lt;td&gt;callback API, keep-up reader&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Publish_Direct&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;9.21&lt;&#x2F;td&gt;&lt;td&gt;constructs and publishes a value&#x2F;event&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;There it is. Publishing by value is &lt;strong&gt;1.77x slower&lt;&#x2F;strong&gt;. The obvious lesson writes itself: &lt;em&gt;don&#x27;t pass structs by value in
a hot loop, use the callback.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Something nagged, though. 1.77x is a &lt;em&gt;lot&lt;&#x2F;em&gt; for one struct copy. 64 bytes is four 16-byte SSE moves, or one AVX-512 store
on a good day. It should be nearly free. I decided to take it apart.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;four-benchmarks-one-variable-at-a-time&quot;&gt;Four benchmarks, one variable at a time&lt;a class=&quot;post-anchor&quot; href=&quot;#four-benchmarks-one-variable-at-a-time&quot; aria-label=&quot;Anchor link for: four-benchmarks-one-variable-at-a-time&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Instead of trusting one row that changes several things at once, I wrote four benchmarks that each change exactly one
thing. (They live in a &lt;a class=&quot;external&quot; rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pintomau&#x2F;store-forwarding-demo&quot;&gt;tiny standalone repo&lt;&#x2F;a&gt; so you can run them
yourself. No ring buffer required.) Every one writes a &lt;em&gt;different&lt;&#x2F;em&gt; value each iteration, so none of them can be
waved away as a constant the compiler folded flat:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;&#x2F;&#x2F; build a fresh value every iteration, then publish by value&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;func&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt; BenchmarkConstructed&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;testing&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;B&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    var&lt;&#x2F;span&gt;&lt;span&gt; seq&lt;&#x2F;span&gt;&lt;span style=&quot;color: #66D9EF;font-style: italic;&quot;&gt; byte&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    for&lt;&#x2F;span&gt;&lt;span&gt; b.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;Loop&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        o&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; :=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;object&lt;&#x2F;span&gt;&lt;span&gt;{}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        o.x[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; seq&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;        publishByValue&lt;&#x2F;span&gt;&lt;span&gt;(o)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        seq&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;++&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;&#x2F;&#x2F; publish by value from buffers built AHEAD of the loop&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;func&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt; BenchmarkPrepared&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;testing&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;B&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    var&lt;&#x2F;span&gt;&lt;span&gt; bufs [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;object&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    for&lt;&#x2F;span&gt;&lt;span&gt; i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; := range&lt;&#x2F;span&gt;&lt;span&gt; bufs {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        bufs[i].x[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #66D9EF;font-style: italic;&quot;&gt; byte&lt;&#x2F;span&gt;&lt;span&gt;(i)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    var&lt;&#x2F;span&gt;&lt;span&gt; i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #66D9EF;font-style: italic;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    for&lt;&#x2F;span&gt;&lt;span&gt; b.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;Loop&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;        publishByValue&lt;&#x2F;span&gt;&lt;span&gt;(bufs[i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;3&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt; &#x2F;&#x2F; value changes, but it was built long ago&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;++&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;&#x2F;&#x2F; fill all 64 bytes of the slot in place, via callback&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;func&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt; BenchmarkFullWrite&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;testing&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;B&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt; &#x2F;* ... *&#x2F;&lt;&#x2F;span&gt;&lt;span&gt; }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;&#x2F;&#x2F; write a single changing byte into the slot, via callback&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;func&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt; BenchmarkOneByteWrite&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;testing&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;B&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt; &#x2F;* ... *&#x2F;&lt;&#x2F;span&gt;&lt;span&gt; }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Stripped of the ring buffer&#x27;s own overhead, the gap is stark. On my Ryzen 5 9600X:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;BenchmarkConstructed-12     5.51 ns&#x2F;op&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;BenchmarkPrepared-12        1.69 ns&#x2F;op&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;BenchmarkFullWrite-12       1.61 ns&#x2F;op&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;BenchmarkOneByteWrite-12    1.53 ns&#x2F;op&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Read that table and the story flips completely.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Payload size is free.&lt;&#x2F;strong&gt; &lt;code&gt;FullWrite&lt;&#x2F;code&gt; fills all 64 bytes; &lt;code&gt;OneByteWrite&lt;&#x2F;code&gt; writes one. They&#x27;re basically the same
(1.61 vs 1.53 ns). Writing the whole cache line costs almost nothing over writing a single byte, because what
you&#x27;re really paying for is the cache line changing hands, not the bytes going into it. So the gap was never
&quot;we copy 64 bytes.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The by-value API is cheap too.&lt;&#x2F;strong&gt; &lt;code&gt;Prepared&lt;&#x2F;code&gt; passes a 64-byte struct by value, exactly like &lt;code&gt;Constructed&lt;&#x2F;code&gt; does
(and it runs at 1.69 ns), right down near the floor. Passing a struct by value, when the value was built
ahead of time, is not the villain either.&lt;&#x2F;p&gt;
&lt;p&gt;So what is &lt;code&gt;Constructed&lt;&#x2F;code&gt; doing that the others aren&#x27;t? Only one thing: it builds the value &lt;em&gt;in place, right
before the call&lt;&#x2F;em&gt;. That construction (&lt;code&gt;o := object{}; o.x[0] = seq&lt;&#x2F;code&gt;), is the entire difference between 1.69 ns
and 5.51 ns.&lt;&#x2F;p&gt;
&lt;p&gt;The original &quot;1.77x API penalty&quot; was a lie of composition. The honest by-value cost is a rounding error. Everything
else was the caller&#x27;s construction pattern, and the benchmark had quietly folded the two together.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;so-why-does-building-it-in-place-cost-3x&quot;&gt;So why does building it &lt;em&gt;in place&lt;&#x2F;em&gt; cost 3x?&lt;a class=&quot;post-anchor&quot; href=&quot;#so-why-does-building-it-in-place-cost-3x&quot; aria-label=&quot;Anchor link for: so-why-does-building-it-in-place-cost-3x&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This is where you have to stop reading Go and start reading assembly. &lt;code&gt;go test -gcflags=-S&lt;&#x2F;code&gt; on &lt;code&gt;Constructed&lt;&#x2F;code&gt; shows
exactly what the CPU is being asked to do:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;asm&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; o := object{}; o.x[0] = seq&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; X15, (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;CX&lt;&#x2F;span&gt;&lt;span&gt;)        &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; 16-byte zeroing store&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; X15, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;16&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;CX&lt;&#x2F;span&gt;&lt;span&gt;)      &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; 16-byte zeroing store&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; X15, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;32&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;CX&lt;&#x2F;span&gt;&lt;span&gt;)      &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; 16-byte zeroing store&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; X15, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;48&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;CX&lt;&#x2F;span&gt;&lt;span&gt;)      &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; 16-byte zeroing store&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MOVB   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;DL&lt;&#x2F;span&gt;&lt;span&gt;, o+&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;80&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;SP&lt;&#x2F;span&gt;&lt;span&gt;)     &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; 1-byte store        &amp;lt;- note the size&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; publishByValue(o): copy the argument out&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;CX&lt;&#x2F;span&gt;&lt;span&gt;), X14        &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; 16-byte LOAD over the bytes just written&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; X14, (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;DX&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Zeroing the struct is four 16-byte stores. Setting the first byte is one 1-byte store. Then, to pass &lt;code&gt;o&lt;&#x2F;code&gt; by value,
the argument copy immediately &lt;em&gt;reads those bytes back&lt;&#x2F;em&gt;, starting with a 16-byte load that overlaps the first
16-byte zeroing store &lt;strong&gt;and&lt;&#x2F;strong&gt; the 1-byte store.&lt;&#x2F;p&gt;
&lt;p&gt;And this is the crux.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stores don&#x27;t go to the cache. Not at first&lt;&#x2F;strong&gt;: When the CPU executes a store, it does not write to L1. It appends
an entry to a small hardware queue called the store buffer (~64 entries on Zen): &quot;address X, N bytes, this data.&quot;
The entry sits there until the instruction retires, and only then drains to L1, quietly, in the background. Why?
Speculation. The CPU executes ahead of branches it has guessed. If the guess was wrong, it must throw work away,
and you can&#x27;t &quot;unwrite&quot; the cache. The store buffer is the holding pen for writes that aren&#x27;t yet official.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;That creates a problem for every load&lt;&#x2F;strong&gt;: If pending writes live in the store buffer and not in the cache,
then the cache is stale by definition. So every load must check: &quot;is there a store buffer entry for my
address, newer than me?&quot; If yes, reading L1 would return old data.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The fast path: forwarding&lt;&#x2F;strong&gt;: If a single pending store entry fully contains the load&#x27;s bytes, the hardware
just copies the data straight out of that entry into the load. No cache involved, done in a few cycles. This
is store-to-load forwarding, and it&#x27;s why x = 5; y = x costs nothing. AMD&#x27;s own
&lt;a class=&quot;external&quot; rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.amd.com&#x2F;v&#x2F;u&#x2F;en-US&#x2F;58455_1.00&quot;&gt;optimization guide&lt;&#x2F;a&gt; states the covering rule plainly: the
load-store unit forwards &quot;when there is an older store that contains &lt;strong&gt;all of the load&#x27;s bytes&lt;&#x2F;strong&gt;&quot;
(Software Optimization Guide for the AMD Zen5 Microarchitecture, #58455, ch. 2, Load-Store Unit).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The slow path: our stall&lt;&#x2F;strong&gt;: Now the crux. Each store buffer entry is one packet: (addr, size, data). The
forwarding hardware is a matcher, not a merger: it can grab data from one entry, but it has no ALU to
stitch bytes from entry #1 together with bytes from entry #5, in age order, in the load&#x27;s shadow.&lt;br &#x2F;&gt;
&lt;br &#x2F;&gt;
So when our 16-byte load arrives and finds byte 0&#x27;s newest value in the MOVB entry and bytes 1–15&#x27;s newest value
in the older MOVUPS entry, the matcher shrugs: no single entry covers me. The only correct fallback is to make
the cache authoritative again: wait for those entries to retire and drain to L1, then read L1 normally. That
wait, pipeline twiddling its thumbs while stores age out, is the 15–20 cycles
(&lt;a class=&quot;external&quot; rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.agner.org&#x2F;optimize&#x2F;microarchitecture.pdf&quot;&gt;Agner Fog&lt;&#x2F;a&gt; measures ~12 for the partial-overlap case
on Zen 5, §25.17 of the microarchitecture manual; the end-to-end gap here works out to ~20 cycles per iteration).&lt;br &#x2F;&gt;
&lt;br &#x2F;&gt;
That&#x27;s the whole thing. Not a cache miss, not memory bandwidth. A pattern-matching limitation in a queue-lookup
circuit, and the penalty is &quot;wait until the queue drains.&quot; (For a perf-counter walkthrough of the same
narrow-write-then-wide-read shape on Intel, see
&lt;a class=&quot;external&quot; rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;easyperf.net&#x2F;blog&#x2F;2018&#x2F;03&#x2F;09&#x2F;Store-forwarding&quot;&gt;Store forwarding by example&lt;&#x2F;a&gt; on Easyperf.)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Here&#x27;s the collision, drawn out. Building the value leaves several stores sitting in the store buffer, all still
in flight (newest at the bottom, in program order):&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  store buffer (pending writes to o on the stack)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ┌────────────────────────────────────────────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  │  MOVUPS X15  -&amp;gt;  bytes [ 0 .. 15]  (16 bytes)  │  ◄ zeroes byte 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  │  MOVUPS X15  -&amp;gt;  bytes [16 .. 31]  (16 bytes)  │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  │  MOVUPS X15  -&amp;gt;  bytes [32 .. 47]  (16 bytes)  │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  │  MOVUPS X15  -&amp;gt;  bytes [48 .. 63]  (16 bytes)  │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  │  MOVB   seq  -&amp;gt;  byte [0]          (1 byte)    │  ◄ overwrites byte 0 (newest)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  └────────────────────────────────────────────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Then the argument copy immediately issues:  LOAD bytes [0 .. 15].&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Draw every pending store and the load as spans over the same byte axis:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           byte offset within o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           0               15 16          31 32   ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           |----------------|----------------|--------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  pending  ################                     MOVUPS zero [ 0..15]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  stores                    ################     MOVUPS zero [16..31]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                          ...    (two more, [32..63])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           #                                     MOVB seq -&amp;gt; [0] &amp;lt;- NEWEST&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  the load ================                     LOAD [ 0..15]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           ^&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           byte 0&amp;#39;s newest data lives in the MOVB entry,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           bytes 1..15&amp;#39;s newest data in the first MOVUPS:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           no single store spans the load window&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  cannot forward  -&amp;gt;  stall&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The 1-byte store runs &lt;em&gt;last&lt;&#x2F;em&gt;, so at load time byte 0&#x27;s freshest value lives in the &lt;code&gt;MOVB&lt;&#x2F;code&gt; while bytes 1..15 live in
the earlier 16-byte &lt;code&gt;MOVUPS&lt;&#x2F;code&gt;. The load wants all 16 as a unit, but its bytes have two different most-recent
writers. No single pending store covers the read, so the core can&#x27;t shortcut it. It stalls until those writes
land in L1, then reads them back the slow way.&lt;&#x2F;p&gt;
&lt;p&gt;Now compare &lt;code&gt;Prepared&lt;&#x2F;code&gt;. Its loop assembly is &lt;em&gt;almost identical&lt;&#x2F;em&gt;, it still does the double copy, the value into the
argument area and then into the slot. The one difference is where the construction lives:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;asm&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; buffers built ONCE, above the loop:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; X15, (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;CX&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...                 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; the stores that fill bufs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;loop&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;CX&lt;&#x2F;span&gt;&lt;span&gt;), X14    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; same 16-byte readback...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;MOVUPS&lt;&#x2F;span&gt;&lt;span&gt; X14, (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;DX&lt;&#x2F;span&gt;&lt;span&gt;)    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;; ...but the stores that wrote CX retired long ago&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Same instructions, same straddling load. But the stores that built those buffers ran &lt;em&gt;before&lt;&#x2F;em&gt; the loop. They retired
and drained to L1 long before the first readback. Draw the same moment and the collision is simply gone:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  store buffer (writes to bufs already retired)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ┌──────────────────────────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  │           (empty)            │   &amp;lt;- nothing pending&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  └──────────────────────────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  L1 cache (bufs live here, settled)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ┌──────────────────────────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  │ bytes [0 .. 63] of bufs[i&amp;amp;3] │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  └──────────────────────────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           ▲&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           │  LOAD bytes [0 .. 15]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           └──  no pending store to reconcile&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               served straight from L1  -&amp;gt;  no stall&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;By the time the copy runs a million times, there is nothing fresh in the store buffer for it to trip over, so the load
is served from cache at full speed.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s the whole difference between 5.5 ns and 1.7 ns: not the copy, not the API, just &lt;em&gt;when&lt;&#x2F;em&gt; the stores that feed the
copy were issued. Same load instruction, same bytes, the only thing that changed is whether a half-finished store
was still sitting under it.&lt;&#x2F;p&gt;
&lt;p&gt;The benchmark, in other words, was never measuring the &lt;code&gt;Publish&lt;&#x2F;code&gt; API. It was measuring a &lt;em&gt;construct-then-copy
anti-pattern&lt;&#x2F;em&gt;, one that happens to be really easy to write, which is exactly why the number still matters
even though the blame was in the wrong place.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-t-the-library-just-fix-this&quot;&gt;Can&#x27;t the library just fix this?&lt;a class=&quot;post-anchor&quot; href=&quot;#can-t-the-library-just-fix-this&quot; aria-label=&quot;Anchor link for: can-t-the-library-just-fix-this&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The stall doesn&#x27;t happen in the ring buffer&#x27;s memory. It happens in &lt;strong&gt;the caller&#x27;s stack frame, before the value ever
reaches the ring.&lt;&#x2F;strong&gt; Here&#x27;s &lt;code&gt;Publish&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;func&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;r &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;RingBuffer&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;T&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt; Publish&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;payload&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;T&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    r.buffer[nextSequence&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span&gt;r.mask]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; payload&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;   &#x2F;&#x2F; copy the value INTO the slot&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;By the time that line runs, it&#x27;s already over. Trace the caller instead:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;o&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; :=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;object&lt;&#x2F;span&gt;&lt;span&gt;{}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;       &#x2F;&#x2F; zeroing stores  -&amp;gt; caller&amp;#39;s stack&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;o.x[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; seq&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;        &#x2F;&#x2F; 1-byte store    -&amp;gt; caller&amp;#39;s stack&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;rb.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;Publish&lt;&#x2F;span&gt;&lt;span&gt;(o)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;       &#x2F;&#x2F; argument copy: read o back off the stack  &amp;lt;- the stall&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The straddling load is the &lt;em&gt;argument copy&lt;&#x2F;em&gt;, the CPU reading &lt;code&gt;o&lt;&#x2F;code&gt; back out of your stack to pass it by value.
Both the stores and the load live in your frame, and they all finish before the ring buffer&#x27;s own store
into the slot even executes. The slot is just the innocent final destination. You cannot fix a stall
in someone else&#x27;s stack frame by rearranging your data structure.&lt;&#x2F;p&gt;
&lt;p&gt;But the library &lt;em&gt;does&lt;&#x2F;em&gt; have a structural answer. The sibling method hands you a pointer straight to the slot:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;func&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;r &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;RingBuffer&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;T&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt; PublishFunc&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; func&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;T&lt;&#x2F;span&gt;&lt;span&gt;)) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;    f&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span&gt;r.buffer[nextSequence&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span&gt;r.mask])&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;   &#x2F;&#x2F; you fill the slot in place&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There&#x27;s no intermediate &lt;code&gt;o&lt;&#x2F;code&gt;, so there&#x27;s nothing to copy and nothing to read back. You build the event where it will
live.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-to-actually-do-in-production&quot;&gt;What to actually do in production&lt;a class=&quot;post-anchor&quot; href=&quot;#what-to-actually-do-in-production&quot; aria-label=&quot;Anchor link for: what-to-actually-do-in-production&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Strip away the microbenchmark scaffolding, the rotating buffers were only ever a way to &lt;em&gt;simulate&lt;&#x2F;em&gt; &quot;a value whose
stores already retired&quot;, and the guidance is a three-line decision:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Constructing the event at publish time?&lt;&#x2F;strong&gt; Fill it in the slot. Use &lt;code&gt;PublishFunc&lt;&#x2F;code&gt; for one, &lt;code&gt;PublishBatchFunc&lt;&#x2F;code&gt; for
many (which also amortizes the cursor store across the batch):&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;rb.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;PublishFunc&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;func&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FD971F;font-style: italic;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;object&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    *&lt;&#x2F;span&gt;&lt;span&gt;slot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;text-decoration: underline;&quot;&gt;object&lt;&#x2F;span&gt;&lt;span&gt;{}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;   &#x2F;&#x2F; or just overwrite every field&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    slot.x[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; seq&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;})&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;No argument copy, no stall, and it stays fast even when every field differs every time (&lt;code&gt;FullWrite&lt;&#x2F;code&gt;, 1.61 ns).&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Already holding a finished value&lt;&#x2F;strong&gt;: one that arrived from a channel, a decoded frame, a slice you&#x27;re forwarding?
Then &lt;code&gt;Publish(v)&lt;&#x2F;code&gt; is fine. Passing an existing value by value was never the stall; that&#x27;s exactly what &lt;code&gt;Prepared&lt;&#x2F;code&gt;
measured, and it ran near the floor. The trap is &lt;em&gt;construct-then-copy in the same breath&lt;&#x2F;em&gt;, not by-value itself.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; ev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; := range&lt;&#x2F;span&gt;&lt;span&gt; incoming {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt; &#x2F;&#x2F; ev built elsewhere, earlier&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    rb.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E22E;&quot;&gt;Publish&lt;&#x2F;span&gt;&lt;span&gt;(ev)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #88846F;&quot;&gt;         &#x2F;&#x2F; no fresh stores under the copy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Never&lt;&#x2F;strong&gt; write &lt;code&gt;o := T{}; fill(o); rb.Publish(o)&lt;&#x2F;code&gt; back to back in a hot loop. That one shape pays the full stall,
and no API change can save it.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;one-more-twist-your-cpu-may-disagree&quot;&gt;One more twist: your CPU may disagree&lt;a class=&quot;post-anchor&quot; href=&quot;#one-more-twist-your-cpu-may-disagree&quot; aria-label=&quot;Anchor link for: one-more-twist-your-cpu-may-disagree&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Running on an Apple M4 did not render the same results. On arm64 the gap mostly evaporates, the by-value construction
path is roughly on par with the rest, and in go-spmc-ring&#x27;s full benchmark it&#x27;s actually the &lt;em&gt;faster&lt;&#x2F;em&gt; option.
The store-forwarding stall that dominates on x86 simply doesn&#x27;t reproduce there. (Apple silicon has its own,
entirely different stall lurking in that code, but that&#x27;s a story for another post.)&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;The full standalone reproduction (four benchmarks, the disassembly, and the arm64 note) is on &lt;a class=&quot;external&quot; rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pintomau&#x2F;store-forwarding-demo&quot;&gt;GitHub&lt;&#x2F;a&gt;. It runs in about 15 seconds and needs nothing but a Go toolchain. Clone it and watch your own CPU stall.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>go-spmc-ring</title>
          <pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate>
          <author>Mauro Pinto</author>
          <link>https://pintomau.dev/projects/go-spmc-ring/</link>
          <guid>https://pintomau.dev/projects/go-spmc-ring/</guid>
          <description xml:base="https://pintomau.dev/projects/go-spmc-ring/">&lt;p&gt;A single-writer, multiple-reader (SPMC) ring buffer in Go, inspired by the LMAX Disruptor pattern. It focuses on
cache-line awareness, false-sharing avoidance, and lock-free reader lifecycle management: &lt;strong&gt;unlike the classic
Disruptor (where the consumer graph is wired once before the first event) readers here join and leave a
live stream at runtime without locks, allocations, or writer stalls.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This is useful for runtimes and actor model-like systems where you need some entering and reentering readers.&lt;&#x2F;p&gt;
&lt;p&gt;We also preview the compatibility and usage with the preview SIMD package in Go 1.26, which allows for vectorized
operations on the ring buffer.&lt;&#x2F;p&gt;
&lt;p&gt;On a Ryzen 5 9600X (Linux, Go 1.26) a single publish takes ~5.2 ns (≈193 M ops&#x2F;s), batched publishes reach ~2.1 ns&#x2F;item
(≈476 M items&#x2F;s), and eight concurrent readers cost the writer less than 7%.&lt;&#x2F;p&gt;
&lt;p&gt;Check the &lt;a class=&quot;external&quot; rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pintomau&#x2F;go-spmc-ring&quot;&gt;repo&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</description>
      </item>
    </channel>
</rss>
