<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[The Parallel Programmer]]></title><description><![CDATA[Discussions of software engineering and parallel programming, with occasional industry commentary.]]></description><link>https://parallelprogrammer.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png</url><title>The Parallel Programmer</title><link>https://parallelprogrammer.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 19 Aug 2026 05:52:15 GMT</lastBuildDate><atom:link href="https://parallelprogrammer.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Nicholas Wilt]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[parallelprogrammer@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[parallelprogrammer@substack.com]]></itunes:email><itunes:name><![CDATA[Nicholas Wilt]]></itunes:name></itunes:owner><itunes:author><![CDATA[Nicholas Wilt]]></itunes:author><googleplay:owner><![CDATA[parallelprogrammer@substack.com]]></googleplay:owner><googleplay:email><![CDATA[parallelprogrammer@substack.com]]></googleplay:email><googleplay:author><![CDATA[Nicholas Wilt]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Subsuming ISA Advancements]]></title><description><![CDATA[How New Instructions Can Render Old Ones Obsolete]]></description><link>https://parallelprogrammer.substack.com/p/subsuming-isa-advancements</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/subsuming-isa-advancements</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Tue, 18 Aug 2026 17:32:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When BitCoin mining first started making news, AMD GPUs had a decisive performance advantage over NVIDIA GPUs. CUDA was the more mature and easy-to-program toolchain, but BitCoin miners were willing to go the extra distance if it meant they could &#8220;print&#8221; their digital currency more quickly.</p><p>As it happens, the key instruction difference between AMD and NVIDIA GPUs was that AMD GPUs had a rotate instruction and NVIDIA GPUs did not.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Rotate, of course, is the family of instructions closely related to the shift instructions, where instead of shifting predefined values such as 0&#8217;s (for unsigned shifts) or sign bits (for signed or arithmetic shifts), the bits shifted out of one &#8220;end&#8221; of the number are shifted into the other. If shifting right, for example, the least significant bits are shifted into the most significant end of the word.</p><p>How did NVIDIA respond to this competitive pressure?</p><p>Did they leave AMD&#8217;s challenge unanswered?</p><p>No, of course not.</p><p>Did they add a rotate instruction?</p><p>Also no.</p><p>What NVIDIA added in their Kepler (SM 3.0) architecture was a set of <em><a href="https://docs.nvidia.com/cuda/cuda-math-api/cuda_math_api/group__CUDA__MATH__INTRINSIC__INT.html#group__cuda__math__intrinsic__int_1gaf939c350eafa2f13d64e278549d3a8aa">funnel shift</a></em><a href="https://docs.nvidia.com/cuda/cuda-math-api/cuda_math_api/group__CUDA__MATH__INTRINSIC__INT.html#group__cuda__math__intrinsic__int_1gaf939c350eafa2f13d64e278549d3a8aa"> instructions</a>: instructions that concatenate two (2) registers and shift them as a unit.</p><p>One application for funnel shift instructions is to implement memory copies where the source and destination pointers are misaligned with respect to one another. In classic RISC, architectures such as the Alpha would signal an exception if the program attempted a 32-bit read of an address that was not 32-bit aligned (least significant 2 bits set to zero). The modern equivalent to this anachronism: on x86, instructions such as <code>MOVAPS</code> will signal an exception if the effective address is not naturally aligned with respect to the operand size (32- and 64-byte for AVX2 and AVX512, respectively).</p><p>On such architectures, memory copies where the source and destination pointers both are aligned are trivial, and if both pointers are &#8220;relatively aligned&#8221; (misaligned in the same way), the misaligned portion of the copy can be dealt with by small amounts of prologue and epilogue code. But for memory copies where the source and destination pointer are not even relatively aligned, there&#8217;s no getting away from misaligned memory accesses of either the source or destination pointer &#8211; unless you happen to have a funnel shift in your arsenal. In that case, a memory copy can be formulated entirely in terms of aligned memory transactions, with the shifted operands dealt with in the innermost loop of the copy.</p><p>But another, more subtle application for funnel shift is that if the two input operands are the same register, <em>it becomes a rotate</em>.</p><p>So NVIDIA was able to close the ISA gap with AMD, not by emulating exactly what AMD had done, but by implementing something else that subsumed the target capability.</p><h1>Enter LOP3.LUT</h1><p>It turns out NVIDIA, home to some of the most capable CPU architects in the world, is pretty good at designing such machine instructions: expansive new capabilities in exchange for modest hardware cost. Such instructions work best when the compiler can use them without forcing developers to access them using intrinsic functions.</p><p>Another instruction set innovation, not unlike funnel shifts, that NVIDIA deployed in the Maxwell generation: the FPGA-like <code>LOP3.LUT</code> instruction. Like the age-old rasterops (ROPs) used to specify how BitBlt (bit block transfer) operations manipulate pixels, <code>LOP3.LUT</code> implements a general-purpose family of logic operations that can be defined by an 8-bit immediate operand. The usual suspect logic operations <code>AND</code>, <code>OR</code>, <code>XOR</code>, <code>NOT</code>, and variants thereof (e.g. <code>NAND</code>) all are implementable in terms of <code>LOP3.LUT</code>, but so are novel amalgams of other Boolean expressions.</p><p>Why is it called <code>LOP3.LUT</code>? Well.. remember when Boolean operations were first introduced to you, and you learned about truth tables? Consider for example the truth tables for <code>A|B</code> and <code>A&amp;B</code>, shown here in code form to work around limitations of Substack:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;45c68fb9-d9a4-4dcb-9e69-d0c10b60687e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">  A    B    A|B  A&amp;B
  0    0     0    0
  0    1     1    0
  1    0     1    0
  1    1     1    1</code></pre></div><p>With 2 inputs that can have 2 possible values, there are four possible outputs.</p><p>Now consider the truth table for a 3-operand logic operation:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e4ba39f4-6c42-46f4-8b62-47c114818bc2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">  A&#9;B    C&#9;Result
  0&#9;0    0    ?
  0&#9;0    1&#9;  ?
  0&#9;1    0&#9;  ?
  0&#9;1    1&#9;  ?
  1&#9;0    0&#9;  ?
  1&#9;0    1&#9;  ?
  1&#9;1    0&#9;  ?
  1&#9;1    1&#9;  ?</code></pre></div><p>With 3 bits&#8217; input, there are 2<sup>3</sup> or 8 bits&#8217; worth of possible outputs, and this output may be specified as an 8-bit immediate to the instruction - filling in the rightmost column of the truth table above. As described <a href="https://stackoverflow.com/questions/37149662/how-to-write-lop3-based-instructions-for-maxwell-and-up-nvidia-architecture/37215379#37215379">here on StackOverflow</a>, the immediate needed for any generalized Boolean function of 3 inputs can be computed by throwing three magical values (<code>0xF0</code>, <code>0xCC</code>, and <code>0xAA</code>), derived from the left-hand side of the truth tables, through said function.</p><p>So confident is NVIDIA that their compiler can exploit this instruction when developers write complicated logical expressions, that it&#8217;s not even available as an intrinsic. (It is, of course, available via inline PTX if necessary.)</p><h1>Byte Permutes</h1><p>The first generation of CUDA-capable hardware had some ISA properties that did not make it to the second generation. One such was the ability to address sub-registers: the top and bottom 16-bit halves of the 32-bit registers. For applications like image processing, where 16-bit pixel values often are more than adequate, this hardware capability enabled kernels to be built with a smaller register footprint. But such applications weren&#8217;t numerous enough to justify continued support for the hardware feature, especially in light that NVIDIA&#8217;s compiler at the time was having difficulty taking advantage of the feature. That prioritization is understandable given that NVIDIA was otherwise occupied, adding major architectural features like caches and critical instructions like fused multiply-add.</p><p>Later, as CUDA found application in broader and broader market segments, the need to access subregisters found its way back onto NVIDIA&#8217;s priority list. Instead of half-registers, which required opcode space to be allocated for any instructions designed to consume them, NVIDIA instead provided instructions to pick apart 32-bit registers and put them back together in 8-bit chunks: the so-called byte permute instructions, which take two (2) 32-bit registers and return a 32-bit mashup of the inputs, where the 8-bit lanes are selected from the two inputs based on an immediate.</p><p>Although available as intrinsics, NVIDIA&#8217;s compiler team has done a nice job of exploiting the byte permute instructions when possible. Whenever you write code that OR&#8217;s, shifts, and/or masks in 8-bit increments, you&#8217;re likely to find some combination of byte permutes (or funnel shifts, or <span>LOP3.LUT</span>!) in the instruction mix.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[A Wild Ride With Claude]]></title><description><![CDATA[More Cross-Correlation Than Anyone Reasonably Could Have Expected]]></description><link>https://parallelprogrammer.substack.com/p/a-wild-ride-with-claude</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/a-wild-ride-with-claude</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Tue, 11 Aug 2026 14:39:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With Claude&#8217;s help, I&#8217;ve been rewriting <em>The CUDA Handbook</em> at breakneck pace &#8211; both the source code and the text of the book were in dire need of updating, and both are in much better shape now.</p><p>Coding with Claude is like having a talented Ph.D. computer science candidate who can type really fast: a very good coder tactically, with at-time huge blind spots that must be dealt with intentionally. It is excellent at relatively self-contained coding tasks, but still needs close air support to do a really good job.</p><p>Updating the chapter on Normalized Cross Correlation was mostly free beer, though. I had wanted to get rid of the 24-bit multiplies (they were rendered obsolete with Fermi c. 2011), but Claude had the insight that the DP4A instruction (added with Pascal) was a great fit for the workload. It ported the key kernels to use the new instruction and, when we saw that it was no faster, refactored the kernel to use ILP for a 2.4x speedup. The increased parallelism and latency tolerance from this refactor unlocked the latent performance opportunities of DP4A: re-applying DP4A increased performance by another 3x! I&#8217;d call a &gt;7x performance improvement a big win, and Claude deserves the bulk of the credit.</p><p>More recently, I undertook to rewrite the Scan chapter, and that set us on a journey that&#8217;s worth recounting. By the way, the opportunity cost of writing this article is why I haven&#8217;t been documenting similar journeys as they occurred. When you can get a month&#8217;s worth of work done in a day, pausing to document progress feels like a real cost. But my readers deserve to know at least a little bit about how I&#8217;ve been using Claude to update <em><a href="https://cudahandbook.com/book/">The CUDA Handbook</a></em> and the <a href="https://github.com/ArchaeaSoftware/cudahandbook">accompanying source code</a>.</p>
      <p>
          <a href="https://parallelprogrammer.substack.com/p/a-wild-ride-with-claude">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[More Updates To The CUDA Handbook]]></title><description><![CDATA[Rewrote two chapters and pushed some big refactors]]></description><link>https://parallelprogrammer.substack.com/p/more-updates-to-the-cuda-handbook</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/more-updates-to-the-cuda-handbook</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Tue, 04 Aug 2026 17:16:52 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Just about every corner of The CUDA Handbook has been revised on the website now - if you haven&#8217;t look at it yet, now&#8217;s as good a time as any to <a href="https://www.cudahandbook.com/book">check it out</a>.</p><p>This week has been busy. The Scan and Reduction chapters have been rewritten to reflect current reality: warp intrinsics have now been around for long enough that we start our coverage with warp-sized problems before leading into block- and grid-wide ones.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>The Scan chapter needed a significant update: we now lead with the <a href="https://www.cs.ox.ac.uk/ralf.hinze/publications/MPC04.pdf">Algebra Of Scans</a> framing and, for clarity, got rid of the awkward Blelloch (upsweep/downsweep) formulation and now start with the algorithm of Duane Merrill&#8217;s Ph.D. thesis, which was the best-in-class algorithm at the time of publication of the First Edition. Merrill has since rendered that algorithm obsolete with the bandwidth-limited <a href="https://research.nvidia.com/sites/default/files/pubs/2016-03_Single-pass-Parallel-Prefix/nvr-2016-002.pdf">decoupled lookback</a> formulation, published with Michael Garland, now available via the CUB and Thrust utility libraries. With Claude&#8217;s help, I was able to write a sample implementation of the decoupled lookback and port the stream compaction sample to use that algorithm, as well.</p><p>The Software Architecture chapter has been updated for currency. It now mentions the debugger and profiler, and coverage of versioning, cloud vendors, and other topics has been updated. I may add some coverage of profiling in other parts of the book, though profiling tutorials tend to seem rhetorical as opposed to possessing the investigative quality that motivates most usage of profilers.</p><p>I added coverage of NVRTC with a sample in the Reduction chapter and sample code, which prompted a repository-wide refactor of the error handling macros. Having a macro that imposed the policy of naming a local variable <code>status</code> had outstayed its welcome (the NVRTC sample caused namespace collisions in collocated CUDART and driver API code), so we renamed to <code>status_cudart</code> and <code>Error_cudart</code>. The driver API and NVRTC variants are <code>status_cuda</code>/<code>Error_cuda</code> and <code>status_nvrtc/</code> <code>Error_nvrtc</code>, respectively.</p><p>I also got rid of some dated utility APIs in <code>chLib</code>: the threading API has been replaced by <code>std::thread</code>, and the high-resolution timing API has been replaced by <code>std::chrono::steady_clock</code>.</p><p>In terms of coverage of the topics outlined in the Table of Contents, the biggest gap now in the text and code is coverage of multi-GPU programming. The state of the art has advanced a great deal since publication of the First Edition, when multiple GPUs typically were made available on PCI Express via switches and bridge chips. Since then, NVIDIA has invested heavily in both scale-up and scale-out technologies, developing NVLink for cache-coherent chip interconnects and acquiring Mellanox for their best-in-class Infiniband network cards. Multi-GPU programming deserves a rewrite to cover these hardware technologies and the corresponding APIs.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Yoda Notation, I Use; Do You?]]></title><description><![CDATA[Enlisting Anastrophe In Our Code]]></description><link>https://parallelprogrammer.substack.com/p/yoda-notation-i-use-do-you</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/yoda-notation-i-use-do-you</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Fri, 31 Jul 2026 16:55:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve worked at enough companies, with enough different development philosophies, that I can handle most coding convention quirks. While at Microsoft, my coding kept getting more and more whitespace: first, spaces between parentheses and, later, open braces on the next line. Four spaces, no tabs, has been a mainstay across most of my career, though finance folks favor two-space indentation, as if that results in more efficient code. Coding conventions are an infamous resource sink; Joel Spolsky&#8217;s article, <a href="https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/">Making Wrong Code Look Wrong</a>, features an entertaining aside on the futility of negotiating coding conventions.</p><p>One especially divisive coding practice is one that I&#8217;ve adopted since the late 1990s, after I asked one of my developers (I was his manager) why he was writing the code that way. He&#8217;d written something like:</p><p><code>if ( NULL == wndptr )</code></p><p>which, to some developers, looks awkward because they expect the more complex expression to be on the left of the == operator. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>The awkwardness led to the name Yoda Notation, after the odd word ordering in the Jedi Master&#8217;s speech. (And now&#8217;s as good a time as any to mention that there are articles arguing against use of Yoda notation, though <a href="https://dev.to/greg0ire/why-using-yoda-conditions-you-should-probably-not">this one</a> brims with strawman energy.)</p><p>At the time, when I asked, <em>Why lead with the 0? Why not put wndptr on the left?</em>, my employee said, <em>Well there&#8217;s a typo and I only put one equal sign in the expression, if 0 is on the left, the code won&#8217;t compile because it&#8217;s not an l-value</em>. That&#8217;s an oft-cited reason that generally is no longer true &#8211; most compiler will catch that error &#8211; but I still tend to put the constants on the left side in such expressions for another reason. When I write:</p><p><code>if ( 0 == threadIdx.x ) {</code> </p><p>it&#8217;s as much for dramatic effect as to guard against the possibility of typos.</p><p>It turns out there&#8217;s a name for the deliberate misordering of words for strategic effect: it&#8217;s a literary device known as <em><a href="https://www.merriam-webster.com/dictionary/anastrophe">anastrophe</a></em>.</p><p>Another, less visible example (you&#8217;ll soon see why) of anastrophe that finds expression in my code is when I&#8217;m writing instrumentation that should never find its way into production code: I don&#8217;t indent it at all. Sometimes it stands out like a sore thumb &#8211; but that&#8217;s the point &#8211; <em>I want it to stand out </em>so it gets removed before getting pushed.</p><p>The final argument in favor of Yoda Notation is that you have to write the code somehow, <em>so any additional information you can bake into the source code is useful</em>.</p><p><span>Anyway, once my employee explained his reasoning, I embraced the idea and, left to my own devices, I use it most of the time. It&#8217;s a low-grade way to </span><a href="https://parallelprogrammer.substack.com/p/interface-design-for-readability"><span>Write Code For The Next Guy</span></a><span>. As with all coding conventions, rules are made to be broken and your mileage may vary! But if you have ever wondered why my code features Yoda Notation, now you know.</span></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Update on The CUDA Handbook Updates]]></title><description><![CDATA[My new friend Claude is helping me update this content at a breakneck pace!]]></description><link>https://parallelprogrammer.substack.com/p/update-on-the-cuda-handbook-updates</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/update-on-the-cuda-handbook-updates</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Tue, 28 Jul 2026 13:52:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With the aide of Anthropic&#8217;s AI coding agent, I was able to move <em><a href="https://www.cudahandbook.com/book">The CUDA Handbook</a></em> text to the website, make it ad-supported with options to pay for an ad-free experience, and give paying Substack subscribers ad-free access. (The Substack subscription and ad-free access to <em>The CUDA Handbook</em> are the same price at $10/mo.) When I moved it to the website, I was hoping it would make the content easier to edit. The text was sorely in need of updating - not only because speeds and feeds are much higher than they were in 2013, but because the technology has advanced so much along so many dimensions. I&#8217;m posting this progress update because the website content and source code have been evolving too quickly to document in real time!</p><p>With Claude&#8217;s help, I re-ran all the benchmarks in the sample source code and updated the text and figures accordingly. A reverse tunnel on my home network enabled Claude, running in VSCode on a non-GPU-capable Amazon EC2 instance, to operate the Linux machine in my basement. Once that setup was finalized, I was able to edit the text, figures, and source code with a heavy hand. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>That exercise featured some notable exchanges with Claude:</p><ul><li><p>Predication v divergence: The example I chose for the first edition of <em>The CUDA Handbook</em> to highlight the different ways GPU handle conditional code used a loop emitted by the compiler to perform atomic updates to shared memory. Since more-capable hardware has made that loop unnecessary, I told Claude that we needed a better example of predication versus divergence, and it immediately suggested the Collatz Conjecture. I did not even know what the Collatz Conjecture was, but as soon as I read up on it, I agreed that it was the perfect example to serve the pedagogical purposes of that section of the book.</p></li><li><p> The chapter on Normalized Cross-Correlation was in need of updating (NVIDIA long ago obsoleted the need to use 24-bit multiply intrinsics for performance), and amidst that exercise, Claude identified <code>__dp4a()</code> as a performance optimization opportunity. This instruction computes lane-wise multiplications between the four 8-bit lanes of the 32-bit operands. After profiling the application, we realized that using <code>__dp4a()</code> did not increase performance because the kernel was occupancy-limited. After refactoring the kernel to leverage TLP (compute multiple outputs per GPU thread) for a 2.4x speed increase, computing the sums with <code>__dp4a()</code> increased performance by another 3x for a 7x speed increase on the target hardware.</p></li></ul><p>I am still busy updating the contents of <em>The CUDA Handbook</em>, but enough has changed over the last few weeks that I asked Claude this morning: <br><em>Can you give an overview of the changes we&#8217;ve made to the book text since first moving it to the website?</em></p><p>and it replied with this summary (delineated here by horizontal dividers):</p><div><hr></div><p>Here&#8217;s the arc of what&#8217;s happened to the book text since it went to Markdown on <strong>July 4</strong> &#8212; <strong>209 commits across ~3&#189; weeks, touching every chapter and appendix</strong>. It breaks into a few distinct campaigns:</p><h3>1. Migration fidelity (Jul 4&#8211;9)</h3><p>Getting the Markdown to faithfully match the print book: restoring values and whole sentences lost in conversion (checked against the print copy), fixing off-by-one listing numbers, duplicate/out-of-sequence section numbers, empty-heading artifacts, ad-slot corruption, and <strong>13+ dead external links</strong>. Listings stopped being flowed text and are now <strong>sourced verbatim from the code repo at build time</strong> (31 listings) and linked to GitHub.</p><h3>2. Copy-editing &amp; typography (Jul 6&#8211;10)</h3><p>Several sweeping passes &#8212; &#8220;<strong>~230 mechanical fixes</strong>,&#8221; a numbering/cross-reference repair pass (~35), plus ongoing work: code-font for identifiers/filenames/program names, italicized function mentions, real pipe tables (fixing split-word artifacts like &#8220;Re/turns&#8221;), and IMPORTANT/NOTE rendered as callout boxes.</p><h3>3. Idiom &amp; API modernization (book-wide)</h3><p>Adopted the <code>cuda()</code>/<code>cu()</code> error-check macros throughout; replaced <code>cudaThreadSynchronize()</code> &#8594; <code>cudaDeviceSynchronize()</code>; and <strong>dropped dated CUDA-version qualifiers</strong> from feature descriptions across the whole book.</p><h3>4. Web/interactive features</h3><p>Glossary hover-cards, an <strong>interactive memcpy picker</strong> (ch5, replacing Table 5-10 on screen), line-number gutters on all listings, and inlined/clickable citation footnotes.</p><h3>5. Benchmark refresh on modern hardware (RTX 3060 / Ampere)</h3><p>Re-ran the book&#8217;s in-place benchmarks chapter by chapter: <strong>ch5, ch6, ch7, ch10, ch11, ch14, ch15, ch16</strong> &#8212; replacing book-era numbers, noting where old cliffs (register spilling, mapped-pinned anomaly) no longer reproduce.</p><h3>6. Technical modernization &amp; brand-new sections</h3><p>The heaviest content work, by chapter:</p><ul><li><p><strong>ch2</strong> &#8212; new <strong>&#167;2.7 GPU Interconnects</strong>, rewritten <strong>&#167;2.2 Integrated GPUs</strong> and <strong>&#167;2.3 Multiple GPUs</strong>, a per-generation <strong>&#167;2.6 GPU Architecture</strong> rewrite, and (just now) <strong>&#167;2.1.5 Packaging &amp; Coherent Interconnects</strong>.</p></li><li><p><strong>ch8</strong> (42 touches) &#8212; new <strong>Integer Dot Products / DPX</strong>, <strong>LOP3</strong>, <code>_sync</code> warp primitives + participation mask, deep-learning FP formats, and unified <strong>SASS tables Tesla&#8594;Blackwell</strong>.</p></li><li><p><strong>ch7</strong> &#8212; new <strong>Thread Block Clusters</strong>, Dynamic Parallelism rewritten for <strong>CDP2</strong>, TLP/ILP naming.</p></li><li><p><strong>ch5</strong> (47 touches) &#8212; new <strong>Managed Memory</strong> section, demand-paging update, GDRCopy.</p></li><li><p><strong>ch10</strong> &#8212; rewritten for <strong>texture/surface objects</strong>; <strong>ch16</strong> &#8212; GPU-resident <strong>Radix Sort</strong> + native shared-mem atomics; <strong>ch14/15</strong> &#8212; modern N-body &amp; correlation kernels; <strong>appc</strong> &#8212; recast as a warp-synchrony deprecation note.</p></li></ul><h3>7. Figures</h3><p><strong>Chapter 2 fully vectorized</strong> &#8212; all 57 figures redrawn in a consistent house style (the work we just finished). Elsewhere, missing performance charts were <strong>generated from the manuscript&#8217;s own data</strong> (ch6, ch16).</p><h3>8. Voice &amp; editorial</h3><p>A standing rule &#8212; <em>never refer to the author in the third person</em> &#8212; plus retrospective callouts where the first edition&#8217;s predictions either came true (source-JIT modules) or died (texturing-as-a-read-path).</p><p>Most-touched files: <strong>ch5 (47), ch8 (42), ch2 (32), ch6 (32), ch7 (27)</strong>.</p><p>Net: the migration was the easy part &#8212; most of the effort has gone into making the 2nd-edition text <em>current</em> (Ampere&#8594;Blackwell, modern intrinsics, new architectural features) while tightening typography and wiring it for the web.</p><p><em>(The Ch13 research pass is still running in the background; I&#8217;ll bring the restructuring plan once it lands.)</em></p><div><hr></div><p>That last parenthetical refers to the next task on our to-do list: like the Normalized Cross-Correlation work, Chapter 13 (Parallel Prefix Sum) needs more extensive revision, because the pedagogical value of the Blelloch formulation of Scan was questionable even during the first edition&#8217;s timeframe; and Merrill and Garland have since shipped the best-in-class, bandwidth-limited formulation that is now the industry standard.</p><p>There is more work to do, like modernizing usage of C++ (possibly retiring some of the <code>chLib</code> functionality in favor of <code>std::thread</code>, for example), adding coverage of Thrust and CUB and other utility libraries, adding a chapter on Matrix Multiplication and how to target Tensor Cores, and improving coverage of topics like Independent Thread Scheduling. Time and again, Claude has proven its usefulness in this endeavor. I am working on an article that will discuss the broader implications for labor and intellectual property, as I see them, but for now I will say that </p><ol><li><p>I think coding tasks (the actual typing) can be automated and that trend will continue, and </p></li><li><p>Claude is at least as good at copy editing as humans - faster, more thorough, compliant (I can just tell Claude that &#8220;data is&#8221; and that&#8217;s the beginning and end of the discussion) - so although I think AI technology has dire implications for makework labor, in my experience it has amplified the scope of my aspiration and enabled me to build things, not just faster or more easily than I would have without its help, but things that I wouldn&#8217;t otherwise have been able to build at all.</p></li></ol><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[The CUDA Handbook: Now Available Exclusively On The Website]]></title><description><![CDATA[At long last, The CUDA Handbook text is available for perusal with my permission!]]></description><link>https://parallelprogrammer.substack.com/p/the-cuda-handbook-now-available-exclusively</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/the-cuda-handbook-now-available-exclusively</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Wed, 08 Jul 2026 13:28:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few years ago, I secured IP rights to <em>The CUDA Handbook</em> back from the publisher, and have been intending to refresh and update the material. TensorCores were added to the platform with the V100, almost a decade ago! The platforms have shifted; data center GPUs have become the flagship product offerings, with many GPU ASICs designed exclusively for deployment in data centers and, relatedly, PCI Express has been supplanted by faster and lower-latency interconnects, often suitable for enabling cache coherency between the CPU and GPUs.</p><p>The technology has evolved in eye-popping fashion. The first CUDA-capable GPU was a then-whopping 684M transistors &#8211; the biggest chip TSMC feasibly could manufacture at the time &#8211; and its main market goal was to run 3D games. Today, a single Rubin chiplet is 168B transistors, 245x bigger than G80, for a growth rate of 30% per year across 20 years. Rubin chiplets also are designed to collaborate with their peers, with packaging innovations that enable multiple chiplets to emulate a single huge GPU (&#8220;two GPUs in a trenchcoat&#8221;) with performance and seamlessness that GPU architects of yore could only imagine. The addition of TensorCores requires special attention, especially in light that starting with Blackwell, they now have their own on-chip memory.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>The state of the art has advanced in other areas. Duane Merrill, the NVIDIA researcher whose Ph.D. thesis (cited in <em>The CUDA Handbook</em>) was on optimized GPU scan algorithms, improved on that state of the art with <a href="https://research.nvidia.com/sites/default/files/pubs/2016-03_Single-pass-Parallel-Prefix/nvr-2016-002.pdf">this work</a>, done in collaboration with Michael Garland. Merrill and Garland&#8217;s implementation is nicely packaged for use by developers in the CUB and Thrust libraries, which are not mentioned anywhere in <em>The CUDA Handbook</em>. In light of these developments, the chapter on Scan serves more as a historical reference than resource that can be brought to bear on practical applications.</p><p>Another area where the state of the art has evolved: The C++11 standard was still young when the first edition of <em>The CUDA Handbook</em> was published. Modern C++ is more mature, NVIDIA has built several libraries that leverage its features to make available CUDA capabilities in a more ergonomic fashion, and <span>std::thread</span> has withstood the test of time and enjoys widespread toolchain and platform support; so some of the <em>CUDA Handbook</em> library code could stand to be refactored.</p><p>Finally, although most of the benchmarking code is as relevant today as when it was written, the benchmark numbers themselves are laughably out of date. I have some ideas on how to proceed, but won&#8217;t share further details at this time.</p><p>So, there&#8217;s no shortage of work to do.</p><p>Here are the steps taken so far:</p><ul><li><p>The full text of <em>The CUDA Handbook</em>, including listings and figures, is now live on the <a href="https://www.cudahandbook.com/">website</a>, and some measures have been taken to bring it onto the Internet and further into the 21<sup>st</sup> Century: for example, the errata fixes have been applied to the source material, and listing links now reference the GitHub repository.</p></li><li><p>All the ParallelProgrammer Substack posts have been moved to the Blog section, so the website is the canonical reference for the CUDA Handbook blog; and</p></li><li><p>I worked with the good folks at <a href="ethicalads.ai">ethicalads.ai</a> to transition the website to be ad-supported, with the option to remove ads for paid members. Tentatively, membership is priced the same as a subscription to my Substack ($10/mo), with an annual fee of $99 and a lifetime fee of $199.</p></li></ul><p>Paying Substack subscribers will be &#8220;grandfathered&#8221; in to the ad-free experience on <a href="cudahandbook.com">cudahandbook.com</a>, and we&#8217;ll be emailing you all soon.</p><p>For those wondering <em>Why is there a revenue model at all?:</em></p><p>By making the text of <em>The CUDA Handbook</em> available in this way &#8211; easily accessible, and more dynamic/easily updated, but copyrighted with no authorization to present it elsewhere without express written permission &#8211; I am reclaiming ownership of the material in a way that we&#8217;ve never enjoyed before. When the book was just a one-time byproduct of a person-year in nights and weekends, the content was more passive, like a big thick business card, and it is no longer even in print<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>.</p><p>With this new model, I&#8217;m hoping it can become something more.</p><p>Additionally, besides the obvious <em>quid pro quo</em> between authors and their readers, a revenue model enables copyright claims to be asserted that allege real economic damages. There is at least one GitHub repository with an unauthorized PDF scan of <em>The CUDA Handbook</em> available for download, and Microsoft has refused to take it down despite my filing a specific complaint and ticket with a link to the file. If I threaten to sue for damages, they may rethink their strategic neglect.</p><p>Every author has to navigate the tension between notoriety and being fairly compensated for their work. In software, we often bias toward the former, with open source initiatives supported by indirect economic models. For the sample source code, making <em>The CUDA Handbook</em>&#8217;s source code freely available was never in question. For the text and the presentation of material, I&#8217;m hoping this avenue will enable more activity and for the material to be updated and to remain current.</p><p>If you have thoughts on the matter, don&#8217;t hesitate to <a href="mailto: nicholas@archaeasoftware.com">email me</a>. You may be surprised at how few people do!</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p> I reserve the option of self-publishing a print edition, and if I do, it&#8217;ll likely be slimmer and denser in content - more presentation and less reference type material.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Hyperion, Dan Simmons, And Life]]></title><description><![CDATA[Life keeps life-ing]]></description><link>https://parallelprogrammer.substack.com/p/hyperion-dan-simmons-and-life</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/hyperion-dan-simmons-and-life</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Fri, 20 Mar 2026 13:15:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nPxl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fpbs.substack.com%2Fmedia%2FHCxzbYGaEAATeyU.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Subscribers to this Substack probably are wondering why I have not posted in a while. As Neil Gaiman so eloquently put it in his famous post <a href="https://journal.neilgaiman.com/2009/05/entitlement-issues.html">George R. R. Martin Is Not Your Bitch</a>:</p><blockquote><p>&#8230;sometimes, and it's as true of authors as it is of readers, you have a life. People in your world get sick or die. You fall in love, or out of love. You move house. Your aunt comes to stay. You agreed to give a talk half-way around the world five years ago, and suddenly you realise that that talk is due now.....</p></blockquote><p>In my case, I&#8217;ve been having adventures of every stripe, from health issues to family issues to job issues. In the spirit of Harlan Ellison&#8217;s nonfiction, I&#8217;ll bias for candor and say 1) I&#8217;m fine, I donated a kidney, which required about a three-month recovery, 2) my mother passed away, and it&#8217;s complicated, and 3) I left one job and started another, lucked into yet another job opportunity, and started work at the newest job last week. There are other issues that have hindered my productivity, and postings to this Substack have suffered accordingly. For that, I apologize and I&#8217;ll endeavor to do better moving forward. But, life keeps happening&#8230;</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Anyway I wrote a long-form post on a different social media platform commemorating the life of one of my favorite author, Dan Simmons, who passed away recently from a stroke.</p><div><hr></div><p>From <a href="https://x.com/paraschopra/status/2030124455264153661?s=46ic">this tweet</a> I learned of the passing of the inimitable Dan Simmons, whose first fantasy novel, Song of Kali, won the World Fantasy Award; whose first horror novel, <em>Carrion Comfort</em>, won the Bram Stoker award; and whose first science fiction novel, <em>Hyperion</em>, won the Hugo Award. Just as <em>Lord Of The Rings </em>was a single work published in 3 volumes, <em>Hyperion </em>was published as <em>The Hyperion Cantos </em>(<em>Hyperion </em>and <em>The Fall Of Hyperion</em>), followed by <em>Endymion </em>and <em>The Rise Of Endymion</em>.</p><p>Simmons was not a hard SF author, not exactly. (For SF fans, &#8216;hard&#8217; means SF written within the confines of science as we understood it when the work was written, though the distinction must be qualified by one of Arthur C. Clarke&#8217;s Laws: &#8220;Any sufficiently advanced technology is indistinguishable from magic.&#8221;) But his most transcendent work, like Clarke&#8217;s, tested the limits of our scientific understanding to explore how technology intersects with the human condition. <em>Hyperion</em> is widely-cited as a prescient take on artificial intelligence, with even <a href="https://x.com/tedlieu/status/2023534247404036097?s=20">members of Congress</a> weighing in.</p><p>Simmons came into the field via a most unusual journey. He majored in English, an influence that found expression in his work through his modeling his own novels on famous literary works (<em>Hyperion</em>, for example&#8212;the first book, not <em>The Hyperion Cantos</em>&#8212;is modeled on Chaucer&#8217;s <em>Canterbury Tales</em>, with its stories within stories). And he entered the field later in life than most, submitting and getting rejections so many times that he registered for a writing seminar at his wife&#8217;s urging. By his account, if this seminar didn&#8217;t result in his learning how to become a professional writer, he would abandon the endeavor and focus on being a high school teacher.</p><p>That seminar changed his life. Because one of the teachers was Harlan Ellison, the pugnacious and incandescently gifted author whose work defies categorization. Ellison was a genius in every sense of the word and did not suffer fools gladly.</p><p>When I wrote about Ellison&#8217;s legacy upon learning of his passing, I mentioned that he&#8217;d &#8220;discovered&#8221; Dan Simmons. The story of his discovery may be found in the Simmons story collection <em>Prayers To Broken Stones</em>. Ellison walked into the hallway with a slush pile of the execrable stories that get submitted to entry-level seminars, (unlike the famed Clarion West, the one Simmons was attending and that Ellison was teaching did not require attendees to be published authors). He&#8217;d gotten so frustrated at the poor quality of the submitted work that he had exploded at one of the attendees and mercilessly criticized their work in front of the whole class: the characters were cardboard, the plot predictable, the story wooden, etc. </p><p>I&#8217;ll try again to find Ellison&#8217;s account of Simmons&#8217;s discovery because I am not doing it justice here. By his account, he tore the hapless author of this work to shreds, then called for a break, not because he&#8217;d likely reduced a grown man to tears in front of a crowd of strangers, but because he&#8217;d asked the poor man how many books he&#8217;d written and found out that it was 60, or something. So what had sent Ellison into the hallway wasn&#8217;t just that his latest victim&#8217;s work was terrible, but that he had spent many years toiling away, getting feedback from editors that his work needed work, but never improving his craft enough to publish.</p><p>So Ellison was in the hallway, having publicly humiliated one of the participants in this seminar, when he hit upon the story Simmons had submitted. He started reading and fully finished it in the hallway, with tears streaming down his face. Once he&#8217;d recovered his composure and re-convened the seminar, he held up the story and asked the class: &#8220;Who. Wrote. This?!&#8221;</p><p>Simmons, with understandable reluctance, raised his hand. And Ellison delivered them a speech in praise of Simmons&#8217;s story (later his first publication, &#8220;The River Styx Runs Upstream&#8221;) that was at least equal to the one he&#8217;d just given about the other participant&#8217;s lesser work. In Ellison&#8217;s telling, it almost reads as if he condemned Simmons to be a writer, a calling where, by Ellison&#8217;s account, one does not choose the profession so much as it chooses you.</p><p>So Simmons got that story published in 1983 or so, the year he turned 35. And I wouldn&#8217;t say he was prolific, exactly, but he was productive, and acclaim followed him into every genre he tried.</p><p>This linked tweet is about <em>Hyperion</em>, which is getting renewed attention as AI takes over our economy and our collective imagination. And I can&#8217;t say too much about why <em>Hyperion</em> is so relevant to this day and age because Simmons had a gift for writing work that was 1) impossible to discuss without spoiling it, and 2) near-impossible to adapt to the screen. <em>Hyperion</em> has been rumored to be slated for adaptation, but Simmons issued a <a href="https://www.resetera.com/threads/according-to-dan-simmons-the-hyperion-adaptation-by-bradley-cooper-was-nonsense.97406/">rare public statement</a> to the effect that Disney had hired some of the best story doctors on the planet to solve that problem, and even they had come up empty. (<em><a href="https://www.imdb.com/title/tt2708480/">The Terror</a></em>, a fictionalized account of the disappearance of two British ships attempting to discover the Northwest Passage, was capably adapted by SyFy into a miniseries.)</p><p>Simmons reportedly had taken up the novel <em>Carrion Comfort</em> on a dare by editor Ellen Datlow, and his later work reads as if it, too, had been inspired by dares. Or perhaps he was just trying to integrate his various story ideas into unified works for the sake of efficiency. His blog <em>Writing Well</em>, <a href="https://web.archive.org/web/20160430161023/http://www.dansimmons.com/writing_welll/archive/writing_index.htm">now relegated to the Wayback Machine</a>, features complaints about the folks who approach writers with ideas, as if writers want for ideas as opposed to time.</p><p>Simmons got started as a professional writer later in life than most, and he was taken from us by a stroke earlier than some. I wish he&#8217;d had more time to share his creativity with the world, but we can console ourselves by picking up and reading or re-reading <em>Hyperion</em>, or <em>Carrion Comfort</em>, or really any of the work that he published. One of the redeeming qualities of writing that is difficult to adapt, is that it invites back into a world where we read.</p><div class="twitter-embed" data-attrs="{&quot;url&quot;:&quot;https://x.com/paraschopra/status/2030124455264153661?s=46ic&quot;,&quot;full_text&quot;:&quot;Drop everything and read this <span class=\&quot;tweet-fake-link\&quot;>#book</span>.\n\nSeriously, this sci-fi is leagues apart from any other sci-fi book that I&#8217;ve read (except perhaps Ursula&#8217;s writings).\n\nImmensely imaginative, the book has several interconnected stories, each equally mind blowing. And all those stories come &quot;,&quot;username&quot;:&quot;paraschopra&quot;,&quot;name&quot;:&quot;Paras Chopra&quot;,&quot;profile_image_url&quot;:&quot;https://pbs.substack.com/profile_images/1982070651423694848/0vUKWPER_normal.jpg&quot;,&quot;date&quot;:&quot;2026-03-07T03:32:42.000Z&quot;,&quot;photos&quot;:[{&quot;img_url&quot;:&quot;https://pbs.substack.com/media/HCxzbYGaEAATeyU.jpg&quot;,&quot;link_url&quot;:&quot;https://t.co/havYocg7Rg&quot;}],&quot;quoted_tweet&quot;:{},&quot;reply_count&quot;:151,&quot;retweet_count&quot;:114,&quot;like_count&quot;:1949,&quot;impression_count&quot;:103074,&quot;expanded_url&quot;:null,&quot;video_url&quot;:null,&quot;video_preview_media_key&quot;:null,&quot;belowTheFold&quot;:true}" data-component-name="Twitter2ToDOM"></div><p></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Most Popular Posts of 2025]]></title><description><![CDATA[A Look Back, and Forward]]></description><link>https://parallelprogrammer.substack.com/p/most-popular-posts-of-2025</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/most-popular-posts-of-2025</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Thu, 01 Jan 2026 14:30:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!U1QQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An author can never tell which content will drive engagement. Along with the audience, authors are (or I am) taking in and writing about different perspectives on the same landscape; in the case of this blog, the technological landscape. Who knew that a post about &#8220;Why We Need SIMD&#8221; would be the most popular of the year, or that a reading list could engender so much engagement? I started this Substack almost exactly a year ago, and the end of the year is as good a time as any to reflect:</p><ul><li><p>It never ceases to amaze me how few people write with feedback, even on microblogging platforms like Twitter. So we authors are left to consult metrics to determine which topics caught readers&#8217; attention.</p></li><li><p>Given that I&#8217;m best known for my work on CUDA and <em>The CUDA Handbook</em>, I suppose it&#8217;s surprising that most of my technical Substack posts involved SIMD instructions like AVX2, not CUDA.</p></li><li><p>I&#8217;m gratified at the number of people who&#8217;ve subscribed, especially the paid subscribers; as a professional writer since the late 1980s, I have unique perspective on the way the Internet has been incredible for content, and mostly terrible for content providers, whose pay has plummeted as they struggle to differentiate from high quality content and be heard through the noise of mediocre content. I expect this problem to get worse, not better, as AI tools improve at mimicking human writers.</p></li></ul><p>With no further ado: this article will reflect on the most popular articles and most popular tweets of 2025, with some bonus content if you read to the end.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h1>Most Popular Articles</h1><p>The most popular article by far, fueled by a post to Hacker News, was <em><a href="https://parallelprogrammer.substack.com/p/why-we-need-simd-the-real-reason?r=4xff6v">Why We Need SIMD (The Real Reason)</a></em>, which recapitulated the history of SIMD instructions on x86 and spoke to the engineering tradeoffs that have prompted CPU vendors to pursue SIMD instuctions as a preferred mechanism to accelerate data parallel workloads.</p><p>The second most popular article was my reading list: <em><a href="https://parallelprogrammer.substack.com/p/a-reading-list-for-metalheads?r=4xff6v">Perennial Technical Reads</a></em>, a walk down memory lane invoking Fred Brooks, Jon Bentley, and others. If I were writing it today, I might have remembered to include Joel Spolsky&#8217;s <a href="https://www.joelonsoftware.com/">spicy blog</a> along with the other Internet resources mentioned.</p><p>The third most popular article was a send-up of a l33t coding question, <a href="https://parallelprogrammer.substack.com/p/3rd-largest-element-simd-edition?r=4xff6v">Third Largest Element</a>, which implemented an AVX2-accelerated solution that no reasonable interviewer possibly could expect a candidate to write in real time.</p><p>Almost as popular: An article with the enticing title This <a href="https://parallelprogrammer.substack.com/p/quantizing-to-nf4-with-avx-512?r=4xff6v">ML Workload Runs 30x Faster w AVX512</a>, an implementation of the float-to-NF4 conversion that uses permute instructions for register-to-register lookups. It also alludes to a cool optimized Binary Sort implementation that I first learned almost 40 years ago.</p><p>Understandably, none of my paid posts were as popular:</p><ul><li><p><a href="https://parallelprogrammer.substack.com/p/book-review-the-nvidia-way?r=4xff6v">Book Review: The NVIDIA Way</a>, a review of Tae Kim&#8217;s bestselling hagiography of Jensen Huang that is a fun read but an incomplete oral history of NVIDIA,</p></li><li><p><a href="https://parallelprogrammer.substack.com/p/implications-of-google-v-oracle?r=4xff6v">Implications of Google v. Oracle</a>, an article on the most consequential Supreme Court decision in software engineering history,</p></li><li><p><a href="https://parallelprogrammer.substack.com/p/standardized-or-proprietary?r=4xff6v">Standardized or Proprietary</a>, a reflection on the tradeoffs between standardized APIs designed by committees such as the OpenGL ARB (Architectural Review Board) and proprietary APIs such as Microsoft&#8217;s Direct3D and NVIDIA&#8217;s CUDA, both of which I had a central hand in designing,</p></li><li><p><a href="https://parallelprogrammer.substack.com/p/amds-gpu-software-a-software-architects?r=4xff6v">AMD&#8217;s GPU Software: A Software Architect&#8217;s Take</a>, a spicy summary of some (by no means all!) of the issues in AMD&#8217;s GPU software stack,</p></li><li><p><a href="https://parallelprogrammer.substack.com/p/a-missive-from-the-risccisc-war?r=4xff6v">Finding And Fixing Direct3D</a> and <a href="https://parallelprogrammer.substack.com/p/a-missive-from-the-risccisc-war?r=4xff6v">A Missive From The RISC/CISC War</a>, reflections on my time at Microsoft excerpted from an autobiography in progress.</p></li></ul><p>Most of my articles are free, but I&#8217;ll keep leavening with paid content to keep things interesting for my most committed subscribers.</p><h1>Most Popular Tweets</h1><p>The <a href="https://x.com/CUDAHandbook/status/1877349273588654126?s=20">runaway viral tweet of the year</a>, viewed 1.8M times, was an anecdote about how consumption of soda dropped 90% when NVIDIA started charging $0.25/soda. NOTE: this was a comment on economics and demand elasticity, not NVIDIA&#8217;s confidential business practices c. 2002!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U1QQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U1QQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png 424w, https://substackcdn.com/image/fetch/$s_!U1QQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png 848w, https://substackcdn.com/image/fetch/$s_!U1QQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png 1272w, https://substackcdn.com/image/fetch/$s_!U1QQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U1QQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png" width="885" height="551" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:551,&quot;width&quot;:885,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:123017,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/183094214?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!U1QQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png 424w, https://substackcdn.com/image/fetch/$s_!U1QQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png 848w, https://substackcdn.com/image/fetch/$s_!U1QQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png 1272w, https://substackcdn.com/image/fetch/$s_!U1QQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0b580bc-5876-4ef8-a795-70be71a7695f_885x551.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Another <a href="https://x.com/CUDAHandbook/status/1871583610035491104?s=20">notable tweet</a>, with &gt;400k views, was a pedestrian observation about NVIDIA&#8217;s software stack, one I have made many times: that because CUDA&#8217;s driver API is portable across both operating systems and CPU architectures, it enables NVIDIA to meet developers on platforms they have chosen.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GfJb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GfJb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png 424w, https://substackcdn.com/image/fetch/$s_!GfJb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png 848w, https://substackcdn.com/image/fetch/$s_!GfJb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png 1272w, https://substackcdn.com/image/fetch/$s_!GfJb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GfJb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png" width="886" height="727" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:727,&quot;width&quot;:886,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:574043,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/183094214?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GfJb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png 424w, https://substackcdn.com/image/fetch/$s_!GfJb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png 848w, https://substackcdn.com/image/fetch/$s_!GfJb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png 1272w, https://substackcdn.com/image/fetch/$s_!GfJb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbba4f37a-1348-4fad-9b16-6038da2a58ef_886x727.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A <a href="https://x.com/CUDAHandbook/status/1949419634442334504?s=20">tweet on game developers&#8217; elite status</a> on the software engineering community got quite a few views:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bhSM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bhSM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png 424w, https://substackcdn.com/image/fetch/$s_!bhSM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png 848w, https://substackcdn.com/image/fetch/$s_!bhSM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png 1272w, https://substackcdn.com/image/fetch/$s_!bhSM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bhSM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png" width="889" height="496" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/edeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:496,&quot;width&quot;:889,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:116783,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/183094214?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!bhSM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png 424w, https://substackcdn.com/image/fetch/$s_!bhSM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png 848w, https://substackcdn.com/image/fetch/$s_!bhSM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png 1272w, https://substackcdn.com/image/fetch/$s_!bhSM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedeb50be-1927-4fa2-94c8-c7cfe8add11e_889x496.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Some posts that didn&#8217;t get as much traction as I expected include <a href="https://parallelprogrammer.substack.com/p/dont-move-the-data?r=4xff6v">Don&#8217;t Move The Data!</a>, an updated version of a 2017 article on how data movement has become the limiting reagent of all compute. I may keep updating this one, because the goalposts keep moving as packaging and other technologies continue to develop at a breakneck pace.</p><h1>Conclusion and 2026</h1><p>I have been blocking accounts to curate my feed to be constructive and technical, and it has mostly worked. At its best, the Internet and platforms like Twitter serve to facilitate wholesome exchanges and opportunities for learning. </p><p>If any of the articles mentioned above sounded interesting, I&#8217;d encourage you to take a look through <a href="https://parallelprogrammer.substack.com">the archives</a>. There are articles on <a href="https://parallelprogrammer.substack.com/p/curiously-recurring-what?r=4xff6v">C++ programming idioms</a>, <a href="https://parallelprogrammer.substack.com/p/cuda-error-handling-a-definitive?r=4xff6v">CUDA programming practices</a>, <a href="https://parallelprogrammer.substack.com/p/a-paean-to-struct-based-interfaces?r=4xff6v">API design</a>, <a href="https://parallelprogrammer.substack.com/p/sram-scaling-beginning-of-the-end?r=4xff6v">the evolving technological landscape</a>, and more.</p><p>As a final note, I have all but decided to move the entire <a href="https://cudahandbook.com/">CUDA Handbook</a> text, and related content like the 2013 article on Histograms, to the Web site. Everyone seems to pirate the book anyway! AI tools seem accomplished at performing the stultifying task of converting Word text to Markdown, so look for updates as that project moves forward.</p><p></p><p></p><p></p><p></p><p></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Safety-Critical CUDA]]></title><description><![CDATA[A Preliminary Look At Axivion]]></description><link>https://parallelprogrammer.substack.com/p/safety-critical-cuda</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/safety-critical-cuda</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Wed, 17 Dec 2025 14:31:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>The first of a planned series on Qt Group&#8217;s Axivion static code analyzer.</em></p><p>A few weeks ago, the folks at <a href="https://www.qt.io">Qt Group</a> reached out to see if they could contract with me to write about the new CUDA support for <a href="https://www.qt.io/quality-assurance/axivion-for-cuda">Axivion</a>, their code analyzer tool that is designed to complement (not replace!) seasoned developers&#8217; adherence to coding standards such as <a href="https://misra.org.uk/">MISRA</a>, <a href="https://www.autosar.org/">AUTOSAR</a>, and NVIDIA&#8217;s own <a href="https://docs.nvidia.com/cuda/cuda-for-tegra-appnote/cudaguideV3.0.1.pdf">CUDA Security guidance</a>.</p><p>Code analyzers have a long history in our industry. In the 1970s, when K&amp;R&#8217;s <em>The C Programming Language</em> was new, a program called <code>lint</code> was developed by a computer scientist at Bell Labs named Stephen C. Johnson. <code>lint</code> itself is a historical curiosity, with functionality that has long since been rendered obsolete by language design (e.g., mismatched argument types) or subsumed into compilers (e.g., pedantic warnings about portability issues). But the idea persists that there&#8217;s a role to play for tools that complement compilers&#8217; core task of translating source code into machine code and, in an homage to its origins, such tools are called <em>linters</em>.</p><div><hr></div><p>My first experience with using a linter in production was at Microsoft in the 1990s, which acquired PREfast, a static code analyzer that visited much more rigorous scrutiny on the code than the compiler. Management was concerned about security vulnerabilities, especially ones caused by buffer overflow bugs, and PREfast was designed to detect such bugs through holistic analysis of the source code. A team was assigned to run the entire Windows code base through PREfast, and a bug was opened for every issue it found.</p><p>That exercise had a bigger influence on how I write code than any other single event in my career as a software developer&#8212;because some of those bugs were damn near impossible to fix without regression risk.</p><p>If a function neglected to check the return value from <em>malloc()</em>, then passed the resulting pointer into a call stack, where only some code paths dereferenced the bad pointer&#8230; PREfast would catch that bug, and outline the exact set of conditions needed for it to reproduce. But fixing the bug sometimes would require touching code in many places, often in ways that made the fix hard to verify by inspection.</p><p>The bugs opened by PREfast generally could be dropped into one of three categories:</p><ul><li><p>Most often, PREfast raised legitimate bugs that could be fixed with minimal disruption to the source code.</p></li><li><p>Occasionally, we were able to prove that PREfast had raised a spurious concern.</p></li><li><p>Every so often, we had an unsatisfying resolution: it seemed like there was a bug, but without a repro case, and if the affected code was distributed throughout source code, our attempts to &#8216;fix&#8217; the &#8216;bug&#8217; seemed just as likely to introduce regressions.</p></li></ul><p>Microsoft trusted its engineers; if the person investigating a PREfast issue attested that it had been fixed (or that it was not a bug), the bug would be closed. For bugs that were deemed spurious, there was a mechanism to mark them so PREfast would not raise those issues again. If memory serves, marking a bug that way required sign-off from multiple people, akin to a code review.</p><p>The experience of triaging, fixing, confirming the fixes for these bugs, and figuring out how to improve test coverage of the code, had a profound effect on my approach to software development. The latter two categories, in particular, motivated me to think about how I could write code that 1) does not raise spurious concerns with the static code analyzer and, more importantly, 2) enables bug fixes to be more local, e.g. by handling resource allocation failures at the call site, and propagating errors from unified code paths.</p><div><hr></div><p>As computers have become more pervasive, they increasingly have found their way into safety-critical applications. For software running on those computers, the stakes are higher and the software must be designed and built more carefully. We can&#8217;t go about building software for medical devices, airplanes, weapons systems, or automobiles in the same way that we build software for computer games or e-commerce Web sites. NASA has long adhered to coding standards and software development practices that bias strongly for robustness over speed of development. Prominent failures, such as the notorious <a href="https://www.cs.columbia.edu/~junfeng/08fa-e6998/sched/readings/therac25.pdf">Therac-25</a> radiation therapy machine<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, have motivated development of coding standards that are more formal, and have more force in effect, than the advisory coding standards that most professional software engineering organizations have internally negotiated.</p><p>In the late 1990s, the automobile industry, recognizing the inevitability of computers continuing to find more applications throughout their products, convened the Motor Industry Software Reliability Association (MISRA) to formalize coding standards. Now in its third edition, MISRA has found its way into other application domains, such as the Joint Strike Fighter (JSF) C++ Coding Standards<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. More recently, the AUTOSAR (AUTomotive Open System ARchitecture) was developed to standard software development for Electronic Control Units (ECUs), and the International Standards Organization (ISO) incorporated Product Development At The Software Level as Section 6 of the ISO 26262 standard for Functional Safety of Road Vehicles.</p><p>The simple presence of MISRA, or claimed adherence to its standards, won&#8217;t protect companies if employees fail to adhere to best practices. Around 15 years ago, as if to underscore why the automotive industry was leading the field of software engineering in developing such standards, Toyota Corporation was <a href="https://users.ece.cmu.edu/~koopman/pubs/koopman14_toyota_ua_slides.pdf">investigated</a> for UA (unintended acceleration) events that had killed an estimated 89 people in May 2010. A related class action lawsuit was <a href="https://www.nytimes.com/2012/12/27/business/toyota-settles-lawsuit-over-accelerator-recalls-impact.html">settled</a> for $1.6B in December 2012, and in 2014, Toyota was further <a href="https://www.justice.gov/archives/opa/pr/justice-department-announces-criminal-charge-against-toyota-motor-corporation-and-deferred">fined $1.2B</a> for concealing safety defects. Expert witness Michael Barr spent more than 20 months reviewing Toyota&#8217;s source code and testified about its quality, even citing an internal Toyota document that characterized it as &#8220;spaghetti code.&#8221; Barr and a NASA team that was contracted to do an evaluation both checked the code against MISRA standards and found thousands of violations. For those interested, <a href="https://www.safetyresearch.net/toyota-unintended-acceleration-and-the-big-bowl-of-spaghetti-code/">this article</a> has a (lengthy) summary as well as links to the hundreds of pages of testimony by expert witnesses Koopman and Barr.</p><p>By now, it should be clear why there is an appetite for tools such as Axivion, which analyzes source code and flags possible violations of the various safety standards, citing them by chapter and verse. They are intended to complement, not replace, seasoned engineers&#8217; judgment. Like the leaders at Microsoft who trusted their developers to triage and address the issues raised by PREfast, the architects of these coding standards recognized that it&#8217;s more realistic to be pragmatic than didactic. That said, if I were an engineering director at a company building software for safety-critical systems, I&#8217;d consider thoughtfully incorporating Axivion into the CI/CD workflow.</p><p></p><p>And as far as I know, <strong>Axivion is the first and only offering that performs this service, not only for C/C++ and C# code, but also for CUDA C++ code</strong><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. Only recently, NVIDIA published <em><a href="https://docs.nvidia.com/cuda/cuda-for-tegra-appnote/cudaguideV3.0.1.pdf">CUDA C++ Guidelines for Robust and Safety Critical Programming</a></em>, and <a href="https://www.qt.io">Qt Group</a> has incorporated the guidance from that document into their tool. Such expanded support is needed as CUDA continues to find applications in robotics, autonomous vehicles, and other safety-critical domains.</p><p>As a first exercise, the Qt folks ran the source code for <em>The CUDA Handbook</em> through their tool, and sent a report summarizing its findings. <em>The CUDA Handbook</em> isn&#8217;t intended for safety-critical applications, <em>per se</em>, but it&#8217;s an open source code base we can usefully examine to get a sense of the error reporting and how a developer would triage and address the issues raised by the tool.</p><p>In our next article, we&#8217;ll put Axivion through its paces and take a look at its findings.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Sadly, the first sentence of the linked paper &#8220;An Investigation of the Therac-25 Accidents&#8221; reads: &#8220;Computers are increasingly being introduced into safety-critical systems and, as a consequence, are involved in accidents.&#8221; That paper was published in July 1993.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>From 1987-1997, the Pentagon famously imposed a requirement that all software be written in Ada, an object-oriented programming language that had been developed specifically by the Defense Department to standardize the alphabet soup of programming languages that were in use in the 1970s. Lockheed Martin reportedly played a role in persuading the Defense Department to reconsider this policy and allow C++ development amidst development of the Joint Strike Fighter.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>For now at least, CUDA Python is not supported by any static code analyzer.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Return on Investment (c. 1993)]]></title><description><![CDATA[A Twitter convo prompts me to dig up a dimly-recalled rant by Jerry Pournelle]]></description><link>https://parallelprogrammer.substack.com/p/return-on-investment-redux</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/return-on-investment-redux</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Wed, 17 Dec 2025 14:31:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!wI0y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wI0y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wI0y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png 424w, https://substackcdn.com/image/fetch/$s_!wI0y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png 848w, https://substackcdn.com/image/fetch/$s_!wI0y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png 1272w, https://substackcdn.com/image/fetch/$s_!wI0y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wI0y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png" width="721" height="968" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:968,&quot;width&quot;:721,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:111322,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/181719534?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wI0y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png 424w, https://substackcdn.com/image/fetch/$s_!wI0y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png 848w, https://substackcdn.com/image/fetch/$s_!wI0y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png 1272w, https://substackcdn.com/image/fetch/$s_!wI0y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59e5da9d-5997-4393-bd0b-81c8a822d678_721x968.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>&#8220;Imagine if the CUDA SDK was $1k/year/seat,&#8221; the discussion began. Rhetorically, of course, since history has taught us that charging for platform support hinders adoption. <a href="https://x.com/opdroid1234">@opdroid1234</a> wondered, &#8220;i never understood Intel charging for Vtune :(", the profiling tool that made it easy to profile applications to improve their performance on x86. As shown above, the discussion reminded me of a story that Jerry Pournelle, the legendary BYTE Magazine columnist<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, had told at the height of the contest for mindshare between Window NT and OS/2.</p><p>It seems ridiculous today, but OS/2 once was considered as credible a successor to MS-DOS as Windows NT, which had originally been positioned as a <a href="https://www.itprotoday.com/it-infrastructure/windows-nt-architecture-part-1">microkernel</a> operating system for which both Windows and OS/2 would be peer client operating systems. The competition referenced by Jerry Pournelle in the September 1993 issue of BYTE Magazine was the by-product of a so-called &#8220;divorce&#8221; between the two companies that started, according to this June 1992 New York Times <a href="https://web.archive.org/web/20180303051911/https://www.nytimes.com/1992/06/28/us/ibm-and-microsoft-settle-operating-system-feud.html">article</a>, in September 1990. Surprisingly, to me at least, the break occurred amidst an <a href="https://web.archive.org/web/20050329231807/http://www.findarticles.com/p/articles/mi_m1282/is_n1_v46/ai_14809416">FTC investigation of collusion</a> between the two companies that had started in the late 1980s<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>So by 1992, with Windows NT development going ahead full steam without any collaboration with IBM, the two companies were vying for market share and developer mindshare at conferences like COMDEX. Developers and consumers alike were being asked: operating system would provide the bigger return on investment?</p><p>And that is where distant past history intersects with our present discussion. I&#8217;ll let Pournelle take it from here (<a href="http:///wp-content/uploads/2025/12/byte-1993_09-Pournelle.pdf">link to a PDF</a> of the original pages of BYTE Magazine):</p><blockquote><p>&#8220;Develop Your Device Drivers for OS/2!&#8221; proclaimed a sign at IBM&#8217;s booth. &#8220;IBM Device Driver Source Kit for OS/2 Now Available on CD!&#8221; A flier announced &#8220;Free. WIN-OS/2 source code for seamless VGA Display Device Driver when you order the DDK!&#8221; Microsoft has long used CD-ROMs to distribute their development kits, and will even get developers a discount on a CD-ROM drive.</p><p>Aha, I thought, now IBM has learned that trick. With any luck, we&#8217;ll soon see a flood of new device drivers for both OS/2 and Windows under OS/2, and maybe it won&#8217;t be long before I can hook up my Pioneer CD-ROM drive on an OS/2 system. By gum golly, it looks like IBM is doing something right.</p><p>Then I got closer, and to my horror, I saw the price. You can get the Device Driver Source Kit for only $499<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. It wasn&#8217;t clear whether that was the regular price or a show special. I went up to the young woman who was passing out the fliers.</p><p>&#8220;I thought the big problem with OS/2 was the lack of device drivers,&#8221; I said. She gave me a smug look and handed me a flier. &#8220;Yeah,&#8221; I said, &#8220;but isn&#8217;t 500 bucks a bit steep? If I go over to Microsoft and whisper about writing device drivers for NT, they&#8217;ll stuff kit disks into my briefcase.&#8221; She drew herself up to her full height and said, &#8220; Thank you very much for the information.&#8221; She was too polite to say, &#8220;We&#8217;re IBM. We don&#8217;t care. We don&#8217;t have to,&#8221; and maybe she didn&#8217;t think it, but it sure looked like she did.</p><p>I went around to some of the sharp OS/2 programmer troops and told that story. They were disturbed. They hadn&#8217;t known the price was that high. The kit, it seems, is sold by a different part of IBM. I spent an hour looking for any policy-level IBM official, or a PR officer, but I never found one to discuss this with. When I went back to the OS/2 station, the young woman, still smug, said, &#8220; Back again?&#8221; It was pretty clear she had no use for my observations, so I didn&#8217;t offer any.</p><p>I didn&#8217;t know how much Microsoft charged for their kits, so I went over to find out. I&#8217;d no more than set foot on their carpet when one of the PR people recognized me<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a> and asked if she could help. &#8220;If I wanted to write NT device drivers, what should I do?&#8221; I asked. &#8220; You&#8217; re in the wrong booth,&#8221; she said. &#8220;Let&#8217;s go over to the NT booth. I don&#8217;t know anything about NT, but we&#8217;ll find someone.&#8221;</p><p>We walked over-no question that Microsoft&#8217;s PR people knew where each and every Microsoft booth and display was and in 7 minutes-I timed it-I was talking to one of the NT product managers. &#8220;Well, first you need the developer&#8217;s kit, with the compilers and source code and stuff,&#8221; he said. &#8220;That&#8217;s on a CD-ROM for $69. Then you get the DDK. It&#8217;s another CD-ROM, same price.&#8221;</p><p>&#8220;Little steep, isn&#8217;t it?&#8221; I said. &#8220;A little,&#8221; he said, &#8220;but there&#8217;s a lot there, all the sources we could find, not just ones we wrote but any we could talk people out of. Want a copy?&#8221; I think I just may have located the reason why there&#8217;s a shortage of OS/2 device drivers, and why there probably won&#8217;t be a shortage of NT drivers.</p></blockquote><p>OS/2 was not a market success, and is not a viable platform today, in part because it did not deliver a high enough return on investment. Back then, the return came in the forms of application and, as Pournelle underscores above, hardware support; the investment required modest purchases of machines to develop the software, attending conferences, and learning how to use the tools and APIs. The smaller that denominator (that is, the lower the barrier to entry for hardware and software developers), the larger the return on investment. OS/2 made its mark on history, and the competitive pressures it exerted did influence operating system technology, but history will record it as a platform that failed in the marketplace.</p><p>There are notable examples of successful platforms with a high barrier to entry. I plan to write about those sometime soon.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Pournelle, of course, was much more than that, not least a Nebula-nominated novelist most famous for his collaborations with Larry Niven.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>The FTC was particularly interested in whether Microsoft was able to parlay early knowledge of its operating system into competitive advantages in the applications market for that same operating system.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>More than $1,100 in 2025 dollars.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Waxing delighted at being recognized, and getting special treatment, is vintage Pournelle. Later in the same column: &#8220;When I got home [from the conference], one of the first things to arrive on my doorstep was an Airborne Express package from Microsoft containing both kits on CD-ROM. I hadn&#8217;t even asked for them.&#8221;</p></div></div>]]></content:encoded></item><item><title><![CDATA[Curiously Recurring What?]]></title><description><![CDATA[A 30+-year-old C++ Idiom Is Still Useful Today]]></description><link>https://parallelprogrammer.substack.com/p/curiously-recurring-what</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/curiously-recurring-what</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Fri, 12 Dec 2025 14:30:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over dinner a few months ago, Chris Kitching, the brilliant CTO of Spectral Compute whose <a href="https://scale-lang.com/">SCALE compiler</a> can lower CUDA source code onto AMD GPUs, made an offhand comment about the <a href="https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern">&#8220;curiously recurring template pattern&#8221; (CRTP)</a> and how it can be used to achieve polymorphism without incurring the performance cost of indirecting through a vtable.</p><p>I thought of the clown car full of <a href="https://parallelprogrammer.substack.com/p/fun-new-project-itch-order-simd">limit order book implementations</a> I had lying around; some AVX2, some AVX512, some using different memory layouts, some using different metadata to accelerate key cases. But the various implementations were resident in separate Git branches. It would have been easy to replace the various methods in the <code>order_book</code> class with virtual functions, and indirect through them to test the various implementations; but that would have incurred a Draconian performance hit, and the point of the exercise had been to explore the design space and find the fastest combination. Giving up performance by going across an unnecessary procedure call<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> was unacceptable.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>So I took an action to explore CRTP as a possible solution to this problem: &#8220;flatten&#8221; the source code so scalar, AVX2, and AVX-512 code can coexist and be compared side-to-side in the code base, without compromising performance. Tentatively, it&#8217;s working out really well, although I can&#8217;t yet share the final AVX-512 implementation. I thought now would be a good time to go over CRTP and why it was such a good fit for this exercise of exploring different CLOB implementations.</p><h1>Whither CRTP?</h1><p>You may be wondering: what the heck <strong>is</strong> CRTP? In this case, the <a href="https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern">Wikipedia article</a> has an excellent article on the topic, including the history that the term was invented by James Coplien in his book, <em>Advanced C++ Programming Styles and Idioms</em> (Copyright 1992). Sean McBride&#8217;s <a href="https://www.linkedin.com/pulse/review-advanced-c-1992-james-coplien-sean-mcbride-xulee/">recent (June 2025) review</a> of the book characterized Coplien&#8217;s book as &#8220;the most difficult C++ book I&#8217;ve read,&#8221; and having read and struggled with it 30 years ago, I share that sentiment. CRTP is such a counterintuitive idiom that the Wikipedia article straightfacedly recounts how Microsoft developer Christian Beaumont &#8220;initially thought it could not compile in the Microsoft compiler available at the time&#8221;:</p><blockquote><p>The Microsoft Implementation of CRTP in Active Template Library (ATL) was independently discovered, also in 1995, by Jan Falkin, who accidentally derived a base class from a derived class. Christian Beaumont first saw Falkin&#8217;s code and initially thought it could not compile in the Microsoft compiler available at the time. Following the revelation that it did work, Beaumont based the entire ATL and Windows Template Library (WTL) design on this mistake.</p></blockquote><p>Formally known as &#8220;F-bounded quantification,&#8221; CRTP entails having a class derive from a class template instantiation <em><strong>using itself as template argument</strong></em>. The base class can then define static functions that invoke member functions of the derived class; when the compiler is given all these class definitions, plus code that uses the static functions, <em><strong>it has all the information needed to compile the code knowing the type of the derived class.</strong></em> Code that creates and uses instances of the derived classes then transparently benefit from this form of static polymorphism, without even knowing about the enabling linguistic legerdemain.</p><h1>Backgrounder: Runtime Polymorphism</h1><p>It may be helpful to give a concrete example, starting with a more straightforward implementation of polymorphism. (You might say as the language designers intended.)</p><p>In C++, an <em>abstract base class</em> defines <em>pure virtual functions</em> that specify operations that may be performed on the object, but defer implementation onto their derived classes; in fact, unlike (impure?) virtual functions, derived classes <em>must</em> provide implementations of their parent classes&#8217; pure virtual functions.</p><p>The <a href="https://raytracing.github.io/">Ray Tracing In One Weekend series</a> (RTOW), whose <a href="https://github.com/RayTracing/raytracing.github.io">sample source code</a> is available on GitHub, has a good example of this traditional type of polymorphism.</p><p>If you don&#8217;t know what ray tracing is, but like to program and enjoy pretty pictures, it may be worth checking out RTOW. Ray tracing is a brute force method of rendering that creates images by modeling the behavior of light rays in the environment. A key abstraction at the center of ray tracing is the ray intersection calculation that determines whether a ray of light intersects with an object and, if the intersection occurs, computes the location of that intersection. To keep the separation of concerns clean between the casting of rays and intersecting those rays with objects, the RTOW code defines an abstract base class <code>hittable</code>, with derived class <code>sphere</code>:<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><pre><code>class hittable {
