<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Ken Micklas</title>
    <link rel="self" type="application/atom+xml" href="https://tech.kmicklas.com/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://tech.kmicklas.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2025-04-23T00:00:00+00:00</updated>
    <id>https://tech.kmicklas.com/atom.xml</id>
    <entry xml:lang="en">
        <title>The Prospero Challenge: Artisinal JIT</title>
        <published>2025-04-23T00:00:00+00:00</published>
        <updated>2025-04-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            Ken Micklas
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://tech.kmicklas.com/posts/prospero/"/>
        <id>https://tech.kmicklas.com/posts/prospero/</id>
        
        <content type="html" xml:base="https://tech.kmicklas.com/posts/prospero/">&lt;p&gt;I recently got effectively nerd-sniped by Matt Keeter&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;www.mattkeeter.com&#x2F;projects&#x2F;prospero&#x2F;&quot;&gt;Prospero Challenge&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The reference implementation is a simple interpreter which walks through the instruction list, and uses NumPy to compute in bulk the values at each pixel location.
Intermediate results (complete grids of values) are stored in memory for use by later instructions.
NumPy takes advantage of SIMD for the actual calculations, but the end result is still fairly slow because the memory traffic is extremely high.
Matt notes that this allocates 60 GB of RAM for intermediate results (but some &lt;a href=&quot;https:&#x2F;&#x2F;bernsteinbear.com&#x2F;blog&#x2F;prospero&#x2F;&quot;&gt;basic optimization&lt;&#x2F;a&gt; helps a lot with peak memory usage).&lt;&#x2F;p&gt;
&lt;p&gt;Since the data dependencies are &lt;em&gt;within&lt;&#x2F;em&gt; each pixel and not &lt;em&gt;across&lt;&#x2F;em&gt; pixels, my first thought was that we can at least improve cache usage a lot by iterating over pixels and computing the full expression, rather than iterating over instructions and computing the full grid.
Of course, we can still use SIMD to compute multiple pixels at a time up to the supported width of our favorite SIMD target.&lt;&#x2F;p&gt;
&lt;p&gt;Even better, a quick analysis confirmed what I was hoping for: a significant fraction of intermediate results are used only once, and often shortly after they are computed.
So in theory we can keep these in registers and never store them in memory at all!&lt;&#x2F;p&gt;
&lt;p&gt;However, computing &quot;deep&quot; slices ruins one advantage the reference implementation had on its side: the interpreter loop overhead really starts to matter if you need to run it hundreds of thousands of times.
Although Python is notoriously slow, given that there are ~1 million pixels and ~8 thousand instructions, I suspect (but did not measure) that the interpreter overhead is negligible in the reference implementation.&lt;&#x2F;p&gt;
&lt;p&gt;If we can&#x27;t interpret, what are our options?
Other submissions tried all kinds of off the shelf high-powered JIT&#x2F;compiler tools, like CUDA, LLVM, Cranelift, and others including Matt&#x27;s own Fidget system for implicit surfaces.
Cranelift has a reputation for being easy to set up and use, but LLVM and CUDA... not so much.
But even a friendly JIT like Cranelift seems pretty overpowered considering that we have to support 11 instructions and no control flow.&lt;&#x2F;p&gt;
&lt;p&gt;How hard can it be to just make our own JIT and output some simple AVX instructions?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-instructions&quot;&gt;The instructions&lt;&#x2F;h2&gt;
&lt;p&gt;I don&#x27;t currently have a machine with AVX-512, so, so let&#x27;s look at what we can do with AVX2 for each instruction in &lt;code&gt;prospero.vm&lt;&#x2F;code&gt;.
Each 256 bit AVX register (&lt;code&gt;ymm0&lt;&#x2F;code&gt;-&lt;code&gt;ymm15&lt;&#x2F;code&gt;) can hold 8 single-precision floating point numbers, so we&#x27;ll evaluate 8 pixels at a time.
For demonstration purposes we&#x27;ll store the result in &lt;code&gt;ymm0&lt;&#x2F;code&gt; in each case, with arguments in &lt;code&gt;ymm1&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ymm2&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;var-x-var-y&quot;&gt;&lt;code&gt;var-x&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;var-y&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Let&#x27;s just assume these are already stored (aligned!) in memory, varying appropriately for the pixels we&#x27;re computing:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vmovaps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;rdi&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vmovaps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;rsi&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;const&quot;&gt;&lt;code&gt;const&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;We can&#x27;t directly broadcast a constant to an AVX register; we need to first move it into an SSE register (&lt;code&gt;xmm0&lt;&#x2F;code&gt;), which occupies the bottom part of its corresponding AVX register.
Annoyingly, we also can&#x27;t put a constant straight into the SSE register either.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;mov &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;eax&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;N
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vmovd &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;xmm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;eax
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vbroadcastss &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;xmm0
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This felt like a lot of instructions and it seems like loading from memory is more typical.
I got a minor 1.5% improvement by passing an array with all the constants pre-broadcasted and just loading them directly, but it also makes the compiler code somewhat more complex.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vmovaps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;r8 &lt;&#x2F;span&gt;&lt;span&gt;+ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;OFFSET&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;the-binary-operators&quot;&gt;The binary operators&lt;&#x2F;h3&gt;
&lt;p&gt;These are easy!
Thankfully each binary operator corresponds to exactly one 3-parameter instruction:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vaddps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm2
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vsubps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm2
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vmulps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm2
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vmaxps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm2
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vminps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm2
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;neg&quot;&gt;&lt;code&gt;neg&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;I was kind of surprised that there is no native negation instruction.
The Internet suggests a few different ways to implement it, but I chose the simple option of getting a zero register with XOR and then subtracting:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vxorps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vsubps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;square&quot;&gt;&lt;code&gt;square&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Multiply by itself, of course:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vmulps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;sqrt&quot;&gt;&lt;code&gt;sqrt&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Thankfully also just one instruction:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;vsqrtps &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;ymm1
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;generating-machine-code&quot;&gt;Generating machine code&lt;&#x2F;h2&gt;
&lt;p&gt;Generating the correct machine code directly for our desired instructions would not be terribly difficult but it would be incredibly tedious.
The Rust crate &lt;a href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;crate&#x2F;iced-x86&#x2F;latest&quot;&gt;&lt;code&gt;iced-x86&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; frees us from having to worry about encoding details.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;use &lt;&#x2F;span&gt;&lt;span&gt;iced_x86::code_asm::*;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;let mut&lt;&#x2F;span&gt;&lt;span&gt; asm = CodeAssembler::new(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;64&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;unwrap&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;asm.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;vmovaps&lt;&#x2F;span&gt;&lt;span&gt;(ymm0, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;ptr&lt;&#x2F;span&gt;&lt;span&gt;(rsi)).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;unwrap&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Using the literal number &lt;code&gt;64&lt;&#x2F;code&gt; rather than an enum to indicate 64-bit mode is a bit WTF but the library seems otherwise of decent quality.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;register-allocation&quot;&gt;Register allocation&lt;&#x2F;h2&gt;
&lt;p&gt;This is the main hard part, especially as there are far more intermediate values than registers, so spill choices ought to matter a lot.
I wanted to see how well a really simple linear-scan allocator would do.&lt;&#x2F;p&gt;
&lt;p&gt;At a high level, the algorithm is pretty simple:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Compute live ranges for each intermediate value&lt;&#x2F;li&gt;
&lt;li&gt;Walk over all the instructions, and allocate parameter&#x2F;output slots greedily
&lt;ul&gt;
&lt;li&gt;If a parameter is already in a register, use that&lt;&#x2F;li&gt;
&lt;li&gt;If a parameter has been spilled to memory, choose a register to load it to&lt;&#x2F;li&gt;
&lt;li&gt;When we run out of registers, choose a register to spill based on some heuristic&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The choice of heuristic is the interesting part.
I went with the dead simple thing of always spilling the value that has the earliest last use.
This is not great though if something is used only once far in the future and it holds onto a register the whole time.
I also tried &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Cache_replacement_policies#B%C3%A9l%C3%A1dy&amp;#x27;s_algorithm&quot;&gt;spilling the value with the latest &lt;em&gt;next&lt;&#x2F;em&gt; use&lt;&#x2F;a&gt;, but it was more complex and had slightly worse performance.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s also possible to avoid using registers altogether in some cases as AVX can take some parameters from memory, but for simplicity I always loaded spilled values into a fresh register.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;experiments&quot;&gt;Experiments&lt;&#x2F;h2&gt;
&lt;p&gt;These things turned out to make essentially no difference:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Aligning the memory usage and using &lt;code&gt;vmovaps&lt;&#x2F;code&gt; over &lt;code&gt;vmovups&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Hash_consing&quot;&gt;Hash consing&lt;&#x2F;a&gt; the expressions to remove duplicate values, which eliminates 275 out of the 7866 instructions&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;These made a big difference:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Reusing spill slots when they become dead (more on this below)&lt;&#x2F;li&gt;
&lt;li&gt;Parallelizing the rendering with &lt;a href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;rayon&#x2F;latest&#x2F;rayon&#x2F;index.html&quot;&gt;&lt;code&gt;rayon&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;results&quot;&gt;Results&lt;&#x2F;h2&gt;
&lt;p&gt;TLDR: on my Ryzen 7 2700X desktop (8 cores, 16 threads), the full 1024x1024 image renders in &lt;strong&gt;~48 ms&lt;&#x2F;strong&gt;.
For comparison on my ThinkPad with a Core i7-1355U (2 P-cores, 8 E-cores, 12 threads), it takes ~&lt;strong&gt;80 ms&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This seems pretty solid for a CPU-only solution that is not using any fancy expression simplification techniques.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;tech.kmicklas.com&#x2F;posts&#x2F;prospero&#x2F;out.png&quot; alt=&quot;Prospero rendering&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;startup-latency&quot;&gt;Startup latency&lt;&#x2F;h3&gt;
&lt;p&gt;Since we have a JIT, there is a startup cost to instantiating the machine code for the program.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Phase&lt;&#x2F;th&gt;&lt;th&gt;Ryzen 7 2700X (μs)&lt;&#x2F;th&gt;&lt;th&gt;Core i7-1355U (μs)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Parsing&lt;&#x2F;td&gt;&lt;td&gt;702&lt;&#x2F;td&gt;&lt;td&gt;519&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Optimization (deduplication by hash consing)&lt;&#x2F;td&gt;&lt;td&gt;1,207&lt;&#x2F;td&gt;&lt;td&gt;885&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Generating machine code&lt;&#x2F;td&gt;&lt;td&gt;5,029&lt;&#x2F;td&gt;&lt;td&gt;2,627&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Compilation is not totally insignificant, but I didn&#x27;t try to optimize it.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s interesting to see that the laptop, while slower overall, beats the desktop by so much on startup with its newer CPU and better single core performance.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;parallelization&quot;&gt;Parallelization&lt;&#x2F;h3&gt;
&lt;p&gt;On the desktop scaling is fairly close to linear up to the number of physical cores:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;tech.kmicklas.com&#x2F;posts&#x2F;prospero&#x2F;by-threads.svg&quot; alt=&quot;Time by number of threads&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;reusing-spill-slots&quot;&gt;Reusing spill slots&lt;&#x2F;h3&gt;
&lt;p&gt;Naively each time we spill a value it would get a fresh &quot;slot&quot; in memory.
However once a spilled value is loaded for the last time, a future spill can reuse that slot.
It makes sense that this would help with cache locality, but the effect was much larger than I expected.&lt;&#x2F;p&gt;
&lt;p&gt;On the laptop[^overheat] this brought rendering time from &lt;strong&gt;119 ms&lt;&#x2F;strong&gt; to &lt;strong&gt;54 ms&lt;&#x2F;strong&gt;.
The actual number of slots needed went from 2781 (~89 kB) to 142 (~4.5 kB).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;number-of-registers&quot;&gt;Number of registers&lt;&#x2F;h3&gt;
&lt;p&gt;The above effect made me start to question how much register use really mattered, or if it was all just a question of cache locality.
Threads introduce quite a bit of noise, so rendering 128x128 on the desktop with one thread, from the minimum possible 3 registers up to using all 16:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;tech.kmicklas.com&#x2F;posts&#x2F;prospero&#x2F;by-registers.svg&quot; alt=&quot;Time by number of registers&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Not at all a shape I expected, to say the least.[^laptop] (Reading the processor manuals to get to the bottom of this is a rabbit hole for another time.)&lt;&#x2F;p&gt;
&lt;p&gt;However the lack of a large monotonic improvement makes sense considering the relatively small change in memory accesses:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;tech.kmicklas.com&#x2F;posts&#x2F;prospero&#x2F;memory.svg&quot; alt=&quot;Memory accesses by number of registers&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;You can find &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;kmicklas&#x2F;prospero&quot;&gt;the code&lt;&#x2F;a&gt; on GitHub.&lt;&#x2F;p&gt;
&lt;p&gt;If there is any moral to this story, it&#x27;s that performance at this level is very unpredictable.
It&#x27;s a bit of a cliche that you should measure and not guess performance, but it was more true here than I expected.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;future-experiments-to-try&quot;&gt;Future experiments to try&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;Try doubling the pixel batch size on a CPU with AVX-512 (or quadrupling with FP16?)&lt;&#x2F;li&gt;
&lt;li&gt;Is there a topological sorting of the instructions which has better locality and substantially less required spilling?&lt;&#x2F;li&gt;
&lt;li&gt;Keeping the simplicity of the interpreter but improving the cache locality by operating in medium sized batches of pixels; is memory bandwidth a problem if it mostly fits in L1 cache?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;[^overheat] Sadly at this point in development my aging desktop decided to start overheating when running the benchmarks...&lt;&#x2F;p&gt;
&lt;p&gt;[^laptop] The shape on the laptop is somewhat closer to monotonic but still a bit strange.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Talk: A deep dive into Rust UI</title>
        <published>2024-09-14T00:00:00+00:00</published>
        <updated>2024-09-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            Ken Micklas
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://tech.kmicklas.com/posts/a-deep-dive-into-rust-ui/"/>
        <id>https://tech.kmicklas.com/posts/a-deep-dive-into-rust-ui/</id>
        
        <content type="html" xml:base="https://tech.kmicklas.com/posts/a-deep-dive-into-rust-ui/">&lt;iframe width=&quot;784&quot; height=&quot;441&quot; src=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;LLno9MgyxUA?si=hwTqjSlEcRiqv-OG&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;I gave this talk at the &lt;a href=&quot;https:&#x2F;&#x2F;www.meetup.com&#x2F;Rust-London-User-Group&#x2F;&quot;&gt;Rust London User Group&lt;&#x2F;a&gt; in July.&lt;&#x2F;p&gt;
&lt;p&gt;While the difficulties of creating an ergonomic (and performant) UI framework in Rust are well known, attempting my own convinced me that the design space is still relatively unexplored.
This talk illustrates some of the general challenges, as well as specific tricks I learned while creating &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;kmicklas&#x2F;ravel&quot;&gt;Ravel&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Benchmarking Linux process sandboxing mechanisms</title>
        <published>2023-10-22T00:00:00+00:00</published>
        <updated>2023-10-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            Ken Micklas
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://tech.kmicklas.com/posts/benchmarking-linux-sandboxing/"/>
        <id>https://tech.kmicklas.com/posts/benchmarking-linux-sandboxing/</id>
        
        <content type="html" xml:base="https://tech.kmicklas.com/posts/benchmarking-linux-sandboxing/">&lt;p&gt;Build systems are in the business of computing what are nominally pure functions, but in the messy world of Unix files and processes.
If you care about your builds being correct, reliable, reproducible, or secure, you probably would would like them to avoid arbitrarily accessing the network or filesystem of the host.
Sources of nondeterminism such as time would ideally be controlled as well.
Maybe someday we&#x27;ll perform all builds in a deterministic WebAssembly sandbox, but for now the state of the art is mostly Unix process isolation mechanisms.&lt;&#x2F;p&gt;
&lt;p&gt;Despite having worked on build systems for a good chunk of my career, I realized that I actually had a pretty weak intuition for the performance costs of any kind of sandboxing.&lt;&#x2F;p&gt;
&lt;p&gt;This is a very important question if you would like to design a build system or optimize the use of one.
It could impact both what forms of sandboxing are viable, and at what granularity to apply them.&lt;&#x2F;p&gt;
&lt;p&gt;If sandboxing adds a lot of overhead on top of regular process invocations, then maybe you want sandboxing to apply on the level of &quot;projects&quot; in a multi-project build graph (as in &lt;a href=&quot;https:&#x2F;&#x2F;nixos.org&#x2F;&quot;&gt;Nix&lt;&#x2F;a&gt;&lt;sup class=&quot;footnote-reference&quot;&gt;&lt;a href=&quot;#nix&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;).
If, on the other hand, sandboxing adds almost no overhead, then you can use it anywhere that already has a process boundary.
Build systems like &lt;a href=&quot;https:&#x2F;&#x2F;bazel.build&#x2F;&quot;&gt;Bazel&lt;&#x2F;a&gt;&lt;sup class=&quot;footnote-reference&quot;&gt;&lt;a href=&quot;#bazel&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; use a minimal form of sandboxing at the level of individual translation units (e.g. object files in C++), but as far as I know there is little precedent for &quot;full&quot; sandboxing at this granularity&lt;sup class=&quot;footnote-reference&quot;&gt;&lt;a href=&quot;#full-sandbox&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I couldn&#x27;t find any good reference for the costs of the different sandboxing technologies provided by Linux, so I decided to measure the performance of a few popular tools.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sandboxing-on-linux&quot;&gt;Sandboxing on Linux&lt;&#x2F;h2&gt;
&lt;p&gt;In this post I&#x27;m only going to look at Linux.
There are three major components of a build-grade process sandbox on Linux.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;namespaces.7.html&quot;&gt;&lt;strong&gt;Namespaces&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; give the sandboxed process a &quot;clean room&quot; environment separated from the user accounts, network, process IDs, etc. of the host system.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man2&#x2F;pivot_root.2.html&quot;&gt;&lt;strong&gt;&lt;code&gt;pivot_root&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; isolates the filesystem root to a sandbox directory.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Mount_(Unix)#Bind_mounting&quot;&gt;&lt;strong&gt;Bind mounts&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; make it possible to virtualize the filesystem tree in the sandbox, so it doesn&#x27;t need to correspond directly to some on-disk tree externally.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Process sandboxing has become prominent mainly thanks to Linux containers, an ecosystem and movement primarily oriented toward the needs of running backend services.
Sadly, adoption in build systems still lags far behind the world of services.
Since service startup latency isn&#x27;t usually a huge concern, it also wouldn&#x27;t surprise me if the relevant kernel code paths are not as optimized as they could be.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-benchmarks&quot;&gt;The benchmarks&lt;&#x2F;h2&gt;
&lt;p&gt;In each benchmark, the underlying process is &lt;a href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;true.1.html&quot;&gt;&lt;code&gt;true&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, which exits immediately.
The measured time always includes launching the process and waiting for its completion.
The following setups are tested:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;No sandboxing, just a regular Unix process&lt;&#x2F;li&gt;
&lt;li&gt;No sandboxing, but executed under &lt;code&gt;sh -c&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Minimal &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;containers&#x2F;bubblewrap&quot;&gt;Bubblewrap&lt;&#x2F;a&gt; sandbox (just remounting &lt;code&gt;&#x2F;&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Full Bubblewrap sandbox with all namespaces unshared&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;a href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;unshare.1.html&quot;&gt;&lt;code&gt;unshare&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; command with all namespaces unshared&lt;&#x2F;li&gt;
&lt;li&gt;The Rust &lt;a href=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;unshare&quot;&gt;&lt;code&gt;unshare&lt;&#x2F;code&gt; crate&lt;&#x2F;a&gt; with all namespaces unshared&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;docker run&lt;&#x2F;code&gt; (with &lt;a href=&quot;https:&#x2F;&#x2F;hub.docker.com&#x2F;_&#x2F;alpine&quot;&gt;&lt;code&gt;alpine&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; image)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;podman run&lt;&#x2F;code&gt; (with &lt;code&gt;alpine&lt;&#x2F;code&gt; image)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Just a regular &lt;code&gt;true&lt;&#x2F;code&gt; process gives us the baseline for process invocation latency.
Running under &lt;code&gt;sh&lt;&#x2F;code&gt; doesn&#x27;t tell us anything about sandboxing, but it&#x27;s a useful sense of scale because wrapper shell scripts are extremely common in most build setups.&lt;&#x2F;p&gt;
&lt;p&gt;Bubblewrap &#x2F; &lt;code&gt;bwrap&lt;&#x2F;code&gt; is a very nice low level sandboxing tool which supports a huge number of isolation features.
The second configuration relative to the first should isolate the relative cost of the namespace setup specifically.
Since Bubblewrap requires running under an explicit filesystem sandbox, I add &lt;code&gt;unshare&lt;&#x2F;code&gt; (which just does namespaces) to try to isolate the cost of the bind mount.&lt;&#x2F;p&gt;
&lt;p&gt;In a real build system which natively understands sandboxing, it might make sense to perform the sandbox setup system calls directly in the build system process.
This would only incur one level of process overhead, rather than adding an extra process by using something like &lt;code&gt;bwrap&lt;&#x2F;code&gt;.
To simulate this, I test a Rust library which sets up Linux namespaces in-process, and then invokes the target process (still &lt;code&gt;true&lt;&#x2F;code&gt;).
Since the benchmark script is written in Rust and the measurements are taken internal to the process, this avoids the extra process overhead.&lt;&#x2F;p&gt;
&lt;p&gt;For completeness, I test &lt;a href=&quot;https:&#x2F;&#x2F;www.docker.com&#x2F;&quot;&gt;Docker&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;podman.io&#x2F;&quot;&gt;Podman&lt;&#x2F;a&gt; since they provide most of the isolation features relevant to a build system (plus a lot more features relevant to services).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-note-on-path-resolution&quot;&gt;A note on &lt;code&gt;$PATH&lt;&#x2F;code&gt; resolution&lt;&#x2F;h3&gt;
&lt;p&gt;I ran the benchmarks in a &lt;a href=&quot;https:&#x2F;&#x2F;nix.dev&#x2F;tutorials&#x2F;first-steps&#x2F;declarative-and-reproducible-developer-environments&quot;&gt;Nix shell&lt;&#x2F;a&gt;, which sets a very long &lt;code&gt;$PATH&lt;&#x2F;code&gt; list, so I (correctly) guessed that searching for &lt;code&gt;true&lt;&#x2F;code&gt; by name might affect the results.
Except for the container tests (where this shouldn&#x27;t be as much of an issue with the &lt;code&gt;alpine&lt;&#x2F;code&gt; image), each mechanism invokes &lt;code&gt;true&lt;&#x2F;code&gt; through a resolved path.&lt;&#x2F;p&gt;
&lt;p&gt;While I&#x27;m not super interested in the overhead incurred by long &lt;code&gt;$PATH&lt;&#x2F;code&gt; searches, I also added a benchmark which just invokes &lt;code&gt;true&lt;&#x2F;code&gt; as is.
Like &lt;code&gt;sh -c&lt;&#x2F;code&gt;, this can help give a useful sense of scale.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;results&quot;&gt;Results&lt;&#x2F;h2&gt;
&lt;p&gt;My machine is a Ryzen 7 2700X desktop running NixOS 23.05 on ZFS.
None of the benchmarks had any significant variance (after warm-up time), so the numbers here are all just averages.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Mechanism&lt;&#x2F;th&gt;&lt;th&gt;Total time (ms)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;normal process, resolved&lt;&#x2F;td&gt;&lt;td&gt;0.653&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;normal process, unresolved&lt;&#x2F;td&gt;&lt;td&gt;1.067&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sh -c&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;3.670&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;bwrap&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;2.171&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;bwrap&lt;&#x2F;code&gt; with namespaces&lt;&#x2F;td&gt;&lt;td&gt;2.903&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;unshare&lt;&#x2F;code&gt; command&lt;&#x2F;td&gt;&lt;td&gt;3.110&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;unshare&lt;&#x2F;code&gt; library&lt;&#x2F;td&gt;&lt;td&gt;2.293&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;docker run&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;248&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;podman run&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;402&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Omitting the container tests for reasons of scale, I&#x27;ve plotted both the total time of each mechanism, and the marginal overhead on top of the baseline process invocation time:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;tech.kmicklas.com&#x2F;posts&#x2F;benchmarking-linux-sandboxing&#x2F;chart.svg&quot; alt=&quot;Sandboxing mechanism performance&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;kmicklas&#x2F;sandbox-benchmarks&quot;&gt;The benchmark code&lt;&#x2F;a&gt; can be found on GitHub.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;&#x2F;h2&gt;
&lt;p&gt;Sandboxing via Linux namespaces adds an overhead a few times the overhead of an unsandboxed process.
However it&#x27;s still quite fast, so sandboxing should be used whenever possible in build systems.
In these benchmarks we used &lt;code&gt;true&lt;&#x2F;code&gt; which should have negligible run time, but a real compiler or code generator likely dwarfs the process invocation time, sandboxed or not.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s important to remember that any process invocation is still about five orders of magnitude slower than a function call.
It would be really cool to design a compiler which can use a build system to cache the compilation of individual functions, for example, but the overhead would be prohibitively expensive.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bubblewrap&quot;&gt;Bubblewrap&lt;&#x2F;h3&gt;
&lt;p&gt;Bubblewrap looks pretty well optimized.
It&#x27;s wholly superior to the &lt;code&gt;unshare&lt;&#x2F;code&gt; command, and its namespacing overhead (relative to the filesystem-only configuration) is significantly less than the &lt;code&gt;unshare&lt;&#x2F;code&gt; library, which avoids a whole process layer.
This is impressive considering Bubblewrap&#x27;s focus is security.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;containers&quot;&gt;Containers&lt;&#x2F;h3&gt;
&lt;p&gt;I was quite surprised that Docker and Podman had such high latency.
They obviously do a lot more bookkeeping, but I thought it would be maybe an order of magnitude slower at worst.
Widespread familiarity with container technology may make it seem like an attractive sandboxing mechanism for builds (and technically it checks most of the right boxes), but the overhead makes it problematic.
You certainly could not use it for translation-unit level granularity, but the fact that it&#x27;s in the realm of human-observable latency makes me want to rule it out even for very course granularity.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shell&quot;&gt;Shell&lt;&#x2F;h3&gt;
&lt;p&gt;I was not surprised to find that adding a shell wrapper has an overhead significantly greater than the theoretical minimum (i.e. one extra process overhead).
However the fact that it was roughly on par with the sandboxing mechanisms is a useful heuristic: if you wouldn&#x27;t think twice about wrapping a command in a shell script (very common in build systems), you probably shouldn&#x27;t care about sandboxing overhead.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;path-resolution&quot;&gt;&lt;code&gt;$PATH&lt;&#x2F;code&gt; resolution&lt;&#x2F;h3&gt;
&lt;p&gt;Resolving &lt;code&gt;true&lt;&#x2F;code&gt; on each invocation added a very significant overhead.
I really expected filesystem caching in the kernel to take care of this after the initial run, but apparently not.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;div class=&quot;footnote-definition&quot; id=&quot;nix&quot;&gt;&lt;sup class=&quot;footnote-definition-label&quot;&gt;1&lt;&#x2F;sup&gt;
&lt;p&gt;Technically this isn&#x27;t enforced by Nix itself, but it&#x27;s the canonical way to use it as defined by &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NixOS&#x2F;nixpkgs&quot;&gt;nixpkgs&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;footnote-definition&quot; id=&quot;bazel&quot;&gt;&lt;sup class=&quot;footnote-definition-label&quot;&gt;2&lt;&#x2F;sup&gt;
&lt;p&gt;It&#x27;s always bothered me that Bazel includes &quot;correct&quot; in its motto, when it doesn&#x27;t even try to stop you from using tools from the host.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;footnote-definition&quot; id=&quot;full-sandbox&quot;&gt;&lt;sup class=&quot;footnote-definition-label&quot;&gt;3&lt;&#x2F;sup&gt;
&lt;p&gt;Bazel has &lt;a href=&quot;https:&#x2F;&#x2F;bazel.build&#x2F;reference&#x2F;command-line-reference#flag--experimental_use_hermetic_linux_sandbox&quot;&gt;&lt;code&gt;--experimental_use_hermetic_linux_sandbox&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; but it&#x27;s not widely used and very off the beaten path.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>A trilemma in functional reactive programming models</title>
        <published>2023-06-19T00:00:00+00:00</published>
        <updated>2023-06-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            Ken Micklas
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://tech.kmicklas.com/posts/frp-trilemma/"/>
        <id>https://tech.kmicklas.com/posts/frp-trilemma/</id>
        
        <content type="html" xml:base="https://tech.kmicklas.com/posts/frp-trilemma/">&lt;p&gt;A few months ago I tried to implement a functional reactive programming library in Rust.
It was a good experience, but ultimately I abandoned it due to limitations of Rust that were preventing me from making a system as ergonomic as I would like.
However, that&#x27;s not what this post is about.&lt;&#x2F;p&gt;
&lt;p&gt;The FRP framework I am most familiar with is &lt;a href=&quot;https:&#x2F;&#x2F;reflex-frp.org&#x2F;&quot;&gt;Reflex&lt;&#x2F;a&gt;, and to start, I mostly tried to copy its semantics.
It being Rust, I was determined to make it performant, eventually in constant factors, but to start at least getting the asymptotics right.
However I ran into an algorithmic difficulty which led me to question the semantics exposed by Reflex and other similar frameworks.
That&#x27;s what this post is about.&lt;&#x2F;p&gt;
&lt;p&gt;Eventually I concluded that there are a variety of alternate semantics that are both easier to implement and more efficient, without losing any important expressivity (at least for GUI use cases).&lt;&#x2F;p&gt;
&lt;p&gt;This post assumes general familiarity with push-based (or hybrid &lt;a href=&quot;http:&#x2F;&#x2F;conal.net&#x2F;papers&#x2F;push-pull-frp&#x2F;&quot;&gt;push&#x2F;pull&lt;&#x2F;a&gt;) higher order FRP like Reflex or &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;SodiumFRP&quot;&gt;Sodium&lt;&#x2F;a&gt;.
Note that this is actually a pretty small category.
Most older FRP implementations are pull-based (i.e. not efficient at all) and many newer frameworks commonly referred to as FRP (like &lt;a href=&quot;https:&#x2F;&#x2F;reactivex.io&#x2F;&quot;&gt;ReactiveX&lt;&#x2F;a&gt; and its derivatives) lack the semantic foundations which make &quot;true&quot; FRP both very expressive and hard to implement.&lt;&#x2F;p&gt;
&lt;p&gt;I will be using terminology from Reflex but unfortunately this differs quite a bit between different FRP frameworks.
In particular, some systems use &lt;code&gt;Signal&lt;&#x2F;code&gt; or &lt;code&gt;Stream&lt;&#x2F;code&gt; for what Reflex calls &lt;code&gt;Event&lt;&#x2F;code&gt;, but I prefer &quot;event&quot; as it is more intuitive.&lt;&#x2F;p&gt;
&lt;p&gt;However, there is one important potential terminological confusion with the term &quot;event&quot; which deserves clarification.
In mainstream GUI systems, an event usually refers to an &lt;em&gt;occurrence&lt;&#x2F;em&gt; of an abstract potentially-happening thing.
In (first class) FRP, we build a reactive system by using various combinators to construct values that represent the &lt;em&gt;abstract&lt;&#x2F;em&gt; potential behavior of the system.
So an &quot;event&quot; object represents something that may fire with actual concrete values at various points in the program.
(Denotationally, an event for values of type &lt;code&gt;a&lt;&#x2F;code&gt; is like a mapping &lt;code&gt;Time -&amp;gt; Maybe a&lt;&#x2F;code&gt;.)
Here &quot;event&quot; always refers to this abstract object, not the concrete occurrence of the event.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;event-graphs&quot;&gt;Event graphs&lt;&#x2F;h2&gt;
&lt;p&gt;Efficient implementations of FRP are primarily concerned with a specific task, that is taking input events (e.g. button clicks) and activating a subset of observable output events.
The output events could have some IO effect (e.g. update widgets on the page) or update internal reactive data cells (&lt;code&gt;Behavior&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Dynamic&lt;&#x2F;code&gt; in Reflex).&lt;&#x2F;p&gt;
&lt;p&gt;One way to do this would be to &quot;run&quot; the entire known graph on each frame, like in an &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Immediate_mode_GUI&quot;&gt;immediate-mode GUI&lt;&#x2F;a&gt;.
This isn&#x27;t a totally crazy way to operate for most applications, but as the ongoing debates in the web world about virtual DOM diffing vs. fine grained reactivity show, it is less than ideal.&lt;&#x2F;p&gt;
&lt;p&gt;FRP implementations typically use behind the scenes something analogous the &quot;observer pattern&quot; from classic &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Retained_mode&quot;&gt;retained-mode&lt;&#x2F;a&gt; GUI frameworks to attach to each event a list of downstream events which need to be processed if the upstream event fires.
This allows them to (basically) start from the input events and wake up events downstream only as necessary until reaching some subset of the existing output events at the other end of the graph.&lt;&#x2F;p&gt;
&lt;p&gt;With basic event combinators like &lt;code&gt;filter&lt;&#x2F;code&gt;, &lt;code&gt;map&lt;&#x2F;code&gt;, etc., this is straightforward.
The complications arise when you add complex combinators that have more than a single input:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;haskell&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-haskell &quot;&gt;&lt;code class=&quot;language-haskell&quot; data-lang=&quot;haskell&quot;&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;merge &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;:: Event &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;a &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;-&amp;gt; Event &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;b &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;-&amp;gt; Event&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;These &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;a b&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fa1b3;&quot;&gt;switch &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;:: Behavior&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;Event &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;a&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;-&amp;gt; Event &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;a
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;hackage.haskell.org&#x2F;package&#x2F;reflex-0.9.0.1&#x2F;docs&#x2F;Reflex-Class.html#t:Behavior&quot;&gt;&lt;code&gt;Behavior&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; is a time-varying FRP value, and &lt;a href=&quot;https:&#x2F;&#x2F;hackage.haskell.org&#x2F;package&#x2F;these-1.2&#x2F;docs&#x2F;Data-These.html#t:These&quot;&gt;&lt;code&gt;These&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; is a very nice Haskell type representing two optional values where at least one must be present:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;haskell&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-haskell &quot;&gt;&lt;code class=&quot;language-haskell&quot; data-lang=&quot;haskell&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;These&lt;&#x2F;span&gt;&lt;span&gt; a b
&lt;&#x2F;span&gt;&lt;span&gt;  = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;This&lt;&#x2F;span&gt;&lt;span&gt; a
&lt;&#x2F;span&gt;&lt;span&gt;  | &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;That&lt;&#x2F;span&gt;&lt;span&gt; b
&lt;&#x2F;span&gt;&lt;span&gt;  | &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;These&lt;&#x2F;span&gt;&lt;span&gt; a b
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;the-merge-combinator&quot;&gt;The &lt;code&gt;merge&lt;&#x2F;code&gt;&lt;sup class=&quot;footnote-reference&quot;&gt;&lt;a href=&quot;#1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; combinator&lt;&#x2F;h3&gt;
&lt;p&gt;The hard part about &lt;code&gt;merge&lt;&#x2F;code&gt; is that it waits on two input events, but after either one of them fires, we can&#x27;t yet know what the resulting value will be.
We have to first know whether the other side will also fire.&lt;&#x2F;p&gt;
&lt;p&gt;The only way to get the correct answer and do minimal work, is to process potentially triggered events in a topological ordering of the relation implied by input dependencies.
In other words, if one input to a &lt;code&gt;merge&lt;&#x2F;code&gt; fires, we know that the &lt;code&gt;merge&lt;&#x2F;code&gt; event itself will fire, but we should delay processing it until all &quot;earlier&quot; events (which necessarily include its inputs) have been processed.&lt;&#x2F;p&gt;
&lt;p&gt;To handle this, we can assign each event a &quot;height&quot; based on its inputs, and use these as priorities in a queue.
A single-input event, like &lt;code&gt;map&lt;&#x2F;code&gt; or &lt;code&gt;filter&lt;&#x2F;code&gt;, would have a height one higher than its input, and &lt;code&gt;merge&lt;&#x2F;code&gt; would have a height one higher than the maximum of its inputs.
This automatically gives a topological ordering by construction.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-switch-combinator&quot;&gt;The &lt;a href=&quot;https:&#x2F;&#x2F;hackage.haskell.org&#x2F;package&#x2F;reflex-0.9.0.1&#x2F;docs&#x2F;Reflex-Class.html#v:switch&quot;&gt;&lt;code&gt;switch&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; combinator&lt;&#x2F;h3&gt;
&lt;p&gt;In a fully static event graph, assigning heights is easy, because they can be calculated directly when an event is constructed.
But we run into a problem with &lt;code&gt;switch&lt;&#x2F;code&gt;.
The value of the &lt;code&gt;Behavior&lt;&#x2F;code&gt; can change as time goes on, and because FRP is higher-order, it can come from &quot;anywhere&quot; so there is no statically known subset of events it could be.&lt;&#x2F;p&gt;
&lt;p&gt;Thus, if we want to use the &quot;height strategy&quot; to handle &lt;code&gt;merge&lt;&#x2F;code&gt; events, we&#x27;ll need to dynamically update the height of a &lt;code&gt;switch&lt;&#x2F;code&gt; event when it changes.
Just updating the &lt;code&gt;switch&lt;&#x2F;code&gt; event itself isn&#x27;t a problem, but we need to maintain the invariant that an event has a higher height than its inputs.
We now need to trace down the graph, finding any downstream events which transitively have it as an input, and update their heights to maintain the invariant.&lt;sup class=&quot;footnote-reference&quot;&gt;&lt;a href=&quot;#2&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Now we&#x27;re back to the same worst case performance as an immediate-mode GUI.
In practice, it &lt;em&gt;usually&lt;&#x2F;em&gt; won&#x27;t be that bad, but we have no easy ways to reason about when it will happen.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-trilemma&quot;&gt;The trilemma&lt;&#x2F;h2&gt;
&lt;p&gt;We need to make more precise what it would mean to do efficient event propagation.
Obviously we need to do work at least linear in the number of events that actually fire in a frame.
Additionally, any event that does fire may be the input to another event, which is therefore &quot;potentially firing&quot; but may decide to fire or not depending on the values it receives.
So efficient event propagation would mean doing work linear in the number of potentially firing events in a frame (which includes those that actually do fire).
Now we get to the trilemma:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;If we have both &lt;code&gt;switch&lt;&#x2F;code&gt; and &lt;code&gt;merge&lt;&#x2F;code&gt;, changing a &lt;code&gt;switch&lt;&#x2F;code&gt; and running an event propagation frame requires in the worst case work linear on the total number of existing events, even if this dwarfs the number of potentially firing events.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;If we get rid of &lt;code&gt;switch&lt;&#x2F;code&gt;, we can assign heights statically and process potentially firing events in a priority queue.&lt;&#x2F;li&gt;
&lt;li&gt;If we get rid of &lt;code&gt;merge&lt;&#x2F;code&gt;&lt;sup class=&quot;footnote-reference&quot;&gt;&lt;a href=&quot;#3&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, we don&#x27;t even need heights, and we can process potentially firing events in depth-first order.&lt;&#x2F;li&gt;
&lt;li&gt;If we keep both, to process &lt;code&gt;merge&lt;&#x2F;code&gt;s correctly we need to produce a new topological ordering which respects the new constraints changed by a &lt;code&gt;switch&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Formally proving that the final case cannot be done efficiently is beyond the scope of this post, but hopefully the intuition is clear.
Because a &lt;code&gt;switch&lt;&#x2F;code&gt; event can choose &quot;any&quot; event as its input, it can add an arbitrary ordering constraint to the graph, which means we can reduce the problem to &lt;em&gt;online&lt;&#x2F;em&gt; topological ordering.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;actually-existing-guis&quot;&gt;Actually existing GUIs&lt;&#x2F;h2&gt;
&lt;p&gt;Given that I was trying to implement FRP in Rust, I was determined to find a way to keep predictable performance and minimal graph traversal as a core feature.
Moreover, it occurred to me that, while retained-mode GUI programming is famously hard to do correctly, people &lt;em&gt;do&lt;&#x2F;em&gt; (with effort) create large correct GUI applications, and nowhere in their runtime behavior can we observe anything analogous to the downstream global event trace required to combine &lt;code&gt;switch&lt;&#x2F;code&gt; and &lt;code&gt;merge&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This suggests one of the following possibilities:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;There are useful GUI paradigms that &lt;em&gt;inherently&lt;&#x2F;em&gt; require global event tracing to implement correctly, and well known large GUIs do not express these patterns simply &lt;em&gt;because&lt;&#x2F;em&gt; they are hard to implement.&lt;&#x2F;li&gt;
&lt;li&gt;There are (effectively) no real world GUI paradigms which require global event tracing.&lt;&#x2F;li&gt;
&lt;li&gt;There &lt;em&gt;is&lt;&#x2F;em&gt; something analogous to global event tracing in certain existing GUI implementations, but it&#x27;s not obvious precisely because it&#x27;s more clearly handled with a domain-specific algorithm than a pervasive part of the reactivity machinery.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I think the last one is ultimately the answer.
A helpful analogy is tracing garbage collection.
With manual memory management (i.e. Rust), to implement an arbitrary algorithm, in the worse case you have to embed a garbage collector into the implementation.
However in practice this doesn&#x27;t really happen unless you&#x27;re implementing either an interpreter for a GC language, or some kind of graph algorithm where incremental cleanup is required.
While it&#x27;s certainly easier to use a language with GC and let the host GC handle these cases, it seems the surface area where the GC needs to run is almost always limited to a relatively isolated problem domain.
I like to think that a future Rust-like language would make it easy to use garbage collection as a library for these cases, with the tracing only happening on the domain heap and not the whole program.&lt;&#x2F;p&gt;
&lt;p&gt;Using Rust has made me moderately confident that, outside of these specialized domains, garbage collection isn&#x27;t really required for general purpose programming.
And actually, it seems like we should be even more confident that &quot;event tracing&quot; isn&#x27;t required for GUIs.&lt;&#x2F;p&gt;
&lt;p&gt;General purpose programs are higher order and have extremely complex dynamics that are very hard for experienced programmers to fit in their heads, and yet their object lifetimes are still simple enough that they can almost always be handled by stack regions or reference counting (i.e. only statically known cycles).
On the other hand, GUIs typically have only first order behavior, and even the most complex GUI behavior pales in comparison to the complexity in &quot;backend&quot; systems.
Although bugs in GUIs are common (probably because they aren&#x27;t using FRP!), describing the correct behavior in a modular fashion is usually not hard.&lt;&#x2F;p&gt;
&lt;p&gt;The main exception is GUIs which embed higher order behavior from some other domain, for example Excel.
But of course Excel has a custom incremental update engine, it doesn&#x27;t use GUI event handlers to keep track of cell dependencies.&lt;&#x2F;p&gt;
&lt;p&gt;Likewise, games have &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Entity_component_system&quot;&gt;entity component systems&lt;&#x2F;a&gt;, not elaborate webs of event handlers. (And at the level of memory management, they often use keys&#x2F;indices rather than a web of references.)&lt;&#x2F;p&gt;
&lt;p&gt;In the case of reactivity, what can we achieve if we assume that &quot;extremely dynamic event graphs&quot; correspond to limited problem domains better handled by the application?
Stay tuned for part two, where I&#x27;ll discuss the space of possible semantics for keeping the core ideas from FRP, without the need to ever do global event tracing.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;div class=&quot;footnote-definition&quot; id=&quot;1&quot;&gt;&lt;sup class=&quot;footnote-definition-label&quot;&gt;1&lt;&#x2F;sup&gt;
&lt;p&gt;I&#x27;ve presented here a simplification of &lt;a href=&quot;https:&#x2F;&#x2F;hackage.haskell.org&#x2F;package&#x2F;reflex-0.9.0.1&#x2F;docs&#x2F;Reflex-Class.html#v:merge&quot;&gt;Reflex&#x27;s &lt;code&gt;merge&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, since the actual version has some type-level complexity that is not relevant to FRP itself.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;footnote-definition&quot; id=&quot;2&quot;&gt;&lt;sup class=&quot;footnote-definition-label&quot;&gt;2&lt;&#x2F;sup&gt;
&lt;p&gt;There is a trick we can use to share the work of keeping heights up to date for a chain of single-input events like &lt;code&gt;map&lt;&#x2F;code&gt; or &lt;code&gt;filter&lt;&#x2F;code&gt;, but this won&#x27;t help us asymptotically unless we assume these make up increasing fractions of larger FRP graphs.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;footnote-definition&quot; id=&quot;3&quot;&gt;&lt;sup class=&quot;footnote-definition-label&quot;&gt;3&lt;&#x2F;sup&gt;
&lt;p&gt;This is the option taken by all other existing systems I&#x27;m aware of.
Please let me know if you find a counterexample!&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
</content>
        
    </entry>
</feed>
