<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Nova&#39;s blog</title>
    <link rel="self" type="application/atom+xml" href="https://novacrazy.github.io/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://novacrazy.github.io"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-20T00:00:00+00:00</updated>
    <id>https://novacrazy.github.io/atom.xml</id>
    <entry xml:lang="en">
        <title>Three-State Logic for FMA</title>
        <published>2026-09-20T00:00:00+00:00</published>
        <updated>2026-09-20T00:00:00+00:00</updated>
        
        <author>
          <name>Nova</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://novacrazy.github.io/2026-09-20-tribool-fma/"/>
        <id>https://novacrazy.github.io/2026-09-20-tribool-fma/</id>
        
        <content type="html" xml:base="https://novacrazy.github.io/2026-09-20-tribool-fma/">&lt;div class=&quot;olivine-admonish&quot;&gt;
TL;DR: WebAssembly is weird. Whether it has true fused multiply-add is not
known at compile time, and basing algorithm decisions around any assumption
has the potential to break things.
&lt;/div&gt;
&lt;p&gt;You would think that whether or not a backend has true native Fused Multiply-Add (FMA) instructions would be pretty cut and dry, right? Until recently, Thermite types indicated if they have true native FMA instructions via a &lt;code&gt;HAS_TRUE_FMA&lt;/code&gt; associated const &lt;code&gt;bool&lt;/code&gt;. However, that was insufficient, and broken at worst.&lt;/p&gt;
&lt;h2 id=&quot;why-indicate-true-fma&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-indicate-true-fma&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
Why Indicate True FMA&lt;/h2&gt;
&lt;p&gt;It&#39;s quite helpful to have algorithms decide on certain paths or optimizations, at compile-time, based on if true FMA is available. For one, it&#39;s often as fast as a single multiply while doing both multiply and addition, so that alone accelerates many functions. It also only rounds once, so it can bypass extra effort required for that, such as some compensated arithmetic.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-13 z-d-11&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; const&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt; V&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;HAS_TRUE_FMA&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; /*&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; ... &lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;*/&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;was used all over Thermite&#39;s math libraries for these kind of optimizations.&lt;/p&gt;
&lt;p&gt;It&#39;s also worth knowing Thermite has two flavors of FMA, which are important:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mul_add&lt;/code&gt; and family are &lt;strong&gt;always correctly rounded fused multiply-add&lt;/strong&gt;, regardless of backend. If there is no hardware FMA, it falls back to a bit-identical vectorized polyfill, which can be quite expensive.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mul_adde&lt;/code&gt; and family are the &lt;strong&gt;opportunistic&lt;/strong&gt; forms. They are fused when FMA is a single instruction, but plain &lt;code&gt;a * b + c&lt;/code&gt; multiply+add where it isn&#39;t. Always fast, allowed to differ between backends.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;mul_adde&lt;/code&gt; and family are used almost everywhere. Might as well opt for the fastest form available.&lt;/p&gt;
&lt;h2 id=&quot;webassembly&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#webassembly&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
WebAssembly&lt;/h2&gt;
&lt;p&gt;The WASM relaxed-simd proposal added &lt;code&gt;relaxed_madd&lt;/code&gt; and &lt;code&gt;relaxed_nmadd&lt;/code&gt; instructions. &quot;Relaxed&quot; meaning they act like &lt;code&gt;mul_adde&lt;/code&gt;/&lt;code&gt;nmul_adde&lt;/code&gt;, where it will use true FMA if available but otherwise fallback to &lt;code&gt;a*b+c&lt;/code&gt; for performance. Nothing is guaranteed about which one you get, the engine is free to do whatever it wants. The only promise is that the choice is fixed for the lifetime of the module instance, which is what makes detecting it once at startup sound.&lt;/p&gt;
&lt;p&gt;However, one change I made recently was to opportunistically enable true FMA on WASM if it&#39;s detected at startup. Using some WASM tricks, I inject a bit of code that runs before &lt;code&gt;main()&lt;/code&gt;, updating a &lt;code&gt;static mut&lt;/code&gt; global variable:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;used&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;unsafe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;link_section &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt;.init_array&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;static&lt;/span&gt;&lt;span&gt; INIT_WASM_RELAXED_FMA&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; extern&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;    extern&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;z-l-12 z-d-13&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; fn&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; init&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; detect_relaxed_fma&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;    init&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The linker collects &lt;code&gt;.init_array&lt;/code&gt; entries into &lt;code&gt;__wasm_call_ctors&lt;/code&gt;, which runs before user code.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mul_add&lt;/code&gt; and family then branch on that flag at runtime to opt into true FMA. Both arms are bit-identical, so that only trades speed.&lt;/p&gt;
&lt;div class=&quot;olivine-admonish&quot;&gt;
&lt;p&gt;Fun fact: &lt;code&gt;relaxed_madd&lt;/code&gt; and &lt;code&gt;relaxed_nmadd&lt;/code&gt; are separate instructions, and the spec allows an engine to fuse one and not the other. The init checks both independently.&lt;/p&gt;
&lt;p&gt;Another fun fact: Using a &lt;code&gt;static mut&lt;/code&gt; was required because LLVM won&#39;t optimize loops or collapse redundant branches if using atomic accesses, even in single-threaded environments such as WASM. Polynomial evaluation would instead
query the flag every single FMA call. With &lt;code&gt;static mut&lt;/code&gt; that query is hoisted above and branched on once, massively improving the codegen.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;what-broke&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-broke&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
What Broke&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mul_add&lt;/code&gt; was fine, that was always guaranteed to be the correctly rounded fused multiply-add. However, &lt;code&gt;mul_adde&lt;/code&gt; behaves differently. The default behavior of &lt;code&gt;mul_adde&lt;/code&gt; was to check &lt;code&gt;V::HAS_TRUE_FMA&lt;/code&gt; and either call &lt;code&gt;mul_add&lt;/code&gt;, or do &lt;code&gt;a*b+c&lt;/code&gt;, determined at compile time.&lt;/p&gt;
&lt;p&gt;So when &lt;code&gt;mul_adde&lt;/code&gt; for WebAssembly specifically was overridden to ignore &lt;code&gt;V::HAS_TRUE_FMA&lt;/code&gt; and lower straight to &lt;code&gt;relaxed_madd&lt;/code&gt;, leaving the engine to decide, we had cases where math algorithms chose the &lt;code&gt;V::HAS_TRUE_FMA == false&lt;/code&gt; path but &lt;code&gt;mul_adde&lt;/code&gt; used true FMA internally.&lt;/p&gt;
&lt;p&gt;Normally that&#39;s fine, but notably &quot;sum/difference of products&quot; breaks entirely.&lt;/p&gt;
&lt;p&gt;For example, multiplying a quaternion by its own conjugate should give exactly &lt;code&gt;$(0, 0, 0, |q|^2)$&lt;/code&gt;. The fast quaternion product computes each lane as a sum of four products, two of them folded into &lt;code&gt;mul_adde&lt;/code&gt;s:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; sum12&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; Self&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;mul_adde&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;x&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; rhs_x_signed&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; Self&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;mul&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;w&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; rhs&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; sum34&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; Self&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;mul_adde&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;z&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; rhs_z_signed&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; Self&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;mul&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;y&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; rhs_y_signed&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For &lt;code&gt;$q \cdot \bar{q}$&lt;/code&gt; the vector lanes are made of pairs like &lt;code&gt;$t - t$&lt;/code&gt;. If every product is rounded, that&#39;s &lt;code&gt;$\mathrm{fl}(t) - \mathrm{fl}(t) = 0$&lt;/code&gt;, exact, for free. If &lt;code&gt;mul_adde&lt;/code&gt; fuses, one side of the pair is the &lt;em&gt;exact&lt;/em&gt; product and the other is rounded, and what survives is the rounding error:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;q = (0.1, 0.2, 0.3, 0.927...)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;q * conj(q) = [1.5188877683131223e-19, 0.0, -5.551115123125783e-17, 1.0]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The quaternion product has a fast arm and an accurate arm that compensates for this. On backends with no FMA, the accurate path routed back to the fast arm, on the reasoning that all-rounded products already cancel exactly. Correct on SSE2. On WASM, with &lt;code&gt;HAS_TRUE_FMA = false&lt;/code&gt; and an engine that fuses, it produced exactly the garbage above, in the path whose documentation promises exact zeros. The 4x4 determinant and inverse minors had the same defect.&lt;/p&gt;
&lt;p&gt;In effect, &lt;code&gt;HAS_TRUE_FMA&lt;/code&gt; could not be trusted. Not because the value was wrong, but because &lt;code&gt;bool&lt;/code&gt; has no way to say &quot;I don&#39;t know&quot;.&lt;/p&gt;
&lt;h2 id=&quot;tribool&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#tribool&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
Tribool&lt;/h2&gt;
&lt;p&gt;Many years ago I created the rust &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://crates.io/crates/tribool&quot;&gt;&lt;code&gt;tribool&lt;/code&gt;&lt;/a&gt; crate, based off of Boost&#39;s &lt;code&gt;Tribool&lt;/code&gt; class. It&#39;s basically just an enum:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;pub&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; enum&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt; Tribool&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt;    False&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt;    True&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt;    Indeterminate&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; Unknown&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with various three-state logic methods on it. Well, what would you call the state in which we don&#39;t know if FMA is present or not, other than &lt;code&gt;Indeterminate&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;The crate fit perfectly, so I set out and replaced &lt;code&gt;V::HAS_TRUE_FMA&lt;/code&gt; with&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;const&lt;/span&gt;&lt;span&gt; HAS_NATIVE_FMA&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt; Tribool&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Backend&lt;/th&gt;&lt;th&gt;&lt;code&gt;HAS_NATIVE_FMA&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;x86 SSE2 / SSE4.2&lt;/td&gt;&lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;x86 AVX2+FMA, AVX-512&lt;/td&gt;&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;NEON (aarch64)&lt;/td&gt;&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Scalar&lt;/td&gt;&lt;td&gt;whatever the baseline target features say&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;WASM&lt;/td&gt;&lt;td&gt;&lt;code&gt;Indeterminate&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The funny part is after the migration, no algorithm in Thermite mentions &lt;code&gt;Indeterminate&lt;/code&gt;, instead preferring one of:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; &amp;quot;Is FMA cheap here? i.e.: dedicated instruction&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-13 z-d-11&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; const&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; matches!&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt;V&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;HAS_NATIVE_FMA&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt; tribool&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; /*&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; ... &lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;*/&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; &amp;quot;Could two products round differently from each other?&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; Note the boolean NOT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-13 z-d-11&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; const&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt; !&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;matches!&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt;V&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;HAS_NATIVE_FMA&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt; tribool&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt;False&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; /*&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; ... &lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;*/&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As it turns out, the old &lt;code&gt;bool&lt;/code&gt; const was close, but not exactly what was needed for
informing algorithms about the FMA behavior. Depending on the situation we need to know if FMA is cheap (i.e., a single instruction), or just if we can rely on certain rounding behaviors or not.&lt;/p&gt;
&lt;h2 id=&quot;composites&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#composites&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
Composites&lt;/h2&gt;
&lt;p&gt;One more place the old &lt;code&gt;bool&lt;/code&gt; was wrong was for composite types, such as &lt;code&gt;Dual&lt;/code&gt; or &lt;code&gt;Complex&lt;/code&gt; numbers, which reported &lt;code&gt;HAS_TRUE_FMA = false&lt;/code&gt; unconditionally. However, if the question is &quot;Is &lt;code&gt;mul_adde&lt;/code&gt; faster than &lt;code&gt;a*b+c&lt;/code&gt;?&quot;, that&#39;s more interesting.&lt;/p&gt;
&lt;p&gt;Nearly every consumer of &lt;code&gt;HAS_TRUE_FMA&lt;/code&gt; cared because of performance reasons, not because of the single rounding. Even then, making use of FMA for &lt;code&gt;Dual&lt;/code&gt;/&lt;code&gt;Complex&lt;/code&gt;&#39;s own &lt;code&gt;mul_adde&lt;/code&gt; does improve precision some. Therefore, these now simply forward the inner type&#39;s &lt;code&gt;HAS_NATIVE_FMA&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
Conclusion&lt;/h2&gt;
&lt;p&gt;In the end, most algorithms didn&#39;t really care, but when &lt;code&gt;!false != true&lt;/code&gt;, making use of &lt;code&gt;Tribool&lt;/code&gt; is both useful and cool.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Compensated Arithmetic in Core</title>
        <published>2026-09-19T00:00:00+00:00</published>
        <updated>2026-09-19T00:00:00+00:00</updated>
        
        <author>
          <name>Nova</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://novacrazy.github.io/2026-09-19-comp-in-core/"/>
        <id>https://novacrazy.github.io/2026-09-19-comp-in-core/</id>
        
        <content type="html" xml:base="https://novacrazy.github.io/2026-09-19-comp-in-core/">&lt;div class=&quot;olivine-admonish&quot;&gt;