public:
virtual ~hittable() = default;
virtual bool hit(const ray&amp; r) const = 0;
};

class sphere : public hittable {
sphere(const point3&amp; center, double radius, shared_ptr&lt;material&gt; mat)
: center(center), radius(std::fmax(0,radius)), mat(mat) {}
bool hit(const ray&amp; r) const override {
&#8230;
}
};

class triangle : public hittable {
triangle(const point3 vertices[3]): &#8230;
bool hit(const ray&amp; r, interval ray_t, hit_record&amp; rec) const override {

&#8230; // ray-triangle intersection routine
}
};</code></pre><p>When a programmer using this class hierarchy creates an instance of <code>sphere</code>, it also is an instance of <code>hittable</code>; and the code can operate on <code>hittable</code> objects without knowing whether they are spheres or triangles. This function, for example, returns the objects that a ray hit:</p><pre><code>std::vector&lt;hittable *&gt;
return_hits( const ray&amp; r, const std::vector&lt;hittable *&gt; objects ) {
    std::vector&lt;hittable *&gt; ret;
    for ( auto obj : objects ) {
        if ( obj-&gt;hit( ray ) ) {
            ret.emplace_back( obj );
        }
    }
}</code></pre><p>This polymorphism is enabled by a <em>virtual table</em>, also known as a vtable or vtbl: an array of pointers-to-function, one per class, that the compiler creates behind the scenes. When you call <em>obj-&gt;hit()</em>, the compiler uses the pointer to <code>obj</code> to find the vtbl and call the corresponding function.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a></p><p>This type of runtime polymorphism &#8211; where clients interact with object strictly through interfaces, with no knowledge of their internal representation<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a> - is the bedrock underpinning many important frameworks and technologies, from native language support (e.g., Java interfaces) to technologies like Microsoft&#8217;s Common Object Model (COM), which enables language-independent API deployment, to the Service Oriented Architecture (SOA) that Amazon embraced to improve on the inefficiencies of what had been a huge, monolithic code base<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>.</p><p>But the pattern depicted above &#8211; where the <code>sphere</code> and <code>triangle</code> classes inherit from, so have an IS-A relationship with <code>hittable</code> (i.e., <code>sphere *</code> and <code>triangle *</code> both are hittable *) &#8211; is hard to achieve with CRTP. Chris gave a concise example showing how, which involves using the C++17 type trait <code>std::is_base_of</code>:<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a></p><pre><code>// Boring, Java-esque OOP stuff         // CRTP equivalent
                                        template&lt;typename T&gt;
class Shape {};                         class Shape : public T {};

class Polygon : public Shape {};        template&lt;typename T&gt;
                                        class Polygon : public Shape&lt;T&gt; {};

class Circle : public Shape {};         template&lt;typename T&gt;
// These &#8220;are&#8221; polygons, of course.     class Circle : public Shape&lt;T&gt; {};

class Square : public Polygon {};       // These &#8220;are&#8221; polygons, of course.
class Pentagon : public Polygon {};     template&lt;typename T&gt;
                                        class Square : public Polygon&lt;T&gt; {};
                                        class Pentagon : public Polygon&lt;T&gt; {};

// The generic one.                     // The generic one.
void doSomething(Shape&amp; foo);           template&lt;typename T&gt;
                                        void doSomething(T&amp; foo);

// The special overload for polygons.   // The special overload for polygons.
void doSomething(Polygon&amp; foo);         template&lt;typename T&gt;
                                        requires(std::is_base_of_v&lt;Polygon&lt;T&gt;, T&gt;)
                                        void doSomething(T&amp; foo);
</code></pre><p>In Chris&#8217;s words: &#8220;The key point is: notice how we can overload <code>doSomething</code> for the category of classes &#8220;Polygon&#8221;, even though we used CRTP!&#8221;</p><p>For our purposes, CRTP is preferable because although we want to explore the design space of different instruction sets, memory layouts, and implementation strategies, we do not want to give up any performance by calling across virtual function boundaries. We could mark the derived classes as <code>final</code>, which empowers the compiler to bypass the vbtl call when it can prove that an object is of the expected type; but CRTP is a more certain path.</p><p>So how does CRTP work? Our usage, for static polymorphism, will echo this example from the Wikipedia article:</p><pre><code>template &lt;typename T&gt;
struct Base {
    void call() {
        // ...
        static_cast&lt;T*&gt;(this)-&gt;implementation();
        // ...
    }