TL;DR: Opting into algebraic optimizations for scalars silently broke
a significant portion of Thermite&#39;s math library. Here&#39;s how I fixed it.
&lt;/div&gt;
&lt;p&gt;Recent changes have exposed the raw compensated arithmetic primitives like &lt;code&gt;two_sum&lt;/code&gt; and &lt;code&gt;two_prod&lt;/code&gt; in Thermite core, whereas they were previously only in the &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://raygon-renderer.github.io/thermite/thermite_compensated/index.html&quot;&gt;&lt;code&gt;thermite-compensated&lt;/code&gt;&lt;/a&gt; crate. Why? Turns out it&#39;s pretty essential to keep the basic compensated ops around at the very low level.&lt;/p&gt;
&lt;h2 id=&quot;what-is-compensated-arithmetic&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-is-compensated-arithmetic&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
What is Compensated Arithmetic?&lt;/h2&gt;
&lt;p&gt;Compensated arithmetic primitives, more formally known as Error-Free Transforms,
are a way of recovering the error lost by rounding when applying basic
operators such as addition, subtraction, multiplication, and division.
Almost everything else falls out from that.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;a + b&lt;/code&gt; in an &lt;code&gt;f64&lt;/code&gt; isn&#39;t &lt;code&gt;a + b&lt;/code&gt;. The exact sum can need far more bits than the mantissa
has (&lt;code&gt;1e300 + 1e-300&lt;/code&gt; would take about two thousand), so the hardware rounds it to the nearest representable value and drops the rest.
What gets dropped is the interesting part, because for addition it&#39;s always exactly
representable in the same format. Knuth&#39;s 2Sum recovers it in six adds, no branches, no
extra precision.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;math&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\begin{aligned}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;s &amp;amp;= \mathrm{fl}(a + b) \\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;e &amp;amp;= (a + b) - s \\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;s + e &amp;amp;= a + b&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\end{aligned}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$s$&lt;/code&gt; is the result you would have gotten anyway, and &lt;code&gt;$e$&lt;/code&gt; is exactly what the rounding
lost, so the pair &lt;code&gt;$(s, e)$&lt;/code&gt; holds the exact sum in two words. Nothing is lost, the
value is just re-encoded across two floats.&lt;/p&gt;
&lt;p&gt;Multiplication works out the same way. The exact product of two &lt;code&gt;$p$&lt;/code&gt;-bit values needs
&lt;code&gt;$2p$&lt;/code&gt; bits, and the residual left after rounding fits in &lt;code&gt;$p$&lt;/code&gt; of them, so it&#39;s
recoverable as well. With a true fused multiply-add that&#39;s two instructions:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; p&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; a&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; b&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; e&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;mul_sub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;b&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; p&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; fma(a, b, -p)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without FMA, you need Dekker&#39;s algorithm, which cuts both operands into half-width pieces
with Veltkamp splitting and sums the four cross products in the right order. That&#39;s 17
operations instead of 2, so many algorithms prefer other methods when native FMA isn&#39;t
available.&lt;/p&gt;
&lt;p&gt;Everything else is built on those two. Double-double arithmetic (&lt;code&gt;Compensated&amp;lt;f64&amp;gt;&lt;/code&gt;, around
106 bits of significand) is a &lt;code&gt;two_sum&lt;/code&gt;/&lt;code&gt;two_prod&lt;/code&gt; pair plus renormalization. Kahan and
Neumaier summation are &lt;code&gt;two_sum&lt;/code&gt; with the error fed back into the next iteration. A
correctly-rounded &lt;code&gt;mul_add&lt;/code&gt; on hardware without an FMA instruction is Boldo and Melquiond&#39;s
construction, built out of &lt;code&gt;two_prod&lt;/code&gt; and &lt;code&gt;two_sum&lt;/code&gt;. Interval arithmetic needs the residual
to know which way to nudge an endpoint. It&#39;s pretty handy all things considered.&lt;/p&gt;
&lt;p&gt;One catch is that this buys precision, not range. The exponent is untouched, so anything
that overflows or flushes to zero is still gone, and Veltkamp splitting has its own
threshold (&lt;code&gt;$2^{115}$&lt;/code&gt; for &lt;code&gt;f32&lt;/code&gt;) past which operands have to be rebalanced before they can
be split at all.&lt;/p&gt;
&lt;h2 id=&quot;algebraic-scalar&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#algebraic-scalar&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
&lt;code&gt;algebraic_scalar&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Thermite has the &lt;span class=&quot;kw&quot;&gt;&lt;code&gt;algebraic_scalar&lt;/code&gt;&lt;/span&gt; Cargo feature to enable the &quot;Algebraic&quot; float ops (&lt;code&gt;algebraic_*&lt;/code&gt; on &lt;code&gt;f32&lt;/code&gt;/&lt;code&gt;f64&lt;/code&gt;), which allow LLVM to casually reassociate operations to improve performance or improve autovectorization opportunities. With this feature enabled, all basic operations (&lt;code&gt;+&lt;/code&gt;,&lt;code&gt;-&lt;/code&gt;,&lt;code&gt;*&lt;/code&gt;,&lt;code&gt;/&lt;/code&gt;) are set to algebraic ops at the lowest library level, so everything inherits those everywhere. This is honestly great for most scalar code. If you are not using or cannot use SIMD for one reason or another, making the scalar fallbacks as fast as possible is ideal.&lt;/p&gt;
&lt;p&gt;Summation with a division in it is the clearest example:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt; NOTE: &amp;quot;Scalars&amp;quot; in Thermite are still wrapped in `Vector`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; mut&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; sum&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt; Vector&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt;f64&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt; Vector&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;splat&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-13 z-d-11&quot;&gt;for&lt;/span&gt;&lt;span&gt; &amp;amp;&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; in&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; xs&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;    sum&lt;/span&gt;&lt;span&gt; +=&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; x&lt;/span&gt;&lt;span&gt; /&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-2&quot;&gt; Vector&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;splat&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Strict, at &lt;code&gt;-O -C target-cpu=x86-64-v3&lt;/code&gt;, the inner loop is unrolled 8x and every copy looks
like this:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;asm&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;vmovsd&lt;/span&gt;&lt;span&gt;  (%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;rax&lt;/span&gt;&lt;span&gt;), %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;xmm2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l z-l-hl z-d-hl&quot;&gt;&lt;span&gt;vdivsd&lt;/span&gt;&lt;span&gt;  %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;xmm1&lt;/span&gt;&lt;span&gt;, %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;xmm2&lt;/span&gt;&lt;span&gt;, %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;xmm2&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;   # xmm1 = 3.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l z-l-hl z-d-hl&quot;&gt;&lt;span&gt;vaddsd&lt;/span&gt;&lt;span&gt;  %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;xmm2&lt;/span&gt;&lt;span&gt;, %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;xmm0&lt;/span&gt;&lt;span&gt;, %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;xmm0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;# repeated 8 times&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enabling the &lt;span class=&quot;kw&quot;&gt;&lt;code&gt;algebraic_scalar&lt;/code&gt;&lt;/span&gt; feature gives this instead:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;asm&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;vbroadcastsd&lt;/span&gt;&lt;span&gt; __real@3fd5555555555555(%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;rip&lt;/span&gt;&lt;span&gt;), %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm1&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-7&quot;&gt;   # 1/3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;vfmadd231pd&lt;/span&gt;&lt;span&gt;  (%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;rcx&lt;/span&gt;&lt;span&gt;,%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;r10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;8&lt;/span&gt;&lt;span&gt;), %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm1&lt;/span&gt;&lt;span&gt;, %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;vfmadd231pd&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;  32&lt;/span&gt;&lt;span&gt;(%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;rcx&lt;/span&gt;&lt;span&gt;,%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;r10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;8&lt;/span&gt;&lt;span&gt;), %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm1&lt;/span&gt;&lt;span&gt;, %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;vfmadd231pd&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;  64&lt;/span&gt;&lt;span&gt;(%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;rcx&lt;/span&gt;&lt;span&gt;,%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;r10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;8&lt;/span&gt;&lt;span&gt;), %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm1&lt;/span&gt;&lt;span&gt;, %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;vfmadd231pd&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;  96&lt;/span&gt;&lt;span&gt;(%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;rcx&lt;/span&gt;&lt;span&gt;,%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;r10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-10&quot;&gt;8&lt;/span&gt;&lt;span&gt;), %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm1&lt;/span&gt;&lt;span&gt;, %&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;ymm4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The division is gone entirely, replaced by a multiply against a rounded &lt;code&gt;$1/3$&lt;/code&gt;, which is
less precise but &quot;close enough&quot; for most use cases.
The add contracted into that multiply, and the serial accumulator turned into
four independent ones at four doubles apiece, so each iteration retires 16 elements in 4
instructions where the strict version needed 32. With a runtime divisor it does the same
thing and just hoists a single &lt;code&gt;vdivsd&lt;/code&gt; out of the loop. This is a great trade for most
code.&lt;/p&gt;
&lt;p&gt;However, taking a look at &lt;code&gt;two_sum&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; two_sum&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;a&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; Self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; b&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; Self&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; -&amp;gt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;Self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt; Self&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;    let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; s&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; a&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; b&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;    let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; v&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; s&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; a&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;    let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; e&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;a&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;s&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; v&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;b&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; v&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;s&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As real-number algebra, with &lt;code&gt;$s = a + b$&lt;/code&gt; and &lt;code&gt;$v = s - a$&lt;/code&gt;, reassociation collapses &lt;code&gt;$v = (a + b) - a = b$&lt;/code&gt;, and with it the whole error term becomes zero:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;math&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\begin{aligned}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;e &amp;amp;= \bigl(a - (s - v)\bigr) + (b - v) \\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;= (a - a) + (b - b) = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\end{aligned}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LLVM is free to do this in multiple places, including common Cody-Waite style range reductions that occur in almost everything.&lt;/p&gt;
&lt;p&gt;For example, average-precision &lt;code&gt;sin&lt;/code&gt;/&lt;code&gt;cos&lt;/code&gt; reduces its argument modulo &lt;code&gt;$\pi$&lt;/code&gt; by splitting the constant across
several floats, &lt;code&gt;$\pi \approx \pi_A + \pi_B + \pi_C$&lt;/code&gt;, where &lt;code&gt;$\pi_A$&lt;/code&gt; has enough low
mantissa bits zeroed that &lt;code&gt;$n \pi_A$&lt;/code&gt; is &lt;em&gt;exact&lt;/em&gt; for every &lt;code&gt;$n$&lt;/code&gt; in range, and each
following term picks up the bits the previous one couldn&#39;t hold:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; n&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;x&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt; FRAC_1_PI&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;round&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-4&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; r&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt;x&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; n&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt; PI_A&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; n&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt; PI_B&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-9&quot;&gt; n&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt; PI_C&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The subtractions are deliberately sequenced so that each one cancels against a quantity
of its own magnitude, keeping &lt;code&gt;$r$&lt;/code&gt; accurate to full precision even when &lt;code&gt;$x$&lt;/code&gt; is large
and &lt;code&gt;$n \pi$&lt;/code&gt; is nowhere near representable. Algebraically, though:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;math&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\begin{aligned}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;r &amp;amp;= x - n(\pi_A + \pi_B + \pi_C) \\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;= x - n\pi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\end{aligned}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;so LLVM is free to fold the three constants into one rounded &lt;code&gt;$\pi$&lt;/code&gt; and emit a single
&lt;code&gt;$x - n\pi$&lt;/code&gt;. The extra &lt;code&gt;$\approx 50$&lt;/code&gt; bits of the constant vanish, and the reduction
degrades from full precision to whatever &lt;code&gt;$x - n\pi$&lt;/code&gt; happens to give. Not good.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;exp&lt;/code&gt; and &lt;code&gt;ln&lt;/code&gt; have similar range reduction. &lt;code&gt;algebraic_scalar&lt;/code&gt; would silently break that
when using the math library with scalars. The effects of it spread quite far.&lt;/p&gt;
&lt;p&gt;For that reason, &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://raygon-renderer.github.io/thermite/thermite_compensated/index.html&quot;&gt;&lt;code&gt;thermite-compensated&lt;/code&gt;&lt;/a&gt; previously would emit a &lt;code&gt;compile_error!&lt;/code&gt; if used with the &lt;code&gt;algebraic_scalar&lt;/code&gt; feature enabled. It would have been simply broken.&lt;/p&gt;
&lt;h2 id=&quot;the-solution-is-more-than-the-two-sum-of-its-parts&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-solution-is-more-than-the-two-sum-of-its-parts&quot; aria-label=&quot;Permalink to this section&quot;&gt;#&lt;/a&gt;
The Solution is more than the &lt;code&gt;two_sum&lt;/code&gt; of its parts&lt;/h2&gt;
&lt;p&gt;Fixing all of these weak points was simply a matter of moving &lt;code&gt;two_sum&lt;/code&gt;/&lt;code&gt;two_diff&lt;/code&gt;/&lt;code&gt;two_prod&lt;/code&gt;/&lt;code&gt;two_quot&lt;/code&gt; from &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://raygon-renderer.github.io/thermite/thermite_compensated/index.html&quot;&gt;&lt;code&gt;thermite-compensated&lt;/code&gt;&lt;/a&gt; and putting them on &lt;code&gt;FloatRegister&lt;/code&gt;/&lt;code&gt;FloatVectorWithBits&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That way they can bypass the algebraic math intrinsics in the scalar backend implementation, using strict operators for compensated arithmetic, while also allowing anyone to use them anywhere that &lt;code&gt;FloatVectorWithBits&lt;/code&gt; is available. That&#39;s actually an important distinction, because if it were only &lt;code&gt;FloatVector&lt;/code&gt;, &lt;code&gt;two_sum&lt;/code&gt; would have to be available on &lt;code&gt;Complex&lt;/code&gt; or &lt;code&gt;Dual&lt;/code&gt; composite types that implement &lt;code&gt;FloatVector&lt;/code&gt;. Compensated arithmetic on those
only makes sense as an implementation detail, not on the public interface.&lt;/p&gt;
&lt;p&gt;Furthermore, FMA, even &lt;code&gt;mul_adde&lt;/code&gt; (opportunistic FMA: fused where the hardware has it, separate mul+add where not) and family, inherently use strict operators as well so they cannot be reassociated.&lt;/p&gt;
&lt;p&gt;Although not taking advantage of algebraic reassociation, LLVM is smart enough to see that &lt;code&gt;a.two_sum(b).0&lt;/code&gt; (accessing only the sum, ignoring the error) can ignore the error calculation entirely, returning &lt;code&gt;a + b&lt;/code&gt;, &lt;em&gt;but with a strict operator&lt;/em&gt;. In a way it provides an escape hatch for algorithms to ensure that the sum is exactly that, not mixed in with nearby values. Same with the others.&lt;/p&gt;
&lt;p&gt;This pattern and just using &lt;code&gt;two_sum&lt;/code&gt;/&lt;code&gt;two_prod&lt;/code&gt; deep in the math implementations helped improve or stabilize precision across a wide variety of functions, and fixed a few latent bugs that would have crippled the emulated FMA paths. Range
reduction, accumulators, etc., are now fixed and stable under the feature.&lt;/p&gt;
&lt;p&gt;Originally, this change was just because I wanted &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://raygon-renderer.github.io/thermite/thermite_compensated/index.html&quot;&gt;&lt;code&gt;thermite-compensated&lt;/code&gt;&lt;/a&gt; to compile regardless of what crate features users used. However, I&#39;ve since found quite a few places in Thermite&#39;s math libraries where either strict operators or compensated arithmetic significantly improve precision, nearly for free in many cases, and the autovectorizer can do an even better job everywhere if that feature is enabled. Win-win.&lt;/p&gt;
</content>
        
    </entry>
</feed>