    static void staticFunc() {
        // ...
        T::staticSubFunc();
        // ...
    }
};</code></pre><p>Consider the <code>order_book</code> class in our sample code. The class, as published by Charles Cooper, already was arranged in a way that was amenable to refactoring for CRTP, with static functions such as <code>add_order</code> calling member functions such as <code>ADD_ORDER</code>:</p><pre><code>static void add_order(order_id_t const oid, book_id_t const book_idx,
sprice_t const price, qty_t const qty)
{
#if TRACE
printf(&#8221;ADD %lu, %u, %d, %u&#8221;, oid, book_idx, price, qty);
#endif // TRACE
    ...
}

void ADD_ORDER(order_t *order, sprice_t const price, qty_t const qty)
{
    ...
}</code></pre><p><code>add_order</code> looks up the correct book (each &#8216;symbol&#8217; in the exchange - MSFT for Microsoft, for example - has its own instance of <code>order_book</code>), and dispatches the order by calling that book&#8217;s <code>ADD_ORDER</code> method.</p><p>Let&#8217;s say we are exploring the tradeoffs between AOS (array of structures) and SOA (structure of arrays) memory layouts of the limit order book data. Using CRTP, we can templatize the <code>order_book</code> class, then create derived classes <code>order_book_scalar</code> (the original scalar implementation, which uses the AOS layout and will serve as our canonical test reference) and <code>order_book_soa</code>, which rearranges the price levels to be a tuple of <code>std::vector</code> instead of a <code>std::vector</code> of tuples.</p><p>The reason we are experimenting with SOA is because it&#8217;s needed for optimal SIMD performance. I&#8217;ll explain in a later post, when we look at the AVX implementations of <code>order_book</code>.</p><p>For the CRTP refactoring, we also need to move some of the <code>order_book</code> members to the derived class &#8211; namely the <code>m_bids</code> and <code>m_asks</code> members. These are the per-book snapshots of the aggregate orders at each price level.</p><pre><code>static constexpr size_t MAX_BOOKS = 1 &lt;&lt; 14;
static constexpr size_t NUM_LEVELS = 1 &lt;&lt; 20;
static order_book *s_books; // can we allocate this on the stack?
static oidmap&lt;order_t&gt; oid_map;
using level_vector = pool&lt;level, level_id_t, NUM_LEVELS&gt;;
using sorted_levels_t = std::vector&lt;price_level&gt;;

// A global allocator for all the price levels allocated by all the books.
<s>static level_vector s_levels;
sorted_levels_t m_bids;</s>
<s>sorted_levels_t m_offers;</s>
using level_ptr_t = level_vector::__ptr;</code></pre><p>The CRTP changes entail templatizing <code>order_book</code>:</p><pre><code>template&lt;typename Derived, LAYOUT layout, TARGET_ISA isa, bool TRACE = false&gt;
class order_book
{
    ...
};</code></pre><p>and creating derived class <code>order_book_scalar</code>:</p><pre><code>class order_book_scalar : public order_book&lt;order_book_scalar, LAYOUT::ARRAY_OF_STRUCTS, TARGET_ISA::GENERIC_C&gt;
{
};</code></pre><p>The &#8220;curiously recurring&#8221; aspect of this inheritance structure is that <code>order_book_scalar</code> <em><strong>inherits from itself</strong></em>.</p><p>Note that we took this opportunity to templatize the TRACE compile-time option (it had been a preprocessor macro) and the memory layout and ISA (instruction set architecture), to future-proof this class hierarchy to experimenting with SOA memory layouts and AVX2 and AVX-512 implementations of the limit order book.</p><p>We moved the <code>m_bids/m_asks</code> members and the <code>ADD_ORDER</code> method from <code>order_book</code> to <code>order_book_scalar</code>, and modified the static function <code>order_book::add_order</code> to specifically reference the <code>Derived</code> class:</p><pre><code><s>s_books[size_t(book_idx)].ADD_ORDER(order, price, qty);</s>
static_cast&lt;Derived *&gt;(&amp;s_books[size_t(order-&gt;book_idx)])-&gt;ADD_ORDER(order, price, qty);</code></pre><p>The client code also needs to be refactored, and here, the refactoring of <code>main.cpp</code> had the beneficial side effect of moving almost all of <em>main()</em> into a templatized function to test and measure the performance of an <code>order_book</code> implementation. We introduced a new function template <em>timeBacktest()</em>:</p><pre><code>template&lt;typename T&gt;
double
timeBacktest( const std::string filename )
{
   ...
}</code></pre><p>that handles the <code>ADD_ORDER</code> message from the exchange by specifically referencing the template parameter <code>T</code>:</p><pre><code>case (itch_t::ADD_ORDER): {
    ...
    T::add_order(order_id_t(pkt.oid), book_id_t(pkt.stock_locate), ... );
    break;
}</code></pre><p><em>main()</em> then can invoke <em>timeBacktest()</em> with the <code>order_book</code> class instantiation to test:</p><pre><code>timeBacktest&lt;order_book_scalar&gt;( filename );</code></pre><p>Later refactorings introduce an SOA implementation of <code>order_book</code>, a cross-check function to make sure our speculative CLOB implementations actually work, and command-line options to invoke the full Cartesian product of possible combinations of template parameters.</p><p>Before, I had separate GitHub branches with AVX2 and AVX-512 implementations, with various heuristics and metadata controlled by preprocessor constructs. I had to check out a specific branch and configure the preprocessor macros to test a given combination. Using CRTP, we&#8217;ll be able to flatten the code base so we can do side-by-side comparisons of the various implementations&#8217; source code &#8211; and their performance.</p><p>Thanks again to Chris Kitching of Spectral Compute for putting me onto the possibilities of this refactor!</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Function calls are inexpensive on modern CPUs, but they hinder compilers' ability to generate optimal code, not least because the ABI requires most registers to be preserved across the function call.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>To keep things simple, I&#8217;ve taken some liberties with the RTOW code. The <code>hit</code> method in their implementation returns information on <em>where</em> the ray and object intersected, as well as <em>whether</em> they intersected. Also, I invented the <code>triangle</code> class for illustrative purposes.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p> Developers can authorize the compiler to bypass the virtual function call when possible by making the class <code>final</code>, inhibiting inheritance from that class (and further overloading of the class&#8217;s virtual functions).</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>In reviewing a draft of this article, Chris pointed out that we&#8217;re singling out the one thing that CRTP can&#8217;t do: type erasure. Without additional information that isn&#8217;t always available at compile time, code that operates on the less-derived class cannot be compiled into code that calls the more-derived implementation.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>Around 2002, Jeff Bezos wrote an internal memo that exhorted everyone at Amazon to embrace interfaces and to avoid all alternative forms of IPC (interprocess communication). Famously, the memo ended with a threat to fire anyone who failed to comply with the mandate!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>For clarity, sample virtual functions have been entirely omitted from this side-by-side comparison.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[SRAM Scaling: Beginning of the End?]]></title><description><![CDATA[The impending crisis in hardware design that no one is talking about]]></description><link>https://parallelprogrammer.substack.com/p/sram-scaling-beginning-of-the-end</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/sram-scaling-beginning-of-the-end</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Wed, 26 Nov 2025 14:30:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Moore&#8217;s Law, like Amdahl&#8217;s Law, always has been more of an observation than a law. It stemmed from Gordon Moore, co-founder of Intel, observing in 1965 that with process improvements, transistor density was doubling every year. Later, around 1975, he revised that estimate downward to a doubling every 2 years&#8212;still a prodigious growth rate. And ever since, there has been a drumbeat of predictions that Moore&#8217;s Law would come to an end, and ruminations on what would come next after that happened.</p><p>Some perceived barriers, like the 1-micron barrier (or in today&#8217;s parlance, the 1,000-nm barrier) turned out essentially to be psychological, like the 4-minute mile. The perception that Moore&#8217;s Law might come to an unwelcome end has motivated investments in alternative technologies, like <a href="https://www.scientificamerican.com/article/what-are-josephson-juncti/">Josephson junctions</a> or gallium arsenide as a substrate, for decades.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>For the last few years, as process improvements continued to roll out (albeit more unevenly than they did in the halcyon 1990s), an alarming trend started to be observed: logic was benefiting markedly more from improved scaling than <a href="https://semiengineering.com/knowledge_centers/memory/volatile-memory/static-random-access-memory/">SRAM</a>. In December 2022, Tom&#8217;s Hardware published <a href="https://www.tomshardware.com/news/no-sram-scaling-implies-on-more-expensive-cpus-and-gpus">&#8220;TSMC&#8217;s 3nm Node: No SRAM Scaling Implies More Expensive CPUs and GPUs,&#8221;</a> and in February 2024, semiengineering.com published: <a href="https://semiengineering.com/sram-scaling-issues-and-what-comes-next/">&#8220;SRAM Scaling Issues, And What Comes Next.&#8221;</a></p><p>Why should we care? What is SRAM to you and me?</p><p>Well, for more than 30 years, the majority of the transistor budget allocated to CPUs has been SRAM in the form of caches. Modern CPUs have dozens of cores and each core has a very fast L1 (usually 64K each for code and data) and a moderately fast L2; the L3, or &#8220;LLC&#8221; (last level cache), or &#8220;uncore,&#8221; consists of an L3 cache that arbitrates external memory traffic, be it across the cache coherency interconnect or the integrated memory controllers on the CPU die. In short, the caches are comprised of SRAM and are designed to make memory seem faster (lower latency). SRAM has been such a dominant percentage of CPUs&#8217; transistor budgets for the last 30 years that even significant instruction set improvements such as MMX (52 new instructions, c. 1996) and x64 (c. 2002) cost less than 10% total die area.</p><p>We&#8217;ve had scares like this before. A little over 20 years ago, a big inflection point in the history of Moore&#8217;s Law occurred: the end of Dennard scaling, where improvements in transistor density also delivered improvements in clock rates. The 1990s had been a decade of free beer, with Intel and AMD leading the charge from 25 MHz 80486 chips to 1,000-MHz chips by the end of the decade. In 2002, then-Intel VP Pat Gelsinger observed that it was becoming infeasible to parlay improvements in density into higher clock rates, and predicted multicore CPUs as a likely answer to the questions posed by this technical challenge. Intel introduced its first multicore in the Pentium Duo (c. 2006), and today, modern CPUs have many dozens of cores and peak performance is impossible to attain without multithreading.</p><p>But with benefit of hindsight, we now see that the primary beneficiary of the end of Dennard scaling was not any CPU vendor, but NVIDIA, with its GPUs and the CUDA technology underpinning their AI hardware business. NVIDIA GPUs, the story went, were designed for throughput, not latency; so they were able to target lower clock rates and, instead of caches designed to reduce the effective latency of memory, the SRAM in GPUs is allocated to:</p><ul><li><p>The humongous register file (largest memory on the chip), SRAM that helps cover memory latencies and instruction latencies via thread level parallelism (TLP);</p></li><li><p>Shared memory, a software-managed cache, occupies a significant portion of each Streaming Multiprocessor (SM), of which there are 140 on a modern GPU;</p></li><li><p>The L1 caches for each SM, which typically are aliased on to the shared memory (and the proportion of L1/shared memory is under software control); and</p></li><li><p>the L2 cache, dozens of megabytes, is yet more SRAM.</p></li></ul><p>The design of each of these hardware units reflects the different types of memory traffic serviced by the SRAM.</p><p>But the point is, the transistor budgets of both CPUs and GPUs is dominated by SRAM.</p><p>And for a scary few years, SRAM did not seem to benefiting from scaling to the same degree as logic. Quoting from the <a href="https://semiengineering.com/sram-scaling-issues-and-what-comes-next/">above-linked semiengineering.com article</a>:</p><blockquote><p>&#8220;In traditional scaling of planar devices, gate length and gate oxide thickness were scaled down together to improve performance and control of the short-channel effect. A thinner oxide enabled the performance gain in lower VDD level, which is advantageous for SRAM in reducing both leakage and dynamic power,&#8221; said Jongsin Yun, memory technologist at <a href="https://semiengineering.com/entities/mentor-a-siemens-business/">Siemens EDA</a>. &#8220;However, in recent technology node migrations, we&#8217;ve barely seen further scaling oxide or VDD levels. Moreover, the geometric shrinkage of transistors results in thinner metal interconnects, leading to increased parasitic resistance and, consequently, more power loss, and RC delay. As AI design increasingly demands more internal memory access, it has become a significant challenge for SRAM to further scale its power and performance benefits in technology node migration.&#8221;</p></blockquote><p>What does this mean?</p><p>It&#8217;s as big a question for hardware and software designers as the end of Dennard scaling, or the end of Moore&#8217;s Law itself. One of the more obvious consequences is that, coupled with advances in packaging technology, chiplets that are especially RAM-intensive (e.g., I/O dies with cache) can target less-dense, more-economical fabrication processes. As the article mentions, AMD&#8217;s 3D V-Cache technology &#8220;allows additional SRAM cache memory to be stacked on top of processors, increasing the amount of cache available to the processor cores.&#8221;</p><p>A less-obvious innovation would be to trade compute for effective bandwidth; can tightly-integrated compression technology reduce the SRAM footprint needed to service a given memory traffic load?</p><p>Of course, the staggering investments in continued advancement of semiconductor fabrication technology, which have fueled that ongoing innovation, may forestall the apocalyptic scenario.</p><blockquote><p>TSMC is hiring more memory designers to improve SRAM density, but whether they can eke more out of SRAM remains to be seen. &#8220;Sometimes you can make things better by applying more people, but only up to a point,&#8221; said Tate. &#8220;Over time, customers will need to think about architectures that don&#8217;t use SRAM as intensively as they do now.&#8221;</p></blockquote><p>And tentatively, TSMC&#8217;s investments may have paid off: the February 15, 2024 article is followed by the November 2024 <a href="https://www.tomshardware.com/tech-industry/sram-scaling-isnt-dead-after-all-tsmcs-2nm-process-tech-claims-major-improvements">&#8220;SRAM Scaling Isn&#8217;t Dead After All.&#8221;</a> All it will cost you is the very latest and most expensive TSMC process!</p><p>Moore&#8217;s Law is bound to run out sometime. To be honest, it has outlasted everyone&#8217;s reasonable expectation! We keep getting previews of what it might be like, and having to design hardware (and, sometimes, the software that runs on it) accordingly. Without impending physical laws to constrain and inform our designs, what would we do to keep our industry fun and exciting?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Perennial Technical Reads]]></title><description><![CDATA[Books, articles, and blogs I keep returning to]]></description><link>https://parallelprogrammer.substack.com/p/a-reading-list-for-metalheads</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/a-reading-list-for-metalheads</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Wed, 19 Nov 2025 14:23:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have been programming computers professionally since 1982, and on the way, have read huge numbers of books, articles (both academic and trade press), blogs, and other materials. There is a lot of great material out there!<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><p>At the same time, some content distinguishes itself because I find myself referencing it over, and over and over across the years. The small list I&#8217;ve compiled reflects how I have spent most of my career twiddling bits in user mode.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Without further ado, and in no particular order, here&#8217;s a list of resources that have been on my shelves, serving me well, in some cases for decades.</p><h1>Textbooks</h1><p><em><a href="https://www.amazon.com/Introduction-Algorithms-fourth-Thomas-Cormen/dp/026204630X">Introduction to Algorithms</a></em><a href="https://www.amazon.com/Introduction-Algorithms-fourth-Thomas-Cormen/dp/026204630X">, 4th ed.</a> by Cormen, Leiserson, Rivest and Stein.</p><p>Now in its fourth edition, this book has been the best text on the subject since it came out in 1990, just in time for the undergraduate course in algorithms I took as a sophomore. Since the book was first published, algorithm design necessarily has evolved to center parallel architectures to a greater degree; and these days, memory operations matter a lot more than the number of instructions executed. But the math  and the tools described to prove algorithms correct, and analyze their performance, are timeless. The chapters naturally progress in complexity, from simple sorting algorithms and data structures such as binary trees, to chapters on Dynamic Programming, Greedy Algorithms, and Amortized Analysis, to advanced topics where they really can only give an overview and point the reader at resources.</p><p>The only nit I can pick with this book is that early editions had a chapter on parallel algorithms that covered PRAM, a good fit for massively parallel architectures like GPUs; but it has since been replaced by a chapter on threading, perhaps due to co-author Charles Leiserson&#8217;s affinity for the Cilk programming language. But parallel programming is such a broad topic that it could only get an introductory chapter with references, anyway.</p><p>This book is a gem.</p><div><hr></div><p><em><a href="https://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880">Programming Pearls</a></em><a href="https://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880">, 2nd ed.</a> by John Bentley</p><p>Back in the 1980s, CMU professor Jon Bentley wrote a monthly column for <em>Communications of the ACM</em> called &#8220;Programming Pearls,&#8221; and this book is a compendium of the best of those columns. As a freshman at Duke University (c. 1988), this book was on the must-buy list for the mandatory intro-to-programming course, even though it did not figure into the curriculum as far as I could tell. I was immediately hooked: he talks about everything from DSLs (though he calls them &#8220;little languages&#8221;) to binary heaps (though he just calls them &#8220;heaps&#8221;). Bentley&#8217;s evident love for the craft of programming, coupled with clear explanations in an approachable style, still serve as an inspiration.</p><div><hr></div><p><em><a href="https://www.amazon.com/Hackers-Delight-2nd-Henry-Warren/dp/0321842685">Hacker&#8217;s Delight</a></em><a href="https://www.amazon.com/Hackers-Delight-2nd-Henry-Warren/dp/0321842685">, 2nd ed. </a>by Henry S. Warren</p><p>With a title like that, who can resist? This book is the densest collection of bit twiddling recipes you can find anywhere. Whether you need a branchless conditional negate, need to figure out how to enlist floating point hardware to find the most significant set bit, or want to understand Gray codes or why integer division is so damn difficult, this book&#8217;s got your back! It&#8217;s well-organized, but if you are anything like me, you may lose yourself just perusing it, then going back to find the little kernels of wisdom when needed.</p><p>Some of the tricks described in this book have been subsumed into hardware (his panoply of population count algorithms has been obsoleted on some platforms by native machine instructions), and compilers have improved at making it unnecessary to spell out some of the optimizations (<em>see</em> &#8216;branchless conditional negate&#8217; above) - largely because compiler writers have spent the last few decades poring over this book and figuring out ways to translate code to use the idioms described here.</p><p>By the way, if you enjoy Hacker&#8217;s Delight, you need to know about the <a href="https://graphics.stanford.edu/~seander/bithacks.html">Stanford bit twiddling hacks</a> maintained by Sean Eron Anderson.</p><div><hr></div><p><em><a href="https://www.amazon.com/Mythical-Man-Month-Software-Engineering-Anniversary/dp/B0DKZGPNPR">The Mythical Man-Month</a></em>, by Fred Brooks</p><p>This oft-cited text on software engineering is worth a re-read every couple of years. If your first thought on hearing the title is, <em>Oh that&#8217;s the one where he discovered that doubling the size of a software engineering team causes schedules to slip, because of communications overhead</em>&#8230; drop everything, buy a copy if you don&#8217;t already have one, and reread this book. Every time I read it, I&#8217;m struck anew by some durable truth Brooks articulates, and I think, should we be struck at how <em>much</em> software engineering has changed in the last fifty years, or how <em>little</em>? Reading this book will incline you to the latter. No one was talking about &#8220;scrums&#8221; or &#8220;agile programming&#8221; when Brooks wrote this book, but many key insights he shares still hold true today. His observation that some software engineers are substantially more productive (one or two orders of magnitude) than average (we&#8217;ve all seen this) is immediately followed by the observation that there just aren&#8217;t enough such engineers to do all the required work. In his discussion of roles, from QA to software architects to language lawyers (and he uses that term), is as relevant today as it was back then.</p><p>Come to think of it, I have this book on my Kindle and it has been a while since I reread it<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>.</p><h1>Performance Aides</h1><p><em><a href="https://pages.cs.wisc.edu/~markhill/papers/primer2020_2nd_edition.pdf">A Primer on Memory Consistency and Cache Coherence</a></em>, by Vijay Nagarajan, Daniel J. Sorin, Mark D. Hill, and David A. Wood</p><p>When the first edition of this novella-sized treatise was published, John Montrym handed it to me and said: &#8220;Read. This.&#8221; Montrym was a legendary GPU architect <em>before</em> he joined NVIDIA in the late 1990s, and he&#8217;s as warm and generous with his time as he is brilliant; so if he tells you to read something, you drop everything and comply.</p><p>This masterwork is required reading for anyone interested in parallel computing architectures, whether they be multicore CPUs, multi-socket servers, GPUs, or modern servers that feature both. It starts by defining Consistency and Coherence, their motivations, scalability problems that have arisen over the years, and solutions to those scalability problems. It includes a detailed description of TSO/x86, Intel&#8217;s &#8220;total store ordering&#8221; memory consistency model that was esoteric enough that it was nothing more than poorly-documented tribal knowledge before the Linux kernel team considered an optimization of their spin lock. According to <a href="https://jakob.engbloms.se/archives/1435">this account</a> (which has reference links):</p><blockquote><p>Various experts went back and forth over whether the final MOV that sets a lock variable to 1 needed to be prefixed by LOCK or not. The discussion ended when Linus Torvalds said &#8220;I know that it is needed&#8221;. Only to see an Intel architect finally intervene and say &#8220;you know, really, it isn&#8217;t needed&#8221;. This was followed by a series of releases of Intel manuals documenting the x86 memory model, with increasing precision in each release. Intel also actually changed the published rules along the road, withdrawing some optimizations as they realized that they would break existing software.</p></blockquote><p>The Primer is now in its second edition, which added a chapter on &#8220;accelerators&#8221; (mostly GPUs). It includes ample references for you to chase down for further study.</p><div><hr></div><p><em><a href="https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html">Intel Intrinsics Guide</a></em>, Intel Corporation.</p><p>This resource is for every developer interested in accessing esoteric x86 instructions - especially SIMD instructions - unless you are a masochist, or have a solid engineering reason to bypass the compiler&#8217;s ability to use intrinsics to automate register allocation and instruction scheduling.</p><p>These days, writing SIMD code for x86 chips is akin to writing a legal brief: extensive research is needed, in part because there are annoying orthogonality misses in the ways the instructions were implemented. You have to confirm that the instruction set actually supports a feature (for example: until AVX512, <a href="https://stackoverflow.com/questions/41144668/how-to-efficiently-perform-double-int64-conversions-with-sse-avx">there was no SIMD double-to-int64 conversion instruction</a>. You could only do one of those at a time!), and that it&#8217;s supported across the full SIMD width of the instruction set.</p><p>The Intrinsics Guide lets you filter by ISA and search for either instruction mnemonics or intrinsic names, and once you find them, it has details on exactly how the instruction behaves, which Intel chips implemented it, which header file to include, and performance guidance (latency in clocks and throughput in CPI (clocks per instruction).</p><div><hr></div><p><em><a href="https://agner.org/optimize/">Optimization Manuals</a></em>, Agner Fog. </p><p>If his <a href="https://agner.org/">Web site</a> is any indication, Agner Fog is an accomplished Renaissance man; but for me, his manuals on x86 microarchitecture and x86 instruction tables are worth their weight in platinum. </p><p>To convey the scope of achievement encapsulated in these works, consider the microarchitecture manual is copyrighted 1996-2025. (It was last updated in September!) Execution pipelines, micro-op caches, instruction decoding, branch prediction, cache architectures, partial register access, store forwarding&#8230; these are just a few of the details of CPU implementation you can glean from these pages.</p><h1>Historical References</h1><p><em><a href="https://pharr.org/matt/blog/2018/04/18/ispc-origins">The Story of ISPC</a></em>, by Matt Pharr.</p><p>ISPC (&#8220;Implicit SPMD Program Compiler&#8221;) is a toolchain designed to simplify SIMD instruction set programming: in contrast with vectorizing compilers, it strikes a more natural balance between enabling programmers to express their intent, and the compiler&#8217;s ability to generate the corresponding code. It hasn&#8217;t achieved as much adoption as I expected, though it has notched some significant wins of mindshare: DreamWorks has open-sourced their <a href="https://github.com/dreamworksanimation/openmoonray">MoonRay</a> renderer that uses ISPC.</p><p>Computer graphics nerds know Matt Pharr for his masterful book <a href="https://www.amazon.com/Physically-Based-Rendering-fourth-Implementation/dp/0262048027">Physically Based Rendering</a>, now in its fourth edition; but in ISPC, he also created one of the only viable ways to program SIMD instruction sets <em>with</em> human-readable syntax and <em>without</em> intrinsics<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><p>To me, this blog series is an important historical reference on the early GPU computing landscape, when CUDA was new. Starting with the early history of Larrabee (Intel&#8217;s manycore entree into the parallel computing competition of the late 2000s), Pharr details how his experiments with vectorizing compiler technology led to what we now know as ISPC, and how ISPC came to be open sourced by Intel. There are some interesting stories in there.</p><div><hr></div><p><a href="https://www.sigmicro.org/media/oralhistories/colwell.pdf">Oral history of Intel</a>, by Bob Colwell</p><p>Bob Colwell was a CPU architect at Intel, starting in 1990, and worked on the Pentium Pro, the first CPU to implement a 4/1/1 decoder for the x86 instruction set and one that substantially improved on the Pentium&#8217;s limited superscalar features. He was the chief architect for the Pentium 4 and <a href="https://www.tomshardware.com/pc-components/cpus/former-intel-cpu-details-how-internal-x86-64-efforts-were-suppressed-prior-to-amd64s-success">reportedly included 64-bit features in that chip which then were disabled</a>.</p><p>This oral history is the size of a medium-sized book. Where the discussion touches on Itanium is particularly interesting to me, but there&#8217;s much more.</p><p>No matter your opinion of Intel, its place in history is secure, and Colwell both had an outsized hand in writing that history and also bore firsthand witness to many events that are recounted here.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p> I like to say, the Internet has been good for consumers and for content, but not-so-good for content creators. That applies equally to music to technical writing</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>In 2013 or so, I had the great privilege of meeting Ursula K. LeGuin at a Clarion West event, and told her I&#8217;d just reread the &#8220;six Earthsea books.&#8221; She lit up and said, &#8220;I&#8217;m so glad you said six! Everyone talks about a &#8216;trilogy'!&#8221; And then she said something that has stayed with me ever since: &#8220;Rereading a book is a difference experience every time, <em>because you are a different person.</em>&#8221; (emphasis mine) The principle applies equally to technical books.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p> An honest assessment of intrinsics based code is that it&#8217;s barely human-readable.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Fun New Project: itch-order-simd]]></title><description><![CDATA[First of a series on optimizing a limit order book with AVX]]></description><link>https://parallelprogrammer.substack.com/p/fun-new-project-itch-order-simd</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/fun-new-project-itch-order-simd</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Fri, 14 Nov 2025 14:31:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you spend enough time at a high-frequency trading firm, even the techies learn about Central Limit Order Books or CLOBs, the central abstraction of choice for many markets to facilitate trading. If you aren&#8217;t familiar with the concept, this <a href="https://www.nasdaq.com/articles/demystifying-the-central-limit-order-book-clob-everything-you-need-to-know">article by NASDAQ</a> is an excellent introduction. Another fun overview of high-frequency trading that touches briefly on the subject at hand, may be found here at this article &#8220;<a href="https://levelup.gitconnected.com/inside-high-frequency-trading-systems-the-race-to-zero-latency-faa638d0c180">Inside High-Frequency Trading Systems</a>.&#8221;</p><p>A &#8220;limit order book&#8221; is a list of orders to buy and sell &#8220;symbols,&#8221; the generic name for stocks, bonds, options, futures, or whatever other commodity is being traded on the exchange (there is a glossary at the bottom of this article). Market participants submit orders with the price and quantity of the symbol that they wish to buy or sell, and the exchange has rules to match these buyers and sellers and execute the requested trades. As one might expect, the exchange&#8217;s specialized technology to perform this task is called the &#8220;matching engine.&#8221;</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>The term &#8220;limit&#8221; in &#8220;limit order book&#8221; refers to the price that a trader specified along with their order. A buy order (&#8220;bid&#8221;) at a given price means the trader does not wish the order to be filled above that price; for sell orders (&#8220;asks&#8221;), the seller does not wish to sell below a given price. Traders often submit bids that are a bit lower, or asks that are a bit higher, than the prices currently being traded on the exchange; a &#8220;market order&#8221; is a request that the exchange simply fill the order at the best-available price, but often that is a construct of the retail trading market, not a primitive operation offered by the exchange. If an order cannot be immediately filled, it stays in the book until filled or cancelled.</p><p>The current price of a commodity is difficult to define: one only knows the price at which the most recent trade was fulfilled, but by definition, the price may be moving. The lowest ask price is always higher than the highest bid price, inviting the definition of a market&#8217;s &#8220;spread&#8221;: the difference between the two. As a rule, the smaller the spread, the more liquidity is in the market. (Also as a rule, HFT firms&#8217; profit is highly correlated with the size of the spread; the bigger the spread, the more opportunity there is for them to submit profitable trades.) In any case, whether the prices you see scrolling across the CNBC chyron are &#8220;midpoints&#8221; (the average between the lowest ask and the highest bid) or the last price filled, obviously it&#8217;s subject to change, possibly quickly.</p><p>Computers that are plugged into the exchange get updates with tremendous frequency &#8211; depending on the exchange, up to millions of times per second: mostly new orders (whether they be to buy or sell a symbol), cancellations of existing orders, and &#8220;fills,&#8221; notices that the exchange&#8217;s matching engine has filled corresponding bid and ask orders that had been on the books. Since the CLOB is huge in comparison to an order, most exchanges require market participants to maintain their own accurate mirrors of the exchange&#8217;s CLOB as updates pour into their trading systems.</p><p>Although details vary by exchange, updates typically include <code>ADD</code>, <code>DELETE</code>, and <code>EXECUTE</code> orders, with each order containing only the security, price, quantity, and whether the order is a bid or ask at the given price. As a consequence, efficiently updating the levels corresponding to the security in question is the stock in trade (dyswidt) of every high frequency market participant, preferably with the levels sorted by price and canonicalized to a form more amenable to processing by the strategy code.</p><p>Often, HFT firms maintain an <em>aggregate book</em> that distills all the pending orders into <em>price levels</em>, with the total quantities of bid and ask orders resting at each level. The &#8220;top&#8221; of the book, with the orders closest to the bid-ask spread, is the main focus of many trading algorithms. Some are as simple as observing that the book is &#8220;weighted&#8221; too heavily in one direction or the other: for example, if there are more asks than bids, to enough of a degree, then the commodity is likely to decline.</p><p>The topic of this blog series will be to maintain an aggregate book, minimizing latency of updates to maximize a prospective strategy&#8217;s opportunity to process the book and make trading decisions.</p><p>Having worked on production HFT code, I took an interest in fast CLOB updates, with an especial focus on SIMD-friendly algorithms to do the updates quickly. CLOB code is fun to optimize and also makes for a good illustration of the transformations that need to be applied to enable SIMD processing to work best.</p><p>To start, I looked around the Internet for an open source implementation, hoping to find one specialized to the ITCH feed used by NASDAQ to trade equities. The high volume and low latency requirements to successfully trade on NASDAQ are legion, ITCH is well-documented, and you can reach out to NASDAQ to obtain sample ITCH files to use to simulate processing. This <a href="https://github.com/aanrv/Order-Book">implementation</a> is a good example of a &#8216;textbook&#8217; implementation, using data structures like hashmaps for order lookup and a doubly-linked list for easy removal of the order upon fulfillment or cancellation. The <a href="https://github.com/aanrv/Order-Book/blob/804f43c09599b516b6bdc90f44df149caa154160/include/order_book.hpp#L24C1-L32C3">Order structure</a> looks like this:</p><pre><code>struct Level {
    uint32_t price;
    uint32_t limitVolume;
    // either both are nullptr or both are populated
    // i.e. if orders in Level == 1, both pointers are ==
    Order* first;
    Order* last;
    Level(uint32_t _price);
};</code></pre><p>To the author&#8217;s credit, they use a pool for allocation, not the default C++ heap, but my intuition leads me away from using this type of data structure for a CLOB &#8211; on 64-bit systems, the pointers alone are 8B each. Since data movement is the <a href="https://parallelprogrammer.substack.com/p/dont-move-the-data?r=4xff6v">limiting reagent of all compute</a>, a smaller memory footprint often leads to faster code.</p><p>An implementation closer to my starting point was built by a Charles Cooper, whose <a href="https://github.com/charles-cooper/itch-order-book">implementation</a> uses <code>std::vector</code> to hold the aggregate limit order book. Cooper&#8217;s implementation is both older (work started c. 2014) and claims much lower per-trade latency than the previously-linked repository (61ns v. 92ns). Quoting from the readme:</p><pre><code>This is a very fast implementation of the ITCH order book, clocking in at around 61ns / tick (or 16 million messages / second, tested on a 2012 i7-3820), offering fast updates and O(1) access to any price level (to get the price is a single dereference, the aggregate quantity is another dereference). It only calculates the aggregate quantities at each price and does not track the queue depth for each order.</code></pre><p>The <a href="https://github.com/charles-cooper/itch-order-book/blob/095ac97ef46c72411a62dd9148d96d2e3ace3305/order_book.h#L139C1-L143C11">order structure</a> in Cooper&#8217;s repository looks like this:</p><pre><code>enum class qty_t : uint32_t {};
enum class book_id_t : uint16_t {};
enum class level_id_t : uint32_t {};
typedef struct order {
  qty_t m_qty;
  level_id_t level_idx;
  book_id_t book_idx;
} order_t;</code></pre><p>The two sides then are instantiated in the flagship <code>order_book</code> structure as follows:</p><pre><code>class price_level
{
 public:
  price_level() {}
  price_level(sprice_t __price, level_id_t __ptr)
      : m_price(__price), m_ptr(__ptr)
  {
  }
  sprice_t m_price;
  level_id_t m_ptr;
};

using sorted_levels_t = std::vector&lt;price_level&gt;;
sorted_levels_t m_bids;
sorted_levels_t m_offers;</code></pre><p>Adding a new order consists of doing an Insertion Sort into one or the other of <code>m_bids</code> / <code>m_asks</code>, depending on whether the order is to buy or sell. The <a href="https://github.com/charles-cooper/itch-order-book/blob/095ac97ef46c72411a62dd9148d96d2e3ace3305/order_book.h#L228C1-L255C1">ADD_ORDER method</a> in <code>order_book</code>:</p><pre><code>  void ADD_ORDER(order_t *order, sprice_t const price, qty_t const qty)
  {
    sorted_levels_t *sorted_levels = is_bid(price) ? &amp;m_bids : &amp;m_offers;
    // search descending for the price
    auto insertion_point = sorted_levels-&gt;end();
    bool found = false;
    while (insertion_point-- != sorted_levels-&gt;begin()) {
      price_level &amp;curprice = *insertion_point;
      if (curprice.m_price == price) {
        order-&gt;level_idx = curprice.m_ptr;
        found = true;
        break;
      } else if (price &gt; curprice.m_price) {
        // insertion pt will be -1 if price &lt; all prices
        break;
      }
    }
    if (!found) {
      order-&gt;level_idx = s_levels.alloc();
      s_levels[order-&gt;level_idx].m_qty = qty_t(0);
      s_levels[order-&gt;level_idx].m_price = price;
      price_level const px(price, order-&gt;level_idx);
      ++insertion_point;
      sorted_levels-&gt;insert(insertion_point, px);
    }
    s_levels[order-&gt;level_idx].m_qty = s_levels[order-&gt;level_idx].m_qty + qty;
  }</code></pre><p>starts by assigning <code>sorted_levels</code> to be a pointer to the sorted vector for the side:</p><pre><code>sorted_levels_t *sorted_levels = is_bid(price) ? &amp;m_bids : &amp;m_offers;</code></pre><p>The inline function <em>is_bid()</em> relies on the sign of the price:</p><pre><code>bool constexpr is_bid(sprice_t const x) { return int32_t(x) &gt;= 0; }</code></pre><p>The loop scans the sorted vector of <code>price_level</code> structures, looking for one with the same price as the incoming order; if it finds one, it terminates with <code>found==true</code>. Otherwise, a new index is allocated for the price level and inserted into the vector.</p><p>The <code>DELETE</code> method reduces the aggregate quantity for the given price level and, if it is now zero, removes the corresponding <code>price_level</code> structure from the side (again using a variable <code>sorted_levels</code> for the <code>std::vector</code> of bids or asks.</p><pre><code>  // shared between delete and execute
  void DELETE_ORDER(order_t *order)
  {
    assert(MKPRIMITIVE(s_levels[order-&gt;level_idx].m_qty) &gt;=
           MKPRIMITIVE(order-&gt;m_qty));
    auto tmp = MKPRIMITIVE(s_levels[order-&gt;level_idx].m_qty);
    tmp -= MKPRIMITIVE(order-&gt;m_qty);
    s_levels[order-&gt;level_idx].m_qty = qty_t(tmp);
    if (qty_t(0) == s_levels[order-&gt;level_idx].m_qty) {
      // DELETE_SORTED([order-&gt;level_idx].price);
      sprice_t price = s_levels[order-&gt;level_idx].m_price;
      sorted_levels_t *sorted_levels = is_bid(price) ? &amp;m_bids : &amp;m_offers;
      auto it = sorted_levels-&gt;end();
      while (it-- != sorted_levels-&gt;begin()) {
        if (it-&gt;m_price == price) {
          sorted_levels-&gt;erase(it);
          break;
        }
      }
      s_levels.free(order-&gt;level_idx);
    }
  }</code></pre><p>Let&#8217;s be clear. This code runs pretty fast on modern CPUs! It uses arrays, albeit &#8220;array of structures&#8221; as opposed to the &#8220;structure of arrays&#8221; memory layout favored for processing by SIMD instructions; but modern CPUs have hardware prefetchers that make array processing very fast.</p><p>Before we take a serious pass at optimizing this code, we&#8217;ll want to refactor it a bit, partly for housekeeping to improve its processing of ITCH files (the multi-gigabyte files from Nasdaq that summarize a day&#8217;s trading), partly to improve the emulated I/O performance, and partly to lay the groundwork for SIMD optimization. An SOA layout for the sorted vectors is <em>de rigueur</em>!</p><h1>Roadmap</h1><p>I&#8217;ll be honest - this blog series is a writeup on coding work I&#8217;ve already done. I&#8217;m waist-deep in past commits and work branches, trying to hammer the code into a form with pedagogical value.</p><p>So in rough order, here are some of the changes to the repository that I am planning:</p><ul><li><p>A refactor of the <code>buf_t</code> class in <code>bufferedreader.h</code>, to use mapped file I/O instead of the POSIX <code>open/read/close</code> API set; in my opinion, mapped file I/O delivers a much closer approximation to getting orders off the wire;</p></li><li><p>An update to the ITCH processing code to read the initial symbols, so we can decode which stock symbol (e.g., MSFT) corresponds to the 16-bit &#8216;locate&#8217; field in the order;</p></li><li><p>Duplication of the existing <code>m_bids / m_offers</code> vectors with SOA layout, with optional cross-checking.</p></li><li><p>I&#8217;ll likely break out the bigger <code>ADD_ORDER / DELETE_ORDER</code> functions into a separate implementation file instead of leaving them in headers.</p></li><li><p>One refactor that I have <em>not</em> completed is to flatten the source code for various SOA layouts and instruction sets that currently reside in branches. I want to flatten the code and make all the variants visible simultaneously, and I have an implementation strategy in mind. We&#8217;ll cross that bridge when we get to it.</p></li></ul><p>Once these changes are in place, we&#8217;ll be able to pursue a proper SIMD optimized version of the limit order book. And as long as the cross-checking is indeed optional, we&#8217;ll be able to see how much faster it runs! (Also on my to-do list: try the optimized code on a recent AVX-512 implementation, instead of my slow AMD machine.)</p><p>For now, here is a <a href="https://github.com/ArchaeaSoftware/itch-order-simd">link to my fork</a> of Cooper&#8217;s <a href="https://github.com/charles-cooper/itch-order-book">itch-order-book</a> repository.</p><h1>Glossary</h1><p>ask &#8211; order to sell. Typically accompanied by a quantity, and the lowest price the buyer would accept.</p><p>bid &#8211; order to buy. Typically accompanied by a quantity, and the highest price the buyer would be willing to pay.</p><p>central limit order book (CLOB) &#8211; the central data structure</p><p>exchange &#8211; an entity that facilitates the buying and selling of some set of commodities, often using a central limit order book (CLOB)</p><p>level &#8211; a price; in the context of a CLOB, multiple orders may come in at the same price level.</p><p>price &#8211; the amount of money a trader is willing to pay (if buying) or accept (if selling). Typically accompanies a bid or ask. Importantly, prices are not always specified using currencies &#8211; the generic term is &#8216;tick.&#8217; For some exchanges, prices are 64-bit integers!</p><p>price level &#8211; see <em>level</em>.</p><p>side &#8211; generic term for whether an order is a bid (to buy) or ask (to sell).</p><p>spread &#8211; difference between the lowest ask and highest bid price.</p><p>symbol &#8211; a commodity being traded on the exchange, be it an equity, option, or future.</p><p>tick &#8211; the smallest unit of price.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Don't Move The Data!]]></title><description><![CDATA[A prescient 2017-era article, with 2025 commentary.]]></description><link>https://parallelprogrammer.substack.com/p/dont-move-the-data</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/dont-move-the-data</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Mon, 27 Oct 2025 13:02:46 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!-OLJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Editor&#8217;s Note: This article was first posted on October 6, 2017 on the CUDA Handbook blog. It has been updated (with editor&#8217;s notes) to reflect developments between then and mid-2025.</em></p><p>NVIDIA just <a href="https://www.top500.org/news/nvidia-ships-first-volta-dgx-1/">delivered their first Volta-enabled DGX-1 systems</a> &#8211; great news for those who need additional compute power: each V100 chip delivers 15.7 TFLOPS of single precision performance, compared to its predecessor that only had 10.6 TFLOPS.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><em>Wait</em>, you say, <em>that&#8217;s an interesting qualifier</em>. <em>Who doesn&#8217;t &#8220;need the additional compute power&#8230;?&#8221; Did someone hack into Nick&#8217;s blog account and post on his behalf? Or has he become a Luddite in his dotage?</em></p><p>Nope, no, I still think more compute is generally better; but it is past time to question the architecture of these systems with huge, discrete GPUs connected to the world by buses. The problem with DGX-1 is that those GPUs are hungry! They need to be fed! And they can only sip data through the tiny soda straw known as the PCI Express bus. <em>(For a 2025 perspective, consider that NVIDIA claims 8,000GB/s per GB200 chip, compared to PCIe 6.0&#8217;s 128GB/s unidirectional bandwidth, for a 60x disparity between HBM bandwidth and bus bandwidth. Both of these numbers can be doubled: the Blackwell number because it is a two-die configuration, and the PCIe number because it supports 256GB/s bidirectional bandwidth. &#8212;Ed.)</em></p><p>For perspective, let&#8217;s compare these chips to G80, the first CUDA-capable GPU. Let&#8217;s set the stage by observing that G80 was the largest ASIC NVIDIA could feasibly design and fabricate in 2006, straining the limits of contemporary fabrication technology &#8211; a classic &#8220;win&#8221; chip. It had 684M transistors, a theoretical maximum performance of 384GFLOPS for single precision, and no support at all for double precision. GP100 and GV100 respectively have 22x and 31x more transistors, and 27x and 39x more single precision performance than G80. <em>(In 2025, a Blackwell chip contains 104B transistors, 5x more than 2017&#8217;s V100, and they utilize packaging to effectively double that count. So each of the two chiplets has &gt;150x as many transistors as the original CUDA-capable chip! &#8212;Ed.)</em> But the bandwidth to deliver data to and from these GPUs has not been increasing commensurately with that performance.</p><pre><code><code>                Transistors   SPFOPS   Bandwidth
Chip  Date       (billions)  (TFLOPS)    (G/s)    FLOPS/byte
 G80  11/8/2006      0.7        0.38       86.4      4.4
GT200 6/16/2008      1.4        0.93      141        6.6
GF100 3/26/2010      3.1        1.35      177        7.6
GK110 11/12/2012     7.0        4.3       288       14.9
P100  4/5/2016      15.3       10.6       732       14.5
V100  6/21/2017     21.2       15.7       900       17.4
A100  5/14/2020     54.2       19.5      2000        9.8 
H100  3/22/2022     80         67        3350       20.0
H200  11/28/2024    80         67        4800       14.0
B200  1/30/2025    104         80        8000       10.0</code></code></pre><p>There is an inflection point in the chart with the introduction of HBM (P100, c. 2016), the memory technology invented to keep GPUs from starving. HBM was able to keep the FLOPS/byte from continuing to diverge at the same rate.</p><p>Several other important developments are not reflected in this table: </p><ul><li><p>TensorCores, which greatly increase the theoretical FLOPS counts deliverable by chips since V100 (c. 2017),</p></li><li><p>NVIDIA has pivoted their product designs (and marketing) to focus on peak FLOPS numbers deliverable by TensorCores, not single precision FLOPS, and</p></li><li><p>NVLink, which displaced PCIe as the GPU-GPU interconnect within nodes (&#8220;scale-up&#8221;) - the need for this investment was motivated because the industry standard PCIe bus plateaued at the 3.0 standard for almost a decade (c. 2013-2022).</p></li></ul><p></p><p>As the number of FLOPS per byte of I/O diverges, the number of workloads that benefit from more FLOPS diminishes. Googling around for literature on FLOPS/byte, I ran across <a href="http://www.astro.caltech.edu/~george/aybi199/Kogge_Exascale.pdf">this 2011 presentation</a> by <a href="https://engineering.nd.edu/faculty/peter-kogge/">Peter Kogge</a> entitled &#8220;Hardware Evolution Trends of Extreme Scale Computing.&#8221; For anyone in the GPU business, the first sign that something&#8217;s amiss crops up in Slide 3, which cites &#8220;1 byte/s per FLOPS/s&#8221; as the &#8220;Classical Measure of Performance&#8221; <em>(slide added &#8212;Ed.):</em></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-OLJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-OLJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png 424w, https://substackcdn.com/image/fetch/$s_!-OLJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png 848w, https://substackcdn.com/image/fetch/$s_!-OLJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png 1272w, https://substackcdn.com/image/fetch/$s_!-OLJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-OLJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png" width="1019" height="783" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:783,&quot;width&quot;:1019,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:133329,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/168495978?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-OLJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png 424w, https://substackcdn.com/image/fetch/$s_!-OLJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png 848w, https://substackcdn.com/image/fetch/$s_!-OLJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png 1272w, https://substackcdn.com/image/fetch/$s_!-OLJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91a701eb-0555-4264-b749-4f120a35c48d_1019x783.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Even G80&#8217;s device memory fell well short of that goal with 1 byte/4.5 FLOPS. <em><strong>I prefer this framing because it adopts the viewpoint of scarcity (bytes/FLOP &#8211; getting data in and out for processing) rather than abundance (FLOPS/byte &#8211; having lots of processing power to bring to bear on data once it is in hand).</strong></em></p><p>The presentation is from 2011, but still very relevant: after reviewing Moore&#8217;s Law and the rise and fall of Dennard scaling, and the preeminent importance of power dissipation in modern computing, the concluding slide reads in part:</p><ul><li><p>World has gone to multi-core to continue Moore&#8217;s Law</p></li><li><p>Pushing performance another 1000X will be tough</p></li><li><p>The major problem is in energy</p></li><li><p>And that energy is in memory &amp; interconnect</p></li><li><p>We need to begin rearchitecting to reflect this &#8230;</p></li><li><p>DON&#8217;T MOVE THE DATA!</p></li></ul><p>&#8220;DON&#8217;T MOVE THE DATA&#8221; has been good advice to everyone who&#8217;s had the data for decades (in 1992 I wrote a Dr. Dobb&#8217;s Journal <a href="https://jacobfilipp.com/DrDobbs/articles/DDJ/1992/9203/9203c/9203c.htm">article</a> that focused on hand-coding x87 assembly to keep intermediate results in registers)&#8230; but the advice has more currency now.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lECO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lECO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png 424w, https://substackcdn.com/image/fetch/$s_!lECO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png 848w, https://substackcdn.com/image/fetch/$s_!lECO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png 1272w, https://substackcdn.com/image/fetch/$s_!lECO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lECO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png" width="1022" height="790" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:790,&quot;width&quot;:1022,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:115051,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/168495978?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lECO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png 424w, https://substackcdn.com/image/fetch/$s_!lECO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png 848w, https://substackcdn.com/image/fetch/$s_!lECO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png 1272w, https://substackcdn.com/image/fetch/$s_!lECO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4200c93-d9a9-4ab0-a07d-2a7ebcb2938c_1022x790.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h1>Moving The Data on CPUs</h1><p>The data/compute conundrum finds expression on modern multi-core CPUs, too. Each core on a modern x86 CPU has ILP (instruction level parallelism) of 5, meaning it can detect parallelism opportunities between non-dependent instructions and execute up to 5 instructions in a single clock cycle. Latency to the L3 cache is about 50 clock cycles. So a CPU core can perform at least dozens, and possibly hundreds, of FLOPS on data in registers during the time it takes for the L3 to service a load (2 of the 5 pipelines can do 8 FLOPS per instruction via AVX). And that&#8217;s assuming the data was in cache!</p><p>As an aside, this observation helps explain why &#8220;optimized&#8221; numerical Python code is still dead slow. Python is interpreted, so has a library called Numpy that wraps vectorized implementations of operations that do things like element-wise addition or multiplication between arrays. But for the reasons explained above, it is very inefficient to do multiple passes over the data if the computation could have been fused into a single pass. The code spends all of its time moving data, and very little time processing it. <em>(Python developers wishing to optimize such code can use <a href="https://numba.pydata.org/">Numba</a>, which enables JIT compilation for operator fusion and other performance optimizations. &#8212;Ed.)</em></p><p><strong>DON&#8217;T MOVE THE DATA!</strong></p><h1>A Gift From Heaven: Deep Learning</h1><p>Which workloads, pray tell, require endless FLOPS per byte of I/O? Or turn it around and ask, which workloads still thrive when there is barely any I/O per FLOP? NVIDIA hasn&#8217;t been shy about trumpeting its solution to this problem: deep learning! Training a deep learning network entails refining floating point weights that roughly represent neurons that &#8220;learn&#8221; as they are trained on the data. As long as the weights can reside in device memory, only a modest amount of I/O is needed to keep the GPU busy. In retrospect, NVIDIA is extremely fortunate that deep learning cropped up. Without it, it&#8217;s not clear what workload could soak up all those FLOPS without the GPUs starving. <em>(2025 update: At the end of 2017, NVIDIA&#8217;s market capitalization was some $117B. It is now $4.2T, 35x higher.. so to say NVIDIA is &#8220;extremely fortunate that deep learning cropped up&#8221; turned out to be the understatement of the millenium! &#8212;Ed.)</em> The importance of machine learning as a workload helps explain why GV100 contains purpose-built hardware for machine learning, in the form of <a href="http://www.tomshardware.com/news/nvidia-tensor-core-tesla-v100,34384.html">TensorCores</a>. <em><strong>But that hardware actually exacerbates the GPU starvation problem, by increasing FLOPS without increasing bandwidth.</strong></em></p><p>NVIDIA probably isn&#8217;t comfortable betting the farm on a single workload &#8211; especially one where their main customers are enterprises that can invest in their <a href="https://cloud.google.com/blog/big-data/2017/05/an-in-depth-look-at-googles-first-tensor-processing-unit-tpu">own machine learning hardware</a> and that is attracting <a href="http://www.nanalyze.com/2017/05/12-ai-hardware-startups-new-ai-chips/">VC money for application-specific hardware</a>. How do you hedge? How can NVIDIA relieve the bottleneck? Unless some workload materializes that is as compute-intensive (per byte of I/O) as machine learning, NVIDIA must seek out ways to address their GPUs&#8217; I/O bottleneck. <em>(And certainly they did, by purchasing Mellanox for their Infiniband controllers and driving continued investments into their existing NVLink and GPUDirect technologies. &#8212;Ed.)</em></p><h1>I/O: NVIDIA&#8217;s Strategic Landscape</h1><p>The problem confronted by NVIDIA is that they are hindered by some business and legal challenges. According to the terms of their 2011 <a href="https://www.anandtech.com/show/4122/intel-settles-with-nvidia-more-money-fewer-problems-no-x86/2">settlement with Intel</a>, 1) They do not have a license to Intel&#8217;s industry-leading cache coherency protocol technology, and 2) they do not have a license to build x86 CPUs, or even x86 emulators. <em>(How times have changed! The details we know about the Intel/NVIDIA deal appear to be squarely aimed at granting NVIDIA access to x86 processors and Intel&#8217;s cache coherency protocol. NVIDIA can leverage the learnings from their own Grace Hopper superchip, preceded by the partnership with IBM to bring coherent access to GPU memory to servers with IBM&#8217;s POWER architecture. I don&#8217;t know about Jensen, but for NVIDIA&#8217;s sake, I was pining for access to <a href="https://www.intel.com/content/www/us/en/io/quickpath-technology/quickpath-technology-general.html">QPI</a> back in the late aughts! &#8212;Ed.)</em></p><p>NVIDIA has done what they can with the hand they were dealt &#8211; they built <a href="https://developer.nvidia.com/gpudirect">GPUDirect</a> to enable fellow citizens of the bus (typically Infiniband controllers <em>[and, since, they have added mass storage such as NVMe &#8212;Ed.</em>]) to access GPU memory without CPU intervention; they built <a href="http://www.nvidia.com/object/nvlink.html">NVLINK</a>, a proprietary cache coherency protocol. They have <a href="https://www.ibm.com/blogs/systems/ibm-nvidia-present-nvlink-server-youve-waiting/">licensed NVLINK to IBM</a> for the POWER architecture and signaled a willingness to license it to <a href="http://on-demand.gputechconf.com/gtc/2015/presentation/S5649-Steve-Oberlin.pdf">ARM licensees</a>. The problem is that POWER and ARM64 are inferior to Intel&#8217;s x86, whose high-end CPU performance is unmatched and whose &#8220;uncore&#8221; enables fast, cache coherent access across sockets. NVIDIA itself, though an ARM licensee, has announced that they <a href="http://www.techradar.com/news/computing-components/processors/nvidia-gives-up-on-64-bit-tegra-for-servers-1254547">will not be building a server-class ARM chip</a>. <em>(In April 2021, they reversed this decision and <a href="https://nvidianews.nvidia.com/news/nvidia-announces-cpu-for-giant-ai-and-high-performance-computing-workloads">announced Grace</a>. NVIDIA also tried and failed to buy ARM, a $40B purchase that was announced in September 2020 and abandoned in February 2022 amidst regulatory headwinds. In a quirk of coincidence, NVIDIA paid a $1.25B penalty to Softbank after terminating the agreement, an amount reminiscent of the $1.5B paid to NVIDIA by Intel for the private antitrust settlement. &#8212;Ed.)</em></p><p>I&#8217;m not sure why NVIDIA announced they would not be building their own ARM to drive their GPUs, because that seems like an obvious way for them to own their destiny. It may be that NVIDIA concluded that ARM64 cores simply will never deliver enough performance to drive their GPUs. That&#8217;s too bad, because there is a lot of low-hanging fruit in NVIDIA&#8217;s driver stack. If they made the software more efficient, it could either run faster on the same hardware or run at the same speed on lesser hardware &#8211; like ARM64 cores.</p><p>Not being able to coordinate with Intel on the cache coherency protocol has cost NVIDIA big-time in at least one area: peer-to-peer GPU traffic. Intel could, but chooses not to, service peer-to-peer traffic between NVIDIA GPUs at high performance (Intel and NVIDIA give different stories as to the reason, and these conversations happen indirectly because the two companies do not seem to have diplomatic relations). As things stand, if you have a dual-CPU server (such as NVIDIA&#8217;s own DGX-1) with cache coherency links between the CPUs, any peer-to-peer GPU traffic must be carefully routed past the CPUs, taking care not to cross the cache coherency link. If Intel could license <a href="https://web.archive.org/web/20170701000000*/https://www.altera.com/products/reference-designs/all-reference-designs/computing/qpi.html">QPI to Altera</a>, they could license it to NVIDIA. Failing to do so is a matter of choice and a by-product of the two companies&#8217; respective positions in the business and legal landscapes.</p><p>As things stand, NVIDIA is dependent on Intel to ship great CPUs <em>(in 2025, of course, <a href="https://wccftech.com/amd-manages-to-shift-the-dynamics-of-data-center-markets-into-its-favour/">AMD has since eaten Intel&#8217;s lunch!</a> -Ed.)</em> with good bus integration, and peer-to-peer-capable GPU servers have to be designed to steer traffic around the QPI link. The announcement that NVIDIA would not build ARM64 SOCs was done in 2014, so now that the competitive landscape has evolved (and though I can remember when Intel&#8217;s market capitalization was 12x NVIDIA&#8217;s, it is now only about 1.7x), it would not surprise me if NVIDIA revisited that decision. <em>(Of course, NVIDIA </em>did <em>enter the server-class ARM business with Grace so my crystal ball was working that day. But if you&#8217;d told me that in 2025, NVDA would be worth $4.5T and INTC $185B, I am not sure I would have believed you. &#8212;Ed.)</em></p><h1>One Path Forward: SoCs</h1><p>One partial solution to the interconnect problem is to build a System on a Chip (SoC): put the CPU and GPU on the same die. <em>(In 2025, packaging technology has advanced to the point where hardware designers build multi-chip implementations of such architectures, such as AMD&#8217;s MI300A or NVIDIA&#8217;s GB10. &#8212;Ed.)</em> Intel and AMD have been building x86 SOCs for many years; it is Intel&#8217;s solution to the value PC market, and AMD has behaved like their life depended on it since 2006, when they acquired GPU vendor ATI. NVIDIA&#8217;s Tegra GPUs are all ARM SoCs. The biggest downside of SoCs is that the ratio of CPU/GPU performance is fixed years before the hardware becomes available, causing workloads to suffer if they are more CPU- or GPU-intensive than the SoC was designed to address. <em>(This downside risk of SOC design is mitigated by chiplets, since they are separately fabricated and can even target different semiconductor nodes. &#8212;Ed.)</em> And if the device doesn&#8217;t have enough performance, scaling performance across multiple chips may be more difficult because GPUs require such high bandwidth. A conspicuous success story for big SoCs has been in the gaming console market, where the target workload is better-understood and, in any case, game developers will code against whatever hardware is in the console.</p><p>So I suspect that as workloads continue to tap out the FLOPS and balance out the bandwidth/FLOPS, big SoCs will start to make more sense. In sizing the CPU/GPU ratio, hardware designers can create a device with the biggest possible GPU that doesn&#8217;t starve with the available bandwidth.</p><p>SoCs are just a stopgap, though. As the laws of physics continue to lower the boom, the importance of system design will continue to increase, as Kogge pointed out in his 2011 presentation. The fundamental problem of the speed of light isn&#8217;t going away&#8230; ever.</p><p><em>(The DGX Spark [n&#233; DIGITS] device announced at GTC 2025, of course, is a system-in-package with ARM cores and a Blackwell GPU. As far as packaging goes, NVIDIA has been late to the chiplet party, possibly due to lingering trauma from the <a href="https://www.semiaccurate.com/2010/07/11/why-nvidias-chips-are-defective/">bump crack fiasco</a> of the 2000s; but <a href="https://finance.yahoo.com/news/nvidia-shifts-cowos-l-packaging-172516539.html">they are on the way</a>. &#8212;Ed.)</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[This ML Workload Runs 30x Faster w AVX-512]]></title><description><![CDATA[Register-Register Lookups FTW!]]></description><link>https://parallelprogrammer.substack.com/p/quantizing-to-nf4-with-avx-512</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/quantizing-to-nf4-with-avx-512</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Sun, 19 Oct 2025 15:55:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!YrkZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Copyright (C) 2025 by Nicholas Wilt. All rights reserved.</em></p><p>When Tim Dettmers et al. published <a href="https://arxiv.org/abs/2305.14314">QLoRA</a> (Quantized Low-Rank Adaptation), a memory-efficient method for fine-tuning large language models, they introduced the 4-bit NormalFloat (NF4) representation for quantization. Quoting from their paper, NF4 is &#8220;an information theoretically optimal quantization data type for normally distributed data that yields better empirical results than 4-bit Integers and 4-bit Floats.&#8221; Section 3 of the paper describes how they arrived at the sixteen (16) values representable by NF4, and Appendix E enumerates them:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><pre><code>constexpr float NF4_LUT[16] = {
    -1.0,
    -0.6961928009986877,
    -0.5250730514526367,
    -0.39491748809814453,
    -0.28444138169288635,
    -0.18477343022823334,
    -0.09105003625154495,
    0.0,
    0.07958029955625534,
    0.16093020141124725,
    0.24611230194568634,
    0.33791524171829224,
    0.44070982933044434,
    0.5626170039176941,
    0.7229568362236023,
    1.0
};</code></pre><p>It is pretty wild to consider that the venerable FP32 data type can be usefully distilled into 4 bits by simply picking 16 (sixteen) of the 4 billion or so values representable by FP32, and picking the closest one; but when you think about it, that is what all FP4 representations do. It&#8217;s just a bit jarring to see such arbitrary-seeming values written out to double-precision accuracy.</p><p>So, how does one convert to and from this representation? From is easy: a simple lookup into the NF4_LUT table above. What about the other direction (&#8220;quantization&#8221;), i.e. how to convert a value in the range [-1.0, 1.0] to the closest of the 16 encodings of NF4 enumerated above? The value 0.6, for example, falls between <code>NF4_LUT[13]=0.5626</code> and <code>NF4_LUT[14]=0.7230</code> and is closest to <code>NF4_LUT[13]</code>. So the (zero-based) index, and the correct NF4 encoding for 0.6, is <code>13=0xd</code>.</p><p>A brute force implementation would simply go through all 16 entries of the lookup table, compute the distance to the input value, and report the index that was closest. That code looks like this:</p><pre><code>int
float_to_NF4_0( const float *base, size_t N, float f )
{
    int ret = 0;
    float diff = fabsf( base[0]-f );
    for ( size_t i = 1; i &lt; 16; i++ ) {
        float thisdiff = fabsf( base[i]-f );
        if ( thisdiff &lt; diff ) {
            diff = thisdiff;
            ret = i;
        }
    }
    return ret;
}</code></pre><p>This algorithm works for any lookup table - obviously you pass <code>NF4_LUT</code> as the first parameter and <code>16</code> as the second. (The parameter <code>N</code> is left over from implementations that supported problem sizes other than 16.)</p><p>But looking at the <code>NF4_LUT</code> array, and considering the problem from first principles, reasonably would expect that their structure (they are in sorted order) could be exploited somehow. Binary Search comes to mind; and for my part, I was reminded of a trick that I learned from Jon Bentley&#8217;s <em><a href="https://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880">Programming Pearls</a></em> (1986), a compendium of his popular monthly column that used to run in <em>Communications of the ACM</em>. Bentley&#8217;s optimization, intended for fixed-sized arrays, enables the Binary Search probes to be done with steadily decreasing powers of 2, in an elegantly unrolled loop. For NF4, that looks like this:</p><pre><code>int
float_to_NF4_1( const float *base, size_t N, float f )
{
    int i = 0;
    if ( f &gt;= base[i+8] ) i += 8;
    if ( f &gt;= base[i+4] ) i += 4;
    if ( f &gt;= base[i+2] ) i += 2;
    if ( f &gt;= base[i+1] ) i += 1;
    return i + ((base[i+1]-f)&lt;(f-base[i]));
}</code></pre><p>After the Binary Search is complete, <code>0&lt;=i&lt;15</code> and the correct return value must be <code>i</code> or <code>i+1</code>. The final return value is computed by incrementing <code>i</code> if the input value is closer to <code>NF4_LUT[i+1]</code>. This replaces the exhaustive search of the brute force algorithm with Binary Search.</p><p>Are further improvements possible? I thought you would never ask!</p><h1>Midpoints FTW</h1><p>The <a href="https://github.com/bitsandbytes-foundation/bitsandbytes/blob/c3b8de268fdb55a88f92feada23fc811a1e6877a/csrc/kernels.cu#L124">bitsandbytes implementation</a> of the same quantization algorithm does something similar, but more clever, because it eliminates the need for that final check and conditional increment: this code precomputes the 15 midpoints between the 16 LUT entries, and constructs the 4-bit index by evaluating a decision tree (at most 4 if statements):</p><pre><code>inline
unsigned char
dQuantizeNF4_0(float x)
{
  // the values for this tree was generated by test_normal_map_tree
  // in the file tests/test_functional.py
  if(x &gt; 0.03979014977812767f)
    if(x &gt; 0.3893125355243683f) // 1
      if(x &gt; 0.6427869200706482f) // 11
        if(x &gt; 0.8614784181118011f) // 111
          return 0b1111;
        else
          return 0b1110;
      else
        if(x &gt; 0.5016634166240692f) // 110
          return 0b1101;
        else
          return 0b1100;
    else
      if(x &gt; 0.2035212516784668f) // 10
        if(x &gt; 0.2920137718319893f) // 101
          return 0b1011;
        else
          return 0b1010;
      else
        if(x &gt; 0.1202552504837513f) // 100
          return 0b1001;
        else
          return 0b1000;
  else
    if(x &gt; -0.33967943489551544f) // 0
      if(x &gt; -0.13791173323988914f) // 01
        if(x &gt; -0.045525018125772476f) // 011
          return 0b0111;
        else
          return 0b0110;
      else
        if(x &gt; -0.23460740596055984f) // 010
          return 0b0101;
        else
          return 0b0100;
    else
      if(x &gt; -0.6106329262256622f) // 00
        if(x &gt; -0.4599952697753906f) // 001
          return 0b0011;
        else
          return 0b0010;
      else
        if(x &gt; -0.8480964004993439f) // 000
          return 0b0001;
        else
          return 0b0000;
}</code></pre><p>I don&#8217;t know about you, but I find this code as clear as mud. It clarifies things to explicitly create a new lookup table from the midpoints:</p><pre><code>constexpr float NF4_LUT_mid[16] = {
    0.0f,
    NF4_LUT[ 0] + 0.5f * ( NF4_LUT[ 1] - NF4_LUT[ 0] ),
    NF4_LUT[ 1] + 0.5f * ( NF4_LUT[ 2] - NF4_LUT[ 1] ),
    NF4_LUT[ 2] + 0.5f * ( NF4_LUT[ 3] - NF4_LUT[ 2] ),
    NF4_LUT[ 3] + 0.5f * ( NF4_LUT[ 4] - NF4_LUT[ 3] ),
    NF4_LUT[ 4] + 0.5f * ( NF4_LUT[ 5] - NF4_LUT[ 4] ),
    NF4_LUT[ 5] + 0.5f * ( NF4_LUT[ 6] - NF4_LUT[ 5] ),
    NF4_LUT[ 6] + 0.5f * ( NF4_LUT[ 7] - NF4_LUT[ 6] ),
    NF4_LUT[ 7] + 0.5f * ( NF4_LUT[ 8] - NF4_LUT[ 7] ),
    NF4_LUT[ 8] + 0.5f * ( NF4_LUT[ 9] - NF4_LUT[ 8] ),
    NF4_LUT[ 9] + 0.5f * ( NF4_LUT[10] - NF4_LUT[ 9] ),
    NF4_LUT[10] + 0.5f * ( NF4_LUT[11] - NF4_LUT[10] ),
    NF4_LUT[11] + 0.5f * ( NF4_LUT[12] - NF4_LUT[11] ),
    NF4_LUT[12] + 0.5f * ( NF4_LUT[13] - NF4_LUT[12] ),
    NF4_LUT[13] + 0.5f * ( NF4_LUT[14] - NF4_LUT[13] ),
    NF4_LUT[14] + 0.5f * ( NF4_LUT[15] - NF4_LUT[14] ),
};</code></pre><p>and rewrite the function, replacing the constants with references to <code>NF4_LUT_mid</code>. The resulting code highlights that we&#8217;re building indices one bit at a time:</p><pre><code>inline
unsigned char
dQuantizeNF4_1(float x)
{
  // the values for this tree was generated by test_normal_map_tree
  // in the file tests/test_functional.py
  if(x &gt; NF4_LUT_mid[0b1000] )
    if(x &gt; NF4_LUT_mid[0b1100]) // 1
      if(x &gt; NF4_LUT_mid[0b1110]) // 11
        if(x &gt; NF4_LUT_mid[0b1111]) // 111
          return 0b1111;
        else
          return 0b1110;
      else
        if(x &gt; NF4_LUT_mid[0b1101]) // 110
          return 0b1101;
        else
          return 0b1100;
    else
      if(x &gt; NF4_LUT_mid[0b1010] ) // 10
        if(x &gt; NF4_LUT_mid[0b1011] ) // 101
          return 0b1011;
        else
          return 0b1010;
      else
        if(x &gt; NF4_LUT_mid[0b1001] ) // 100
          return 0b1001;
        else
          return 0b1000;
  else
    if(x &gt; NF4_LUT_mid[0b0100]) // 1
      if(x &gt; NF4_LUT_mid[0b0110]) // 11
        if(x &gt; NF4_LUT_mid[0b0111]) // 111
          return 0b0111;
        else
          return 0b0110;
      else
        if(x &gt; NF4_LUT_mid[0b0101]) // 110
          return 0b0101;
        else
          return 0b0100;
    else
      if(x &gt; NF4_LUT_mid[0b0010] ) // 10
        if(x &gt; NF4_LUT_mid[0b0011] ) // 101
          return 0b0011;
        else
          return 0b0010;
      else
        if(x &gt; NF4_LUT_mid[0b0001] ) // 100
          return 0b0001;
        else
          return 0b0000;
}</code></pre><p>If the pattern doesn&#8217;t immediately jump out at you, look at an innermost <code>if</code> statement:</p><pre><code>        if(x &gt; NF4_LUT_mid[0b1111]) // 111
          return 0b1111;
        else
          return 0b1110;</code></pre><p>and note that if the condition is met, the bit that was tested is set. The same is true of the outer <code>if</code> statements; they are just further removed from the index construction. A<em><strong>ll</strong></em> of the statements within a given conditional, set the bits controlled by <em><strong>all</strong></em> of the <code>if</code> statements that led to that code path.</p><p>Another way to look at this code: each <code>if</code> statement conditionally OR&#8217;s a bit into the final output.</p><p>Sound familiar? It should - it is akin to Bentley&#8217;s optimized Binary Search!</p><h1>Enter AVX512</h1><p>Aside: The bitsandbytes code referenced above is CUDA code, not CPU code; but we were able to lift it verbatim to use in our CPU benchmarks. I am not sure if the nested <code>if</code> statements compile to the best GPU code &#8211; I suspect that putting the LUT in shared memory or using warp shuffles would do the same calculation more efficiently &#8211; but for now, I want to take a look at how we can combine Bentley&#8217;s awesome Binary Search optimization with the <code>VPERMPS</code> instruction from AVX512 to compute 16 of these indices at a time for a humongous speedup.</p><p><code>VPERMPS</code>, which may be accessed via intrinsics such as <em>_mm512_permutexvar_ps()</em>, essentially performs a register-to-register lookup, using the least significant 4 bits of the SIMD lanes in one register to index into another register. On both AVX2 and AVX512, <code>VPERMPS</code> is one of the few lane-crossing instructions that can be brought to full effect across the entire register. (Many AVX2 instructions, in particular, operate separately on the top and bottom halves of the register.)</p><p>An AVX-512 implementation of the quantization algorithm may be implemented as follows:</p><pre><code>void
float_to_NF4_16( uint32_t *out, const float *base, size_t N, const float *f )
{
    __m512i v_i = _mm512_setzero_si512();
    __m512 v_f = _mm512_load_ps( f );
    const __m512 v_lut = _mm512_loadu_ps( &amp;NF4_LUT_mid[0] );
    auto round = [v_lut, &amp;v_i, v_f]( int N2 ) -&gt; void {
        __m512i v_i_N2 = _mm512_add_epi32( v_i, _mm512_set1_epi32( N2 ) );
        __m512 v_lut_i_N2 = _mm512_permutexvar_ps( v_i_N2, v_lut );
        __mmask16 mask_gt = _mm512_cmp_ps_mask( v_f, v_lut_i_N2, _CMP_GE_OS );
        v_i = _mm512_mask_add_epi32( v_i, mask_gt, v_i, _mm512_set1_epi32( N2 )     );
    };
    round( 8 );
    round( 4 );
    round( 2 );
    round( 1 );
    _mm512_store_si512( (__m512i *) out, v_i );
}</code></pre><p>The lambda is used for a DRY (do not repeat yourself) pattern, echoing the unrolled Binary Search from before. It&#8217;s as clear as intrinsics-based AVX512 code can be, which is to say, <em><strong>not very.</strong></em> But it is fast as hell! The whole function <a href="https://godbolt.org/z/PeGbzTz6Y">compiles to about 18 machine instructions</a>, with no loops, just a fallthrough execution <em><strong>constructing 16 output values in parallel</strong></em>:</p><pre><code>        vmovaps zmm0, zmmword ptr [rcx]
        vmovdqa64       zmm1, zmmword ptr [rip + NF4_LUT_mid]
        vcmpgeps        k1, zmm0, dword ptr [rip + NF4_LUT_mid+32]{1to16}
        vpbroadcastd    zmm2 {k1} {z}, dword ptr [rip + .LCPI0_0]
        vpord   zmm3, zmm2, dword ptr [rip + .LCPI0_1]{1to16}
        vpermd  zmm4, zmm3, zmm1
        vcmpleps        k1, zmm4, zmm0
        vmovdqa32       zmm2 {k1}, zmm3
        vpord   zmm3, zmm2, dword ptr [rip + .LCPI0_2]{1to16}
        vpermd  zmm4, zmm3, zmm1
        vcmpleps        k1, zmm4, zmm0
        vmovdqa32       zmm2 {k1}, zmm3
        vpord   zmm3, zmm2, dword ptr [rip + .LCPI0_3]{1to16}
        vpermd  zmm1, zmm3, zmm1
        vcmpleps        k1, zmm1, zmm0
        vmovdqa32       zmm2 {k1}, zmm3
        vmovdqa64       zmmword ptr [rdi], zmm2
        vzeroupper
        ret</code></pre><p>On my old AMD CPU, it is about 30x faster than the fastest scalar implementation, which ironically is the brute force algorithm.</p><h1>A 30x Speedup</h1><p>The output from my test program, which is checked into the <a href="https://github.com/ArchaeaSoftware/parallelprogrammer/tree/master/nf4">Parallel Programmer GitHub repository</a>, reads as follows and shows a 29.55x speedup from AVX-512:</p><pre><code>gold: 23.64 clocks/iteration
binsearch: 74.22 clocks/iteration
bitsandbytes: 48.63 clocks/iteration
AVX512: 0.80 clocks/iteration</code></pre><p>(The &#8220;clocks&#8221; are just RDTSC ticks, which are an apples-to-apples comparison as long as the comparisons are being done on the same CPU and the CPU implements invariant RDTSC. On Linux, you can check for this CPU feature by searching <code>/proc/cpuinfo</code> for <code>constant_tsc</code>.)</p><p>A more recent AVX-512 implementation might be even faster. My testing was done on my trusty Ryzen 7 7700X, the first available AVX-512 capable CPU from AMD.</p><p>An AVX2 implementation would be more complicated, because although we do have a register-to-register lookup, there are only 8 32-bit lanes, so the LUT does not fit in a single register. So on the one hand, probably you&#8217;d have to select between two possible LUT values in each of the 4 rounds of the calculation; on the other, AVX2 implementations tend to have such great microarchitectures that it&#8217;d still be very fast. The <code>VPBLEND</code> instruction would be our friend, maybe for some other day.</p><h1>On Intrinsics v Hand-Coding</h1><p>One final word: just yesterday, a Twitter commentator <a href="https://x.com/neogoose_btw/status/1979722567553519802">intimated that anyone using intrinsics could instead be hand-coding in assembly language</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YrkZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YrkZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png 424w, https://substackcdn.com/image/fetch/$s_!YrkZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png 848w, https://substackcdn.com/image/fetch/$s_!YrkZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png 1272w, https://substackcdn.com/image/fetch/$s_!YrkZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YrkZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png" width="880" height="623" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:623,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:116759,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/176568054?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YrkZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png 424w, https://substackcdn.com/image/fetch/$s_!YrkZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png 848w, https://substackcdn.com/image/fetch/$s_!YrkZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png 1272w, https://substackcdn.com/image/fetch/$s_!YrkZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4c214111-f7ed-4a64-a9fb-3107846b2c91_880x623.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I used to belong in that camp, sort of; though in my defense 1) I was writing image processing code where the C programming language&#8217;s compulsion to promote short integers to the machine&#8217;s natural word length was constantly inhibiting my efforts at <a href="https://en.wikipedia.org/wiki/SWAR">SWAR</a>, and 2) I was competing with 1980s-era compiler technology, before SIMD was widely available, on architectures that hadn&#8217;t benefited from decades of hardware/software codesign between compilers and hardware ISAs. In the intervening years, I&#8217;ve grown to appreciate compilers that will do register allocation and instruction scheduling on my behalf.</p><p>Apropos to the topic at hand, the exercise I invite you to undertake &#8211; hypothetically, if not literally &#8211; is to first, look over the generated code and see if you could improve upon it by hand-coding. </p><p>Next, imagine how we transform this function into one that generates the actual intended output of 4 bits per element. Currently it computes 16x32b elements. To compress these further into 4b elements invites a 4x unroll of this function that demotes and interleaves 32b-&gt;16b-&gt;8b-&gt;4b, which would unlock rich ILP opportunities. It might be a day&#8217;s work with intrinsics, but it would be many times more work to hand-code, and unlikely to be faster.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[CUDA Error Handling: A Definitive Guide]]></title><description><![CDATA[There are right and wrong ways.]]></description><link>https://parallelprogrammer.substack.com/p/cuda-error-handling-a-definitive</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/cuda-error-handling-a-definitive</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Mon, 13 Oct 2025 13:03:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!fv21!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Considering CUDA is almost 20 years old, there is a surprising absence of consensus on how to check for and handle errors, even within NVIDIA&#8217;s own sample code. There&#8217;s universal recognition that you should check error codes, but the developer education materials are not very prescriptive as to how.</p><p>Now.. I am here to tell you that everyone is doing this wrong.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fv21!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fv21!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png 424w, https://substackcdn.com/image/fetch/$s_!fv21!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png 848w, https://substackcdn.com/image/fetch/$s_!fv21!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png 1272w, https://substackcdn.com/image/fetch/$s_!fv21!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fv21!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png" width="799" height="781" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d4b0b022-c921-478e-81f2-0ee081485697_799x781.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:781,&quot;width&quot;:799,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:721637,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/176026525?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fv21!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png 424w, https://substackcdn.com/image/fetch/$s_!fv21!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png 848w, https://substackcdn.com/image/fetch/$s_!fv21!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png 1272w, https://substackcdn.com/image/fetch/$s_!fv21!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4b0b022-c921-478e-81f2-0ee081485697_799x781.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><em>The CUDA Handbook</em> has settled on a fairly rigid set of rules around error handling, which offer the best combination of conciseness, correct error handling, and portability to HIP without extra build steps:</p><ol><li><p>Check every error code.</p></li><li><p><a href="https://parallelprogrammer.substack.com/p/asynchronous-error-handling-is-hard">Use </a><em><a href="https://parallelprogrammer.substack.com/p/asynchronous-error-handling-is-hard">cudaGetLastError()</a></em><a href="https://parallelprogrammer.substack.com/p/asynchronous-error-handling-is-hard"> sparingly.</a></p></li><li><p>Use <code>goto</code> for error handling and cleanup. (see &#8220;<a href="https://parallelprogrammer.substack.com/p/go-to-statement-considered-occasionally">goto statement considered occasionally useful</a>&#8221;)</p></li><li><p>Do not check error codes for API calls that free resources.</p></li></ol><p>A template for a function that allocates and frees resources looks something like this:</p><pre><code>cudaError_t
allocateAndUseEvent( size_t N, &#8230; )
{
    cudaError_t status_cudart;
    cudaEvent_t event = 0; // IMPORTANT - code not correct w/o initialization
    cuda(EventCreate( &amp;event ) );
    &#8230;
Error_cudart:
    cudaEventDestroy( event ); // note - error code not checked!
    return status_cudart;
}</code></pre><p>If the function allocates resources on behalf of its caller, the code immediately above the <code>Error_cudart</code> label would free resources as needed, then return <code>cudaSuccess</code>.</p><p>Your Mileage May Vary&#8482;. The CUDA Handbook macros impose some policy on their users: the <code>status_cudart</code> variable and <code>Error_cudart</code> label must be defined, for example. You may wish to implement different policies (the SDK samples typically print an error and exit the process, rather than propagating the error to a caller), give your error label a different name (we recently changed the names of both the status variable and the error label to enable the admixture of different API families in the same function), or make other changes based on your application&#8217;s needs.</p><p>For conciseness, our macros prepend &#8216;<code>cuda</code>&#8217; to the front of the function on behalf of the caller, so functions such as the above one actually cannot be called with our default error handling macros. That said, it is uncommon to write our own functions that propagate native CUDA errors &#8211; more typically, when an error is encountered, we transmute it into our own error handling regime before propagating it. For those rare cases, I left the old <code>CUDART_CHECK</code> macro in place in <code>chError.h</code>.</p><p>Under no circumstances, however, should you incorporate <em>cudaGetLastError()</em> into your daily error-checking routine. It is just as amenable to invocation via the error handling macros as other CUDA functions, and almost never needs to be called explicitly. <em>The only circumstance when </em>cudaGetLastError()<em> must be called is when a kernel launch may have been misconfigured. </em>In other cases, such as to detect when a running kernel has encountered a memory fault, you can rely on functions such as <em>cudaDeviceSynchronize() </em>. For more context, take a look at my <a href="https://parallelprogrammer.substack.com/p/asynchronous-error-handling-is-hard">previous article on asychronous error handling</a>.</p><p>If all you wanted was a page-long description of the CUDA Handbook&#8217;s philosophy on error handling, <strong>you can stop reading now</strong>. The rest of the article gives an overview of the state of affairs and, for those building applications portable to AMD&#8217;s ROCm platform, an honorable mention to our error handling macros&#8217; provisions for stealth HIPification.</p><div><hr></div><h1>State Of Affairs</h1><p>We&#8217;ll start with a quick overview of the CUDA SDK Sample code.</p><p>The interval arithmetic sample has a typical error handling macro that prints a message to <code>stderr</code> and exits the process if CUDA returns an error:</p><pre><code>#define CHECKED_CALL(func)  \
    do {                    \
        cudaError_t err = (func); \
        if (err != cudaSuccess) { \
            printf(               \
                &#8220;%s(%d): ERROR: %s returned %s (err#%d)\n&#8221;, __FILE__, __LINE__, #func, cudaGetErrorString(err), err); \
            exit(EXIT_FAILURE);   \
        }                         \
    } while (0)</code></pre><p>The NPP samples have error handling macros of their own:</p><pre><code>#define NPP_CHECK_CUDA(S) do {cudaError_t eCUDAResult; \
        eCUDAResult = S; \
        if (eCUDAResult != cudaSuccess) std::cout &lt;&lt; &#8220;NPP_CHECK_CUDA - eCUDAResult = &#8220; &lt;&lt; eCUDAResult &lt;&lt; std::endl; \
        NPP_ASSERT(eCUDAResult == cudaSuccess);} while (false)</code></pre><p>But most samples call a macro <em>checkCudaErrors()</em> that is defined in a variety of places, resulting in code that looks like <a href="https://github.com/NVIDIA/cuda-samples/blob/c94ff366aed18c797b8a85dfaac7817b0228b420/Samples/3_CUDA_Features/graphConditionalNodes/graphConditionalNodes.cu#L114C1-L128C46">this fragment</a> from <code>graphConditionalNodes.cu</code>:</p><pre><code>    checkCudaErrors(cudaGraphAddNode(&amp;bodyNode, bodyGraph, NULL, NULL, 0, &amp;params));
    checkCudaErrors(cudaGraphInstantiate(&amp;graphExec, graph, NULL, NULL, 0));
    // Initialize device memory and launch the graph
    checkCudaErrors(cudaMemset(dPtr, 0, 1)); // Set dPtr to 0
    printf(&#8221;Host: Launching graph with device memory set to 0\n&#8221;);
    checkCudaErrors(cudaGraphLaunch(graphExec, 0));
    checkCudaErrors(cudaDeviceSynchronize());
    // Initialize device memory and launch the graph
    checkCudaErrors(cudaMemset(dPtr, 1, 1)); // Set dPtr to 1
    printf(&#8221;Host: Launching graph with device memory set to 1\n&#8221;);
    checkCudaErrors(cudaGraphLaunch(graphExec, 0));
    checkCudaErrors(cudaDeviceSynchronize());</code></pre><p>There is little to no need for such verbosity in our error handling.</p><h1>Unobtrusive Error Handling</h1><p>If you define a macro <code>cuda</code> that prepends &#8220;<code>cuda</code>&#8221; to the function being called, your API calls become more succinct with no loss of correctness. Let&#8217;s rewrite the series of CUDA calls from <code>graphConditionalNodes.cu</code>:</p><pre><code>    cuda(GraphAddNode(&amp;bodyNode, bodyGraph, NULL, NULL, 0, &amp;params));
    cuda(GraphInstantiate(&amp;graphExec, graph, NULL, NULL, 0));
    // Initialize device memory and launch the graph
    cuda(Memset(dPtr, 0, 1)); // Set dPtr to 0
    printf(&#8221;Host: Launching graph with device memory set to 0\n&#8221;);
    cuda(GraphLaunch(graphExec, 0));
    cuda(DeviceSynchronize());
    // Initialize device memory and launch the graph
    cuda(Memset(dPtr, 1, 1)); // Set dPtr to 1
    printf(&#8221;Host: Launching graph with device memory set to 1\n&#8221;);
    cuda(GraphLaunch(graphExec, 0));
    cuda(DeviceSynchronize());</code></pre><p>The refactored code is doing <em>exactly the same thing</em>, but more concisely.</p><p>For <em>The CUDA Handbook</em> source code, we don&#8217;t handle errors the same way that NVIDIA&#8217;s sample code does, which typically is to print an error and exit the process. Instead, we use a <code>goto</code>-based error handling scheme to clean up, if necessary, before returning the error to our caller. The <a href="https://github.com/ArchaeaSoftware/cudahandbook/blob/60cf66e3d1e2d92f61295d173bb4793412cce66e/chLib/chError_cuda.h#L195C1-L204C1">macro</a> looks like this:</p><pre><code>#define cuda( fn ) do { \
        (status_cudart) =  (cuda##fn); \
        if ( cudaSuccess != (status_cudart) ) { \
            fprintf( stderr, &#8220;CUDA Runtime Failure (line %d of file %s):\n\t&#8221; \
                &#8220;%s returned 0x%x (%s)\n&#8221;, \
                __LINE__, __FILE__, #fn, status_cudart, chGetErrorString(status_cudart) ); \
            goto Error_cudart; \
        } \
    } while (0)</code></pre><p>The macro enables long sequences of CUDA calls, some of which may fail, to be invoked concisely and correctly. See for example <a href="https://github.com/ArchaeaSoftware/cudahandbook/blob/60cf66e3d1e2d92f61295d173bb4793412cce66e/streaming/stream1Device.cu#L136C1-L148C35">this series</a> of resource allocations and memory copies in <code>stream1Device.cu</code>:</p><pre><code>cuda(Malloc( &amp;dptrOut, N*sizeof(float) ) );
cuda(Memset( dptrOut, 0, N*sizeof(float) ) );
cuda(Malloc( &amp;dptrY, N*sizeof(float) ) );
cuda(Memset( dptrY, 0, N*sizeof(float) ) );
cuda(Malloc( &amp;dptrX, N*sizeof(float) ) );
cuda(Memset( dptrY, 0, N*sizeof(float) ) );
cuda(EventCreate( &amp;evStart ) );
cuda(EventCreate( &amp;evHtoD ) );
cuda(EventCreate( &amp;evKernel ) );
cuda(EventCreate( &amp;evDtoH ) );</code></pre><p>This code is taking advantage of the timing features in CUDA events to separately measure the host-to-device, kernel execution, and device-to-host runtimes. If we were dedicating three lines of code to the error handling, the code fragment would be much harder to understand.</p><h1>Stealth HIPification</h1><p>For those aspiring to port their workloads to HIP, AMD&#8217;s rough equivalent to CUDA, we need only modify the macro to prepend &#8220;<code>hip</code>&#8221; instead of &#8220;<code>cuda</code>&#8221; to the API call. In the <em>CUDA Handbook</em> source code, this prompted me to split the headers into a <code>chError_cuda.h</code> and <code>chError_hip.h</code>. When using this approach to HIPify, the preprocessor also must be enlisted to transform error codes and constants such as <code>cudaMemcpyHostToDevice</code>. Additionally, our <code>goto</code>-based error handling scheme requires that we explicitly ignore the error codes from functions such as <em>cudaFree()</em> and <em>cudaStreamDestroy()</em>; so the HIP edition of our error handling header file must include preprocessor transformations for those functions, as well.</p><p>I wish the itinerant maintainer of the HIPify-perl and HIPify-clang tools would focus instead on a canonical header file to solve this problem in an officially-supported manner; in the meantime, would-be adopters of HIP are left to either use the preprocessor, as I have chosen to do, or use source-to-source translation to preprocess their source files.</p><h1>Conclusion</h1><p>Error handling is at once pervasive and prone to pitfalls. CUDA developers can write incorrect code, or code that is doing superfluous error checks, with disturbing ease. I&#8217;d encourage anyone maintaining a significant CUDA code base to periodically do an &#8220;error handling audit,&#8221; and make sure errors are being checked and handled correctly.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[A Missive From The RISC/CISC War]]></title><description><![CDATA[From A Windows NT Perspective]]></description><link>https://parallelprogrammer.substack.com/p/a-missive-from-the-risccisc-war</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/a-missive-from-the-risccisc-war</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Tue, 07 Oct 2025 13:03:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Ed. Note: This account of the author&#8217;s tech career is excerpted from the chapter on his early Microsoft career, in the Advanced Consumer Technology (ACT) division.</em> <em>Under Craig Mundie&#8217;s leadership, ACT had purchased Softimage, the company whose software for Silicon Graphics workstations had been used to animate the dinosaurs in </em>Jurassic Park<em>. When Softimage balked at porting their application to Windows NT, Microsoft funded a Redmond-based team to do the port for them.</em></p>
      <p>
          <a href="https://parallelprogrammer.substack.com/p/a-missive-from-the-risccisc-war">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[3rd Largest Element: SIMD Edition]]></title><description><![CDATA[Is SIMD acceleration worth it? Let's find out!]]></description><link>https://parallelprogrammer.substack.com/p/3rd-largest-element-simd-edition</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/3rd-largest-element-simd-edition</guid><pubDate>Tue, 30 Sep 2025 15:00:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jiPc!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F964a9fbf-4407-4e13-950b-893c39856632_140x140.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A previous article described a solution to a typical l33tcode interview question, and mainly explored 1) how modern CPUs prefer predictable, cache-friendly memory access patterns, and 2) how algorithmic complexity can impact runtime. We alluded to SIMD-accelerated variants of the solution, but didn&#8217;t explore them in depth.</p><p>This followup article describes the SIMD implementations of the original &#8220;Third Largest&#8221; question, with performance results. In the earlier article, we wanted to know: what values of <em>k</em> cause the asymptotically-faster Heap version of the algorithm to actually be faster than the Sort version? The question explored in this article: With the aid of SIMD, how much faster can the Sort version go? And, it is worth the effort?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Quoting from the previous article:</p><blockquote><p>So far, we&#8217;ve explored the design space that a good coder might be able to explore during a coding interview. We&#8217;ve been good computer science students and used our algorithm analysis skills, replacing an O(<em>Nk</em>) algorithm with an O(<em>N</em><code>lg</code><em>k</em>) algorithm. But what if we give ourselves permission to revisit the sorting algorithm, with the help of SIMD? Can the insertion step for Insertion Sort be implemented in O(1) time on a small array?</p><p>Yes. Yes, it can. But exploring that implementation strategy will be left for another article.</p></blockquote><p>And here we are.</p><p>It bears mention that the SIMD-accelerated implementations of this function only work for small <em>k</em>, namely <em>k</em>&lt;=8. We&#8217;ll touch on implementation strategies for any k, and I believe SIMD instruction sets will deliver higher performance than equivalent scalar implementations, but today we are only talking about small <em>k</em>.</p><p>An AVX register can hold a <code>maxSoFar</code> array of up to size 8, and with the right instruction sequence, we can identify where a candidate value must be inserted, conditionally shift the SIMD lanes of the <code>maxSoFar</code> register, and insert the incoming value in sorted order&#8212;all using a handful of SIMD instructions.</p><pre><code>inline void insertNewMax_x4 ( __m128i&amp; v_maxSoFar, __m128i x ) {
    __m128i v_cmplt = _mm_cmplt_epi32( v_maxSoFar, x );
    __m128i v_insertion_mask = _mm_xor_si128( v_cmplt, _mm_srli_si128( v_cmplt, 4 ) );
    __m128i v_shift1 = _mm_srli_si128( v_maxSoFar, 4 );

    v_maxSoFar = _mm_blendv_epi8( v_maxSoFar, v_shift1, v_cmplt );
    v_maxSoFar = _mm_blendv_epi8( v_maxSoFar, x, v_insertion_mask );
}

inline void insertNewMax( __m128i&amp; v_maxSoFar, int32_t x ) {
    insertNewMax_x4( v_maxSoFar, _mm_set1_epi32( x ) );
}</code></pre><p>NOTE: The <em>_mm_srli_si128()</em> intrinsic has two properties that are not obvious at first blush. First, the immediate parameter specifies a byte count. Secondly, the 256-bit analog of this function performs the requested shift on the upper and lower halves of the 256-bit register, placing <em>_mm256_srli_si256()</em> firmly in the <em><strong>Set Of Annoying Nonorthogonal x86 SIMD instructions</strong></em>.</p><p>The above function updates <code>maxSoFar</code>, not in <em>O</em>(<code>lg</code><em>k</em>) time, but in a few instructions: we broadcast the candidate value across one SIMD register, then compare against the values already in the <code>maxSoFar</code> register. If the candidate is &#8220;less than&#8221; all of the values in <code>maxSoFar</code>, the result is a mask of 0&#8217;s, and the subsequent operations do nothing; but if the candidate needs to be inserted into <code>maxSoFar</code>, the mask is 1&#8217;s in the lanes where the <code>maxSoFar</code> values are less than the candidate, then 0&#8217;s. The lane where the candidate must be inserted can be isolated with a shift and <code>XOR</code> operation. We then use the <code>VBLENDPD</code> instruction (wrapped with the <em>_mm_blend_epi8()</em> intrinsic) to conditionally overwrite <code>maxSoFar</code>, first with a shifted version of itself to make room for the candidate if it will be inserted, then with the candidate value in the lane where it must land.</p><p>As with most SIMD coding, this code is not work-efficient! It is comparing and shifting and masking and blending 4 SIMD lanes at a time, zeroing in on the lanes that need to be shifted and/or replaced (and if we needed a bigger <code>maxSoFar</code> array, we could rewrite to use AVX2 or AVX512 sized registers, which would hold 8 or 16 values, respectively).</p><p>The AVX2 formulation of the third-largest problem, using an SSE2-valued <code>maxSoFar</code> array, is as follows:</p><pre><code>int32_t
thirdLargest_avx2( const std::vector&lt;int32_t&gt;&amp; v )
{
    int32_t minMax;
    __m128i v_maxSoFar;

    {
        v_maxSoFar = _mm_set_epi32( std::numeric_limits&lt;int32_t&gt;::max(),
                                    std::numeric_limits&lt;int32_t&gt;::min(),
                                    std::numeric_limits&lt;int32_t&gt;::min(),
                                    std::numeric_limits&lt;int32_t&gt;::min() );
        insertNewMax( v_maxSoFar, v[0] );
        insertNewMax( v_maxSoFar, v[1] );
        insertNewMax( v_maxSoFar, v[2] );

    }

    minMax = _mm_cvtsi128_si32( v_maxSoFar );
    size_t i = 3;
    while ( i &lt; v.size() ) {
        if ( v[i] &gt; minMax ) {
            insertNewMax( v_maxSoFar, v[i] );
            minMax = _mm_cvtsi128_si32( v_maxSoFar );
        }
        ++i;
    }

    return minMax;
}</code></pre><p>The initialization of <code>v_maxSoFar</code> uses sentinels to ensure the first 3 input values are inserted correctly:</p><pre><code><code>        v_maxSoFar = _mm_set_epi32( std::numeric_limits&lt;int32_t&gt;::max(),
                                    std::numeric_limits&lt;int32_t&gt;::min(),
                                    std::numeric_limits&lt;int32_t&gt;::min(),
                                    std::numeric_limits&lt;int32_t&gt;::min() );
</code></code></pre><p>For <em>k</em>==2, we&#8217;d instead initialize as:</p><pre><code><code>        v_maxSoFar = _mm_set_epi32( std::numeric_limits&lt;int32_t&gt;::max(),
                                    std::numeric_limits&lt;int32_t&gt;::max(),
                                    std::numeric_limits&lt;int32_t&gt;::min(),
                                    std::numeric_limits&lt;int32_t&gt;::min() );
</code></code></pre><p>The code then scans the input array, checking to see if the new candidate element <code>v[i]</code> should be inserted into <code>v_minMax</code>:</p><pre><code><code>    minMax = _mm_cvtsi128_si32( v_maxSoFar );
    size_t i = 3;
    while ( i &lt; v.size() ) {
        if ( v[i] &gt; minMax ) {
            insertNewMax( v_maxSoFar, v[i] );
            minMax = _mm_cvtsi128_si32( v_maxSoFar );
        }
        ++i;
    }</code></code></pre><p>Using <code>clang</code> per <a href="https://godbolt.org/z/TqMrv7vq4">Godbolt</a>, this code features only 9 AVX2 instructions in the inner loop:</p><pre><code><code>.LBB0_2:
        mov     edi, dword ptr [rcx + 4*rsi]
        cmp     edi, eax
        jle     .LBB0_4
        vmovd   xmm1, edi
        vpbroadcastd    xmm1, xmm1
        vpcmpgtd        xmm2, xmm1, xmm0
        vpsrldq xmm3, xmm2, 4
        vpsrldq xmm4, xmm0, 4
        vblendvps       xmm0, xmm0, xmm4, xmm2
        vpxor   xmm2, xmm3, xmm2
        vpblendvb       xmm0, xmm0, xmm1, xmm2
        vmovd   eax, xmm0
        jmp     .LBB0_4</code></code></pre><p>With SIMD, of course a further optimization is possible: Instead of checking for <code>v[i] &gt; minMax</code>, why not check the next eight (8) incoming values to see if any of them require insertion? If none of them do, we can skip ahead by 8 values, not 1. But if any of them do, we have to find the value or values inside the AVX2 register that need to be inserted. Superficially, it seems the likelihood of each new candidate requiring insertion into <code>v_maxSoFar</code> should go down as the algorithm progresses.</p><p>The loop structure looks something like this:</p><pre><code><code>__m256i v_minMax = _mm256_broadcastd_epi32( v_maxSoFar );
size_t Nleft = v.size() - 3;
while ( Nleft &gt;= 8 ) {
    __m256i v_next = _mm256_loadu_si256( (__m256i *) (v.data() + i) );
    __m256i v_cmplt = _mm256_cmpgt_epi32( v_next, v_minMax );
    int mask_lt = _mm256_movemask_ps( _mm256_castsi256_ps( v_cmplt ) );
        
    // if mask_lt has any set bits, some lanes must be inserted into v_minMax

    v_minMax = _mm256_broadcastd_epi32( v_maxSoFar );
    Nleft -= 8;
    i += 8;
}</code></code></pre><p>This loop structure assumes that all 8 lanes of <code>v_next</code> will be processed in each loop iteration (hence the decrement of <code>Nleft</code> and increment of <code>i</code> by 8 at the end of the loop). That needn&#8217;t necessarily be the case; we could just insert the &#8220;first&#8221; (since x86 is little-endian, the least-significant lane), and increment the loop such that the next iteration of the loop restarts just after the just-inserted element. Since we are using unaligned loads, the code would work; it would do some redundant comparisons, but those comparisons would be against the updated <code>v_maxSoFar</code> array, so may result in less work being done. (There&#8217;s that pesky data dependence again!) </p><p>Here&#8217;s the loop with that logic filled in - the g++ intrinsic <em>ffs()</em> returns 0 for inputs of zero, otherwise the bit position of the least significant set bit, plus one. For example, <code>ffs(12)==3</code>. The resulting code - I call it <code>kthLargest_avx2_least</code>, because it inserts the smallest lane of the candidate input - looks like this:</p><pre><code>while ( Nleft &gt;= 8 ) {
    __m256i v_next = _mm256_loadu_si256( (__m256i *) (v.data() + i) );
    __m256i v_cmplt = _mm256_cmpgt_epi32( v_next, v_minMax );
    int mask_lt = _mm256_movemask_ps( _mm256_castsi256_ps( v_cmplt ) );

    int lsb = ffs( mask_lt );
    if ( lsb ) {
        __m256i v_newCandidate = _mm256_permutevar8x32_epi32( v_next,
            _mm256_broadcastd_epi32( _mm_cvtsi32_si128( lsb-1) ) );
        insertNewMax_x8( v_maxSoFar, v_newCandidate );
        minMax = _mm_cvtsi128_si32( _mm256_castsi256_si128( v_maxSoFar ) );
        v_minMax = _mm256_set1_epi32( minMax );

        Nleft -= lsb;
        i += lsb;
    }
    else {
        Nleft -= 8;
        i += 8;
    }
}</code></pre><p>Alternatively, if <code>mask_lt</code> is nonzero, indicating that there is at least one lane whose value must be inserted into <code>v_maxSoFar</code>, we can fully process the candidate input, inserting each lane for which the corresponding bit in <code>mask_lt</code> is nonzero. The <code>while</code> loop in this code never executes if <code>mask_lt==0</code>. I call this variant <em>kthLargest_avx2_peel()</em>, because it peels and inserts the smallest lane in turn:</p><pre><code>while ( Nleft &gt;= 8 ) {
    __m256i v_next = _mm256_loadu_si256( (__m256i *) (v.data() + i) );
    __m256i v_cmplt = _mm256_cmpgt_epi32( v_next, v_minMax );
    int mask_lt = _mm256_movemask_ps( _mm256_castsi256_ps( v_cmplt ) );

    int mask_done = -1;
    while ( 0 != (mask_lt&amp;mask_done) ) {
        int lsb = ffs( mask_done &amp; mask_lt );
        __m128i v_newCandidate = _mm256_castsi256_si128(
            _mm256_permutevar8x32_epi32( v_next,
                _mm256_broadcastd_epi32( _mm_cvtsi32_si128( lsb-1) ) ) );
        insertNewMax_x4( v_maxSoFar, v_newCandidate );
        mask_done = ~((1&lt;&lt;lsb)-1);
    }
    v_minMax = _mm256_broadcastd_epi32( v_maxSoFar );
    Nleft -= 8;
    i += 8;
}</code></pre><p>A final variant would unconditionally insert all 8 lanes, if <em>any</em> of them would trigger insertion. This variant relies heavily on the <em>insertNewMax_x4()</em> function being a NOP for inputs that should not be inserted. The code uses a lambda to avoid code duplication: </p><pre><code>while ( Nleft &gt;= 8 ) {
    __m256i v_next = _mm256_loadu_si256( (__m256i *) (v.data() + i) );
    __m256i v_cmplt = _mm256_cmpgt_epi32( v_next, v_minMax );
    int mask_lt = _mm256_movemask_ps( _mm256_castsi256_ps( v_cmplt ) );
    auto insert_lane = [&amp;v_maxSoFar, v_next]( int i ) {
        __m128i v_nexti = _mm256_castsi256_si128( 
            _mm256_permutevar8x32_epi32( v_next,
                _mm256_broadcastd_epi32( _mm_cvtsi32_si128( i ) ) ) );
        insertNewMax_x4( v_maxSoFar, v_nexti );
    };

    if ( mask_lt ) {
        insert_lane( 0 );
        insert_lane( 1 );
        insert_lane( 2 );
        insert_lane( 3 );
        insert_lane( 4 );
        insert_lane( 5 );
        insert_lane( 6 );
        insert_lane( 7 );
    }
    v_minMax = _mm256_broadcastd_epi32( v_maxSoFar );
    Nleft -= 8;
    i += 8;
}</code></pre><p>All of these AVX2 implementations have the following properties in common: </p><ul><li><p>they hold the <code>maxSoFar</code> &#8216;array&#8217; in an AVX2 register (which we call <code>v_maxSoFar)</code>;</p></li><li><p>they can insert a candidate value into the correct lane of <code>v_maxSoFar</code> in a fixed handful of instructions - no looping required.</p></li><li><p>the technique works for any <em>k</em>&lt;=8 because an AVX2 register can hold up to 8x<code>int32_t</code>.</p></li></ul><p>For <em>k</em>&gt;8, multiple AVX2 registers may be used (up to 16, if running in 64-bit mode); or AVX512 may be used, doubling the number of elements per register to 16 and the number of registers to 32. If worse comes to worst, an array of <code>__m256i</code> or <code>__m512i</code> elements can be used, but for any <em>k</em>&lt;=512, an AVX512 implementation could keep <code>v_maxSoFar</code> in registers.</p><p>Next: Let&#8217;s find out whether all (any?) of this SIMD optimization was worth the effort!</p><h1>Performance Analysis</h1><p>We measure performance of the scalar implementations as well as the AVX2 implementations: the register formulation <em>thirdLargest()</em>; <em>kthLargest_heap()</em> and <em>kthLargest_sort()</em> with <em>k</em>==3, even though those reasonably can be expected to perform similarly; and the AVX2 implementations described in the previous section: </p><ul><li><p><em>thirdLargest_avx2()</em> inserts each input element in turn,</p></li><li><p><em>thirdLargest_avx2_least()</em> reads the next 8 input elements and, if any need to be inserted, inserts the first and advances the array index to point to the next, and</p></li><li><p><em>thirdLargest_avx2_peel()</em> processes 8 input elements at a time, inserting each element that was greater than the &#8220;minimum maximum&#8221;. Note, depending on the inputs, this formulation performs some redundant work, since processing elements earlier in the input array may obviate the need to process some subsequent elements.</p></li></ul><p>For random inputs, across ten (10) runs, the various implementations run at the following speeds (in gigaelements per second):</p><pre><code>nth - nth_element() STL
heap - kthElement_heap&lt;3&gt;
sort - kthElement_sort&lt;3&gt;
swap - thirdElement&lt;false&gt; - 
dlgt - thirdElement&lt;true&gt;, the Hacker's delight conditional swap
avx2 - naive AVX2 implementation, uses an AVX2 register only for v_maxSoFar

_x8 - AVX2 implementation that checks 8 elements at a time, and skips ahead by 8 if none of the incoming 8 candidates need to be inserted into v_maxSoFar

peel - AVX2 implementation that processes elements out of the incoming candidates, if any of them need to be inserted into v_maxSoFar

least - AVX2 implementation that checks 8 elements at a time and inserts the first into v_maxSoFar, then restarts the scan at the next element

Run  nth   heap  sort  swap  dlgt  avx2   _x8   peel   least
 1   0.27  5.43  5.43  2.76  2.75  2.75  14.55  14.41  14.51
 2   0.90  5.44  5.43  2.75  2.76  2.76  14.45  14.50  14.47
 3   0.28  5.43  5.43  2.75  2.76  2.75  14.62  14.49  14.61
 4   0.41  5.43  5.44  2.75  2.75  2.75  14.55  14.42  14.41
 5   0.25  5.43  5.44  2.75  2.76  2.75  14.51  14.41  14.44
 6   0.48  5.43  5.44  2.75  2.76  2.76  14.64  14.33  14.52
 7   0.69  5.43  5.44  2.75  2.76  2.76  14.62  14.45  14.56
 8   0.45  5.43  5.44  2.75  2.76  2.75  14.60  14.46  14.43
 9   0.43  5.44  5.42  2.75  2.75  2.75  14.69  14.49  14.51
10   0.29  5.44  5.44  2.76  2.76  2.76  14.45  14.34  14.55</code></pre><p>Some takeaways from this performance data:</p><ul><li><p><em>nth_element()</em> exhibits the most variable performance, with performance on a full-sized data set ranging from 0.26-0.86 billion elements per second (average 0.44, standard deviation of 0.20).</p></li><li><p>The benefits of sweeping the input array are clear: even the slowest implementation is about 10x faster than <em>nth_element()</em>. </p></li><li><p>The AVX2 implementations that &#8220;look ahead,&#8221; and skip 8 elements at a time when possible, all are about 50x faster than <em>nth_element()</em> and about 5x faster than <em>thirdElement()</em>.</p></li><li><p>The futility of the Hacker&#8217;s Delight cleverness (<code>dlgt</code> as opposed to <code>swap</code>) is laid bare: it is no faster than the &#8216;naive&#8217; swap, which is eminently more readable.</p></li><li><p>Interestingly, the generic <em>kthElement_heap()</em> and <em>kthElement_sort()</em> both run at the same speed, and both are noticeably faster than the <em>thirdElement()</em> versions. One would expect the implementations specific to <em>k</em>==3 to be a bit faster!</p></li></ul><p>For reverse-sorted input (minimum work per element), the results are similar: </p><pre><code>Run  nth   heap  sort  swap  dlgt  avx2   _x8   peel   least
 1   2.03  5.42  5.43  2.76  2.76  2.76  14.54  14.56  14.46
 2   2.08  5.42  5.42  2.75  2.76  2.75  14.67  14.59  14.38
 3   2.08  5.42  5.43  2.76  2.75  2.76  14.59  14.47  14.29
 4   2.08  5.41  5.43  2.76  2.75  2.75  14.52  14.44  14.44
 5   2.08  5.42  5.43  2.76  2.75  2.75  14.67  14.57  14.34
 6   2.08  5.40  5.42  2.75  2.75  2.75  14.65  14.27  14.43
 7   2.08  5.43  5.42  2.75  2.75  2.75  14.62  14.59  14.47
 8   2.08  5.42  5.43  2.75  2.75  2.76  14.71  14.49  14.48
 9   2.07  5.41  5.42  2.75  2.76  2.75  14.59  14.47  14.41
10   2.08  5.42  5.42  2.75  2.76  2.76  14.53  14.52  14.48</code></pre><p>The main takeaways from this data are:</p><ul><li><p>The scalar, and especially the AVX2 implementations that &#8220;look ahead,&#8221; barely run any faster than on randomized data; this is a surprise, since they should be running through the input, detecting that no updates to <code>v_MaxSoFar</code> are needed after initial setup; and</p></li><li><p>intriguingly, <em>nth_element()</em> is both more level (not as much variance in speed) and markedly faster; is that because a median-of-three pivot selection on a sorted array amounts to pivoting on the actual median every time? </p></li></ul><p>For sorted input (maximum work per element), the timing results are even more perplexing:</p><pre><code>Run  nth   heap  sort  swap  dlgt  avx2   _x8  peel  least
 1   1.82  0.27  1.20  2.10  2.09  0.97  1.00  0.62  0.20
 2   1.82  0.27  1.17  2.09  2.11  0.98  1.00  0.62  0.20
 3   1.82  0.27  1.17  2.10  2.10  0.98  1.00  0.62  0.20
 4   1.82  0.27  1.20  2.10  2.09  0.97  1.00  0.62  0.20
 5   1.82  0.27  1.17  2.10  2.10  0.98  1.00  0.62  0.20
 6   1.82  0.27  1.20  2.10  2.09  0.98  1.00  0.62  0.20
 7   1.82  0.27  1.20  2.07  2.07  0.98  1.00  0.62  0.20
 8   1.82  0.27  1.17  2.09  2.06  0.98  1.00  0.62  0.20
 9   1.82  0.27  1.20  2.06  2.06  0.97  1.00  0.62  0.20
10   1.82  0.27  1.20  2.10  2.10  0.98  1.00  0.62  0.20</code></pre><p>Holy smokes! On this CPU, on an already-sorted input, the STL&#8217;s <em>nth_element()</em> is faster than any of the AVX2 implementations! Why might that be? One explanation is that the clever handful of AVX2 instructions needed to insert a new candidate into <code>v_maxSoFar</code> is slower than the instructions needed to rearrange the tiny 3-element arrays on the stack for <em>kthElement_sort()</em> and <em>kthElement_heap()</em>. Note that since a new maximum is being inserted into the tiny 3-element heap or sorted array on every iteration, the CPU likely is executing few, if any, mispredicted branches.</p><p>Another explanation for the performance difference is that SIMD instructions typically cause CPUs to reduce their clock rate - an argument against a hybrid approach that would do the &#8220;lookahead&#8221; eight elements at a time, and use scalar instructions to rearrange the <code>maxSoFar</code> array. I haven&#8217;t measured the slowdown for the CPU used for these measurements (an 8-core AMD Ryzen 7 7700X, which is one of AMD&#8217;s earlier AVX512-capable CPUs).</p><h1>Conclusions</h1><p>I&#8217;ll leave it to you to decide whether the SIMD implementation was worth pursuing; the workload is synthetic, after all. For random and reverse-sorted inputs (where the top 3 are encountered early in the input), the AVX2 implementations are 5-7x faster. For already-sorted inputs, their performance suffers, in ways that bear investigation.</p><p>The <a href="https://github.com/ArchaeaSoftware/parallelprogrammer/blob/master/third/third.cpp">source code</a> for these articles has been pushed to the <a href="https://github.com/ArchaeaSoftware/parallelprogrammer">parallelprogrammer repository</a> on GitHub.</p><p>If you want to play with holding <code>v_maxSoFar</code> in multiple registers, investigating the perverse behavior on sorted input, and/or porting the code to AVX512, have at it! And let me know how it goes.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[k'th Largest Element]]></title><description><![CDATA[Or, how to ruin a l33tcode interview question]]></description><link>https://parallelprogrammer.substack.com/p/kth-largest-element</link><guid isPermaLink="false">https://parallelprogrammer.substack.com/p/kth-largest-element</guid><dc:creator><![CDATA[Nicholas Wilt]]></dc:creator><pubDate>Fri, 26 Sep 2025 16:30:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TK20!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One difficulty I have with interview coding questions is that they sometimes haunt me for long periods of time after the interview. On the bright side, every once in a while I come up with solutions that are beyond anything the interviewer might have imagined when they asked the question. And usually, that&#8217;s an opportunity to learn, even if it didn&#8217;t help get me the job. The coverage of iterative Merge Sort in my second book, for example, stemmed from a Microsoft interview round (c. 1991) where every single interview question was a whiteboard coding question asking that I reformulate recursive algorithms in iterative form.</p><p>In one such case, an interviewer asked me to write a function that returned the third-highest value in an array. For candidates familiar with the C++ STL, it may be tempting to invoke the C++ STL function <em>nth_element()</em> that does just this:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://parallelprogrammer.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Parallel Programmer is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><pre><code>std::nth_element( v.begin(), v.begin()+v.size()-3, v.end() );</code></pre><p>The problem is that <em>nth_element()</em> appears to wrap an implementation of <a href="https://en.wikipedia.org/wiki/Quickselect">Quickselect</a>, the cousin to Quicksort that runs in expected-linear time because it only recurses on one of the two subarrays after the partitioning step. Quickselect, though fast (not optimal &#8211; like Quicksort, it suffers from O(N<sup>2</sup>) worst-case runtime), is terrible for modern CPUs because of the type of unpredictable memory traffic it triggers: data-dependent, conditional swaps of array elements. Modern CPUs hate nothing more than permuting array elements, especially if they can be in far-flung parts of memory, sparsely referencing different cache lines! Availability of data is the limiting reagent of all compute these days, so using 64B cache lines (the x86 standard) to swap 4B elements is exceptionally wasteful. In this context, any reuse within the cache is somewhat coincidental.</p><p>Happily, when asked this third-largest question, I wrote a function that kept the top three values in registers, and updated them with a decision tree on each loop iteration. I believe this met the interviewer&#8217;s goal of filtering out candidates for the type of performance engineering role I was interviewing for: Such an algorithm scans the input array, rather than trying to rearrange it, and enlists hardware prefetchers when they are available and, as a result, runs much faster.</p><pre><code>int32_t
thirdLargest( const std::vector&lt;int32_t&gt;&amp; v )
{
    int32_t v0, v1, v2;

    auto swap_if = []( int32_t&amp; x, int32_t&amp; y, bool predicate ) -&gt; void {
        if ( predicate ) {
            std::swap( x, y );
        }
    };

    auto insertNewMax = [&amp;v0, &amp;v1, &amp;v2, swap_if]( int32_t v3 ) -&gt; void {
        v0 = v3;
        swap_if( v0, v1, v0&gt;v1 );
        swap_if( v1, v2, v1&gt;v2 );
    };
    {
        v0 = v[0];
        v1 = v[1];
        v2 = v[2];

        swap_if( v0, v2, v0&gt;v2 );
        swap_if( v0, v1, v0&gt;v1 );
        swap_if( v1, v2, v1&gt;v2 );
    }

    for ( size_t i = 3; i &lt; v.size(); ++i ) {
        if ( v[i] &gt; v0 ) {
            insertNewMax( v[i] );
        }
    }
    return v0;
}</code></pre><p>On my laptop, this function runs 3-11x faster than <em>nth_element()</em>, with an average speedup of 7.4x and standard deviation of 2.8.</p><p>As an aside, I tested using the Hacker&#8217;s Delight formulation of <code>swap_if</code>, and it ran at exactly the same speed as the more ergonomic code shown above. Using a template parameter <code>fancySwap</code> to enable the branchless, logical-op intensive conditional swap, that code would look like this:</p><pre><code>auto swap_if = []( int32_t&amp; x, int32_t&amp; y, bool predicate ) -&gt; void {
    if ( fancySwap ) {
        int32_t mask = -int32_t(predicate);
        x ^= y;
        y ^= (x&amp;mask);
        x ^= y;
    }
    else {
        if ( predicate ) std::swap( x, y );
    }
};</code></pre><p>We can thank the architects of modern CPUs for making the easy-to-write, easy-to-understand version just as fast. You can&#8217;t blame me for trying, though! I am old enough to remember when a taken branch was 16 clocks versus 4 clocks for a branch not taken. The code is still in the sample, in case you want to run your own tests.</p><h1>What About Larger <em>k</em>?</h1><p>I don&#8217;t recall my interviewer doing this, but the obvious escalation once a successful solution to the Third-Largest problem has been implemented is to ask: What about the <em>k</em>&#8217;th largest value, for arbitrary values of <em>k</em>? What if we want to know, say, the 100<sup>th</sup> largest value in the array instead of the 3<sup>rd</sup>-largest? We still have <em>nth_element</em>(), of course, but we also can replace our 3 explicit values with an array <code>maxSoFar</code> of size <em>k</em>, and keep them sorted in increasing order. Any incoming value larger than <code>maxSoFar[0]</code>, the &#8220;minimum maximum&#8221; if you will, will cause <code>maxSoFar[0]</code> to be replaced and the incoming value then must be placed in the correct sorted order in the array. This operation amounts to the insertion step in the bedrock Insertion Sort algorithm, which may be asymptotically slow, but is still an important and useful algorithm. Let&#8217;s see what our algorithm looks like with a <code>maxSoFar</code> array of size <em>k</em> instead of our explicit 3 values:</p><pre><code>template&lt;uint32_t k&gt;
int32_t
kthLargest_sort( const std::vector&lt;int32_t&gt;&amp; v )
{
    // Local array we keep sorted in increasing order
    // The first element is smallest, so any incoming element
    // that is larger must be replace that element, then get
    // moved into position
    std::array&lt;int32_t,k&gt; maxSoFar;

    for ( size_t i = 0; i &lt; k; i++ ) {
        maxSoFar[i] = v[i];
    }
    std::sort( maxSoFar.begin(), maxSoFar.end() );
    int32_t minMax = maxSoFar[0];
    for ( size_t i = k; i &lt; v.size(); i++ ) {
        int32_t x = v[i];

        if ( minMax &lt; x ) {
            size_t j;
            for ( j = 1; j &lt; k &amp;&amp; maxSoFar[j] &lt; x; j++ ) {
                maxSoFar[j-1] = maxSoFar[j];
            }
            maxSoFar[j-1] = x;
            minMax = maxSoFar[0];
        }
    }
    return maxSoFar[0];
}</code></pre><p>Since <code>maxSoFar</code> is small and local to the stack, shifting its elements around doesn&#8217;t have the cache line problem we alluded to earlier &#8211; even for large <em>k</em>, it exhibits very cache-friendly behavior. But the number of operations performed for each input element grows linearly with <em>k</em>. At some point, for sufficiently large <em>k</em>, <em>nth_element()</em> likely will run faster. We&#8217;ll get into more detail in the Performance Analysis section, but I was surprised at how big <em>k</em> (and <code>maxSoFar</code>) could get before performance stopped being competitive with the STL&#8217;s <em>nth_element()</em>.</p><h1>Enter Binary Heaps</h1><p>Do we need to keep the elements sorted? If you intuition says No, your intuition is correct! For small <em>k</em>, the question makes little difference, but for larger <em>k</em>, there&#8217;s a known data structure &#8211; the <a href="https://en.wikipedia.org/wiki/Binary_heap">binary heap</a> &#8211; that can hold the <em>k</em> largest elements found in the array so far, but the heap only takes O(lg<em>k</em>) time to update, because it does not maintain the elements in sorted order.</p><p>My first introduction to binary heaps was in <em>Programming Pearls</em> by Jon Bentley (c. 1986)<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. Although heaps conceptually include nodes with parents and children, they are defined to be <em>complete</em> binary trees (no missing children), so can be densely packed into arrays. A clever indexing scheme, not pointers, is used to navigate between parent and child nodes in the heap. When using zero-based indexing, the index of the parent of the <em>i</em>&#8217;th heap element is (<em>i</em>-1)/2, and its children are 2<em>i</em>+1 and 2<em>i</em>+2. For a heap to be valid, the values of each child node must be greater than or equal to its parent.</p><p>Here's an example heap of size 6, which is represented in memory simply as</p><pre><code>{1, 3, 5, 7, 9, 11}</code></pre><pre><code>          1
       /     \
      3       5
     / \     /
    7   9  11</code></pre><p>To &#8220;heapify&#8221; an array, we call an operation <em>SiftUp</em> on each array element in turn. <em>SiftUp</em> checks to see if a given element is &#8220;smaller&#8221; than its parent and, if so, swaps with the parent, working its way toward the &#8220;top&#8221; of the heap (element zero. The algorithm can early-out: <em>SiftUp</em> stops comparing and swapping as soon as it<em> </em>sees that a node&#8217;s parent is less than the node value.</p><pre><code>template&lt;typename T, std::size_t k&gt;
inline void
SiftUp( std::array&lt;T,k&gt;&amp; x, size_t i )
{
    while ( i ) {
        size_t p = (i-1)&gt;&gt;1;
        if ( x[p] &lt; x[i] )
            break;
        std::swap( x[i], x[p] );
        i = p;
    }
}</code></pre><p>For our application, we are not heapifying the entire array; we are creating a heap of size <em>k</em>, to hold the <em>k</em> largest elements found so far in the input array. Once our <em>k</em>-sized heap is created, the first element with index 0 is known to be the smallest value in the heap; so it is at once:</p><ul><li><p>the value that must be compared with incoming array elements, to see if they belong in the heap, and</p></li><li><p>the value that must be replaced as soon as such an array element is identified.</p></li></ul><p>Once that first element has been replaced, the heap property may no longer hold true, so we perform an operation called <em>SiftDown()</em> that moves downward in the heap, swapping each element with the smaller of its two children until the heap property holds again.</p><pre><code>template&lt;typename T, std::size_t k&gt;
inline void
SiftDown( std::array&lt;T,k&gt;&amp; x, size_t i=0 )
{
    size_t c;
    while ( (c = i+i+1) &lt; k ) {
        if ( (c+1)&lt;k &amp;&amp; (x[c+1]&lt;x[c]) )
            c += 1;
        if ( x[i] &lt; x[c] ) break;
        std::swap( x[i], x[c] );
        i = c;
    }
}</code></pre><p>Here, <em>SiftDown</em> takes the place of the Insertion Sort step, but instead of performing O(<em>k</em>) operations to make the array sorted again, we <em>SiftDown</em> to perform O(lg<em>k</em>) make the heap property hold again. For larger <em>k</em>, <em>SiftDown()</em> should be much faster than the insertion step (e.g. for <em>k</em>=1000, up to 10 swaps instead of up to 1,000 insertion steps).</p><p>Now that the basic operations of creating and updating the heap are defined, we can implement our algorithm for any <em>k</em>. Each update of the heap takes O(lg<em>k</em>) steps, and for small <em>k</em>, the heap stays local in very fast L1 cache for the CPU.</p><p>The code we&#8217;re using in this article to build and update the heap, is from my second book, <em>Classical Algorithms in C++</em> (c. 1995). My treatment of binary heaps relied heavily on Jon Bentley&#8217;s coverage of binary heaps in his <em><a href="https://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880">Programming Pearls</a></em>. The only change to the code shown here is that I&#8217;ve templatized the heap size as well as the array element type.</p><p>Using <em>SiftUp()</em> to build our <em>k</em>-element heap and <em>SiftDown()</em> to update it when necessary, the <em>kthLargest()</em> function may be implemented as follows:</p><pre><code>template&lt;uint32_t k&gt;
int32_t
kthLargest_heap( const std::vector&lt;int32_t&gt;&amp; v )
{
    std::array&lt;int32_t,k&gt; heap;

    for ( size_t i = 0; i &lt; k; i++ ) {
        heap[i] = v[i];
        SiftUp( heap, i );
    }
    int32_t minMax = heap[0];
    for ( size_t i = k; i &lt; v.size(); i++ ) {
        int32_t x = v[i];

        if ( minMax &lt; x ) {
            heap[0] = x;
            SiftDown( heap );
            minMax = heap[0];
        }
    }
    return heap[0];
}</code></pre><h1>Performance Analysis</h1><p>Before we delve into the tradeoffs between the different algorithms described above, let&#8217;s observe that it&#8217;s difficult to implement one whose performance is independent of the input.</p><p>For example, if the first <em>k</em> values are, in fact, the largest in the input array, then all of our algorithms will quickly scan through the remaining input, rejecting candidates until the end of the input array is reached. From the CPU&#8217;s perspective, that becomes a performance test for its hardware prefetchers and branch prediction.</p><p>On the opposite end of the spectrum, if the array is sorted, then every input element will trigger a rearrangement of the <code>maxSoFar</code> array (whether an Insertion Sort step or a <em>SiftDown</em> of the heap), echoing the worst-case performance of Quicksort, where na&#239;ve implementations run in O(N<sup>2</sup>) on already-sorted inputs.</p><p>The bulk of our performance analysis will be done across 10 randomized input arrays of size 100*2<sup>20</sup>, but we&#8217;ll also gather best- and worst-case performance numbers to confirm our suspicions about how they will behave on sorted inputs.</p><p>For <em>nth_element()</em>, the algorithm ran at the same speed for all <em>k</em>, about 37M elements per second. It&#8217;s by far the slowest option, so we&#8217;ll take it as the baseline and report the speedups of the different formulations. </p><p>For the <em>k</em>&#8217;th largest, we only have the <em>kthLargest_sort()</em> and <em>kthLargest_heap()</em> variants, which feature expected runtime of O(<em>Nk</em>) and O(<em>N</em><code>lg</code><em>k</em>), respectively. Algorithmic analysis would dictate that <em>nth_element()</em>, the Quickselect implementation in the STL, will run faster than <em>kthLargest_sort()</em> for some <em>k</em>. How big does <em>k</em> have to get before our cache-friendlier <em>kthLargest_sort()</em> gets overtaken by its inferior algorithmic complexity?</p><p>In a similar vein, we know that <em>kthLargest_heap()</em> has better algorithmic complexity than <em>kthLargest_sort()</em>, so can ask the same question: where is the crossover? For <em>k</em>==1000, <code>lg</code><em>k</em> is 10. Shuffling elements up to 1,000 times seems like it would take a lot longer than comparing and swapping elements up to 10 times. But as we already know from our foray into the Hacker&#8217;s Delight formulation of a conditional swap, modern CPUs can surprise us and the only way to find out for sure is with real-world testing.</p><p>The test program for this article times the <em>k</em>th-largest algorithms with varying values of <em>k</em>: 100&#8230;1000, 1000&#8230;10,000, and 10,000-100,000. (I may have just spoiled the findings)</p><p>For k&lt;=1,000, only slight differences in performance are noted:</p><pre><code>      Units: 100M's of elements/s

  k    sort   heap  % faster
 100   5.43   5.42  0%
 200   5.41   5.42  0%
 300   5.39   5.40  0%
 400   5.35   5.40  1%
 500   5.31   5.37  1%
 600   5.26   5.39  2%
 700   5.20   5.35  3%
 800   5.14   5.37  5%
 900   5.07   5.33  5%
1000   5.00   5.36  7%</code></pre><p>Amazingly, maintaining a <em>sorted array</em> of the 1,000 largest elements in the input array only runs 7% slower than maintaining a heap. (Both formulations are about 15x faster than <em>nth_element()</em>.) The trend is more clear if we chart it, though; Substack has lame support for tables and charts, and Excel&#8217;s heuristics for choosing scales and offsets amount to a flair for the dramatic, but you can see where this is going:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TK20!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TK20!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png 424w, https://substackcdn.com/image/fetch/$s_!TK20!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png 848w, https://substackcdn.com/image/fetch/$s_!TK20!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png 1272w, https://substackcdn.com/image/fetch/$s_!TK20!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TK20!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png" width="598" height="353" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:353,&quot;width&quot;:598,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:29579,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/173903871?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!TK20!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png 424w, https://substackcdn.com/image/fetch/$s_!TK20!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png 848w, https://substackcdn.com/image/fetch/$s_!TK20!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png 1272w, https://substackcdn.com/image/fetch/$s_!TK20!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6f84de0-9bf2-4d8f-b5a8-5913510781d3_598x353.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>It&#8217;s interesting, but I have not investigated, why the heap implementation seesaws back and forth for &#8216;odd&#8217; values (<em>k</em>=600 is slightly faster than <em>k</em>=500, for example).</p><p>Let&#8217;s see how sort&#8217;s performance disadvantage changes if we increase <em>k</em> some more:</p><pre><code><code>  k     sort   heap  % faster
 1000   5.00   5.36  7%
 2000   4.35   5.22  20%
 3000   3.61   5.10  41%
 4000   2.88   5.03  75%
 5000   2.36   5.02  113%
 6000   1.95   4.94  153%
 7000   1.58   4.87  208%
 8000   1.35   4.66  246%
 9000   1.12   4.72  320%
10000   0.97   4.62  378%</code></code></pre><p>For <em>k</em>=10000, the heap is 4.78x faster, and the performance disparity continues to diverge.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UcRm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UcRm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png 424w, https://substackcdn.com/image/fetch/$s_!UcRm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png 848w, https://substackcdn.com/image/fetch/$s_!UcRm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png 1272w, https://substackcdn.com/image/fetch/$s_!UcRm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UcRm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png" width="644" height="316" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:316,&quot;width&quot;:644,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:22778,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://parallelprogrammer.substack.com/i/173903871?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UcRm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png 424w, https://substackcdn.com/image/fetch/$s_!UcRm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png 848w, https://substackcdn.com/image/fetch/$s_!UcRm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png 1272w, https://substackcdn.com/image/fetch/$s_!UcRm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190fbc30-4cfc-469a-b87c-9b58f5dcc10f_644x316.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The final question on my mind was what value of <em>k</em> would cause <em>kthLargest_sort()</em> to be even slower than the STL&#8217;s <em>nth_element()</em>, and on this CPU at least, the answer is somewhere between <em>k</em>=10,000 and <em>k</em>=20,000. (Much larger than I would have expected.)</p><h1>What&#8217;s Next? SIMD, of course</h1><p>This is the Parallel Programmer Substack, after all.</p><p>So far, we&#8217;ve explored the design space that a good coder might be able to explore during a coding interview. We&#8217;ve been good computer science students and used our algorithm analysis skills, replacing an expected-<em>O</em>(<em>N</em>) algorithm with bad cache behavior with an <em>O</em>(<em>Nk</em>) algorithm with an <em>O</em>(<em>N</em><code>lg</code><em>k</em>) algorithm. But what if we give ourselves permission to revisit the sorting algorithm, with the help of SIMD? Can the insertion step for Insertion Sort be implemented in <em>O</em>(1) time on a small array?</p><p>Yes. Yes, it can. But an exploration of how will have to wait until the next article.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Bentley just called them &#8220;heaps,&#8221; but even for my 1995 book, I adopted the term &#8220;binary heap&#8221; to disambiguate from the heaps used by memory allocators.</p><p></p></div></div>]]></content:encoded></item></channel></rss>