<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://redvice.org/feed.xml" rel="self" type="application/atom+xml" /><link href="https://redvice.org/" rel="alternate" type="text/html" /><updated>2026-07-11T22:57:41+00:00</updated><id>https://redvice.org/feed.xml</id><title type="html">Red Vice</title><subtitle>A blog for technical ranting and rambling</subtitle><author><name>CoffeeFlux</name><email>ryan@luciaonline.net</email></author><entry><title type="html">Binary Coverage the Wrong Way</title><link href="https://redvice.org/2026/coverage-the-wrong-way/" rel="alternate" type="text/html" title="Binary Coverage the Wrong Way" /><published>2026-07-03T00:00:00+00:00</published><updated>2026-07-03T00:00:00+00:00</updated><id>https://redvice.org/2026/coverage-the-wrong-way</id><content type="html" xml:base="https://redvice.org/2026/coverage-the-wrong-way/"><![CDATA[<h2 id="the-right-way">The Right Way</h2>
<p>Way back in the age of the dinosaurs, if you wrote a program and wanted to test that it was correct, you mostly had two choices: you could either manually construct malformed inputs and try them in succession, or hook up a program which would randomly generate inputs and try them automatically <sup id="fnref:fuzzing"><a href="#fn:fuzzing" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. Those programs were called fuzzers, or sometimes generators, and this was “fuzz testing”. It also for quite a long time wasn’t very good: the generator would have no feedback from the program under test except “did this random input crash or not”. This feedback, although useful, doesn’t allow the fuzzer to tell if it is <em>making progress</em> relative to the last random input or doing a good job, and so your fuzzer would just repeatedly hit the same shallow code. You’d leave it running for a week and then when you come back realize it was never generating inputs that would hit interesting parts of your program, and they were all immediately being thrown out or never progressing past a <code class="language-plaintext highlighter-rouge">if(packet_header == 0xdeadbeef)</code> check.</p>

<p><a href="https://lcamtuf.coredump.cx/afl/">American Fuzzy Lop</a> revolutionized the field by introducing <em>coverage-guided</em> fuzzing. Instead of having the only feedback to your fuzzer be whether an input crashed or not, it also recoded what <em>code coverage</em> it hit over the course of a program execution; if an input executes a piece of the program that it never reached before, then it hit new code, and probably is now able to also test some new interesting surface area of the program which might have some bugs. This was immensely more effective than “dumb fuzzing” of yesteryear.</p>

<p>AFL collects coverage for programs by compiling the program under test with a custom clang compiler pass, which adds coverage feedback on CFG edges: if you hit a new basic block in the code, you get a new coverage bitmap entry set<sup id="fnref:ngram"><a href="#fn:ngram" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>. If running the program on an input sets results in a bitmap which has new bits set than previously, it hit new code; that input can be saved to use as the seed input to mutate for future executions.</p>

<p>You can also do feedback based fuzzing for blackbox binaries that you don’t have the source code to. <a href="https://aflplus.plus/">AFL++</a> for example has a <code class="language-plaintext highlighter-rouge">qemu_mode</code> backend: it executes the program under test with <code class="language-plaintext highlighter-rouge">qemu-user</code>, which will lift your x86 assembly to QEMU’s TCG IR and then lower it back with its JIT compiler to x86, but now which can safely execute under its type-2 hypervisor environment. AFL++ has a fork of QEMU which adds a custom TCG hook that adds coverage instrumentation to blocks in the middle-end; the IR for each block is extended with TCG operations that write a coverage bitmap entry the same way that AFL enhances clang IR with extra LLVM operations. Despite sounding like a lot of work, this <code class="language-plaintext highlighter-rouge">qemu_mode</code> is only like ~3-5x slower than running a program natively.</p>

<p>The gold standard for blackbox binary coverage is to let hardware do it for you. Instead of needing to add extra operations to the program under test, which need to be executed when you run a fuzz testcase, you can instead use <a href="https://github.com/intel/libipt">Intel PT</a>. Enabling certain performance counter configuration registers will start processor tracing: your CPU will then automatically record metadata as part of its execution pipeline, and the fuzzer can look at the resulting metadata to pull out a trace of what basic blocks it executed. This is <em>unreasonably</em> effective, with Intel PT based tracing only being something like ~10% slower than normal native execution.</p>

<p>The issue, of course, is that if you’re like me your laptop has an AMD processor and not Intel. AMD have their own magic performance counting features, including a branch trace buffer that records what code is being executed, but its 1) much worse documented 2) much slower than Intel PT 3) impossible to use in a precise manner - that is, the counters are either sampling based and so only give you <code class="language-plaintext highlighter-rouge">1/n</code> events, or may end up dropping events if a metadata buffer fill up before you’re able to drain it. In either case that makes it sadly unsuitable for fuzzing (you technically could build a fuzzer on top of merely stochastic coverage, and it might even not be that bad. But in practice no one has done so, and it’s probably not worth it.)</p>

<p>Which brings me to my bad idea. I wanted to write my own fuzzer for a side-project, and so wanted to collect precise blackbox binary coverage. But didn’t want to write my own dynamic recompilation engine (again), or buy a new laptop. What is a software engineer to do?</p>

<h2 id="the-wrong-way">The Wrong Way</h2>
<p>We can instead instrument the guest code by replacing all the branches with <code class="language-plaintext highlighter-rouge">INT3</code> instructions. Wait come back I can explain.</p>

<p>In order to collect precise coverage we need the program to do something whenever it hits a branch. We can’t insert new behavior in each block, because it already is using all of the bytes in the block for its normal instructions: trying to resize blocks requires fixing up code pointers, and then you’re just back at QEMU-but-bad. The minimal length of a branch instruction is two bytes long: <a href="https://www.felixcloutier.com/x86/jcc">Jcc</a> has a form which is only a single opcode byte plus a displacement to branch with. <code class="language-plaintext highlighter-rouge">INT3</code> is only one instruction long, hijacks control flow in a way that doesn’t clobber GPRs which would prevent continuing execution after the instruction, and gives us the location of the branch that we overwrite as context so we can compute the coverage bitmap entry that we want to record in our coverage map. It’s perfect!</p>

<p>Unfortunately, if you tried to do this on Linux with <code class="language-plaintext highlighter-rouge">ptrace</code> you’d probably want to die. Linux will turn hit <code class="language-plaintext highlighter-rouge">INT3</code> into a SIGTRAP. That can be caught by a <code class="language-plaintext highlighter-rouge">ptrace</code> based debugger that is supervising your process. That signal will include the tracee’s PC and register state, which you could use to implement the coverage hook, and then <code class="language-plaintext highlighter-rouge">PTRACE_CONT</code> to continue the process. It’s just that this entire dance would be several context switches: guest program in ring 3 -&gt; <code class="language-plaintext highlighter-rouge">INT3</code> trap to x86 interrupt handler in ring 0 -&gt; Linux kernel delivers a <code class="language-plaintext highlighter-rouge">ptrace</code> event in your debugger in ring 3 -&gt; your debugger <code class="language-plaintext highlighter-rouge">PTRACE_CONT</code> via syscall to ring 0 -&gt; Linux kernel syscall implementation resumes the guest in ring 3. Each of those context switches are very expensive, especially because context switching between privilege levels requires even more work from your processor than same-ring context switches. Oh, if only there was another way!</p>

<p>The answer, of course, is virtualization extensions. Your computer can run a smaller computer inside of it through the magical power of Intel VT-x (or, as we call it on Linux, <code class="language-plaintext highlighter-rouge">kvm</code>). The code inside the smaller computer thinks it is running completely bare metal: that guest code can configure interrupt handlers and MSRs and paging however it wants, just like a normal kernel, and even run userspace code under the guest kernel <sup id="fnref:hvci"><a href="#fn:hvci" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>. All while, in reality, the guest kernel is being ran in a virtualized machine hosted by your normal Linux kernel; you can configure it so that when the guest kernel tries to do I/O to the real world, they actually trap to the host kernel, which turns them into a userspace event that the “virtual machine manager” can implement and then resume the virtual machine with the result of. Crucially, while the virtual machine is executing, it is <em>executing bare metal</em> - your processor is actually running all of the instructions for the guest VM, and they all use the normal processor state like general purpose registers and hardware page tables, just with some extra bits that say to also do a hypervisor translation for the physical memory so it can’t reach outside the bounds of the VM and things. The important thing is that it means the VM runs at basically 100% native speed, except for when it needs to VMExit back to the host kernel because the guest kernel tried to do something that traps to the hypervisor. It turns out this works so well, in fact, that basically the entire internet runs on top of <code class="language-plaintext highlighter-rouge">kvm</code> in the form of cloud based hypervisors like Firecracker.</p>

<p><a href="https://info.varnish-software.com/blog/tinykvm-the-fastest-sandbox">TinyKVM</a> is a neat project by the author of Varnish<sup id="fnref:varnish"><a href="#fn:varnish" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>. It implements a VMM as a C++ library, which you can use to run arbitrary Linux binaries: it sets up a minimal guest VM with the required x86 processor state to execute usermode code, with a tiny guest kernel that has interrupts which mostly just trap to the VMM in the host. That means that you can run a guest binary in the VM, and when it does a syscall like <code class="language-plaintext highlighter-rouge">write</code> it bubbles up to a VMExit, the host VMM sees that it did a syscall with <code class="language-plaintext highlighter-rouge">rax=1</code> and all the other GPRs for arguments, and then do its own <code class="language-plaintext highlighter-rouge">write</code> syscall with the real FD and arguments on the host.</p>

<p>This doesn’t automatically lets us solve the issue of too many context switches for the <code class="language-plaintext highlighter-rouge">INT3</code> hooks. But because the guest kernel is a full kernel, it can configure the interrupt handle that <code class="language-plaintext highlighter-rouge">INT3</code> context switches to however it wants - and I was able to fork <code class="language-plaintext highlighter-rouge">tinykvm</code> into something I call <a href="https://github.com/chc4/tinycov">TinyCOV</a>, which hacks up the guest kernel a bit. x86 actually has the privilege level for interrupts configured as part of the IDT data structure describing the interrupt gate! That means that we can have <code class="language-plaintext highlighter-rouge">INT3</code> trap from the guest userspace code to the guest kernel interrupt handler but <em>stay</em> in ring 3, and even put our coverage hook implementation directly in the guest “kernel” code to be executed before returning from the interrupt back to continue execution. This cuts down each <code class="language-plaintext highlighter-rouge">INT3</code> coverage hook down to only two context switches, to and from the guest interrupt routine, and zero privilege level changes. And it means we never even need to VMExit either, because the guest kernel is able to handle the interrupt entirely itself instead of needing the VMM to emulate some privileged operation.</p>

<p>Because we overwrite the branch itself with an <code class="language-plaintext highlighter-rouge">INT3</code>, just returning from the interrupt back to where we trapped doesn’t actually work and we’d end up skipping the actual branch operation. Since <code class="language-plaintext highlighter-rouge">INT3</code> is only one byte but the minimum length of a branch is two however, we can use the second byte to record some metadata for the branch hook: we can store an index there, and then use the index plus the offset of the <code class="language-plaintext highlighter-rouge">INT3</code> into the page to uniquely identify the location, and then JIT compile the same branch instruction that we overwrote (but with some jump instructions for either side of the branch back to the original successor instructions). Handling dynamic jumps, like <code class="language-plaintext highlighter-rouge">JNZ rax</code> is a bit trickier, but still possible.</p>

<p>Since we still have the ability to VMExit from the kernel back to the VMM if we want, we also are able to leverage that in order to implement code discovery: <code class="language-plaintext highlighter-rouge">tinycov</code> hooks branch instructions a block at a time, starting from the binary entrypoint. It writes a sentinel value as the metadata byte for the <code class="language-plaintext highlighter-rouge">INT3</code> the first time it hooks a branch, and then in the guest kernel traps to the hypervisor if it sees the coverage hook still has the sentinel value and thus its the first time it’s ran. The VMM is then able to disassemble the successors of the basic block and hook <em>their</em> branches, pushing the code discovery frontier forward, and replace the sentinel value with the correct trampoline index value for the JIT code so that future hits of the branch can stay entirely in the guest kernel.</p>

<h2 id="results">Results</h2>

<p>Despite how scuffed all of this sounds, it does actually work: I’m able to run random binaries under <code class="language-plaintext highlighter-rouge">tinycov</code> and collect a coverage bitmap of code that was hit, plus a mode to instead generate a full program trace. Because it works by hijacking control flow <em>entirely</em>, we can even put arbitrary code in the <code class="language-plaintext highlighter-rouge">INT3</code> kernel handler: I was able to implement <a href="https://github.com/googleprojectzero/CompareCoverage">CmpCOV</a> style dictionary building by recording if a branch ends in a <code class="language-plaintext highlighter-rouge">cmp</code> instruction and recording the register operand value, since the kernel interrupt handler (and the VMM VMExit handler) have access to the full set of GPRs from the program under test synchronously, which you can’t quite do with Intel PT style asynchronous coverage traces.</p>

<p>That said, how fast is <code class="language-plaintext highlighter-rouge">tinycov</code> compared to QEMU? The answer is, uh, really slow. I ran some benchmarks, and it’s like 700% slower - and worse on branch heavy code like <code class="language-plaintext highlighter-rouge">bzip4</code>, which will be taking more <code class="language-plaintext highlighter-rouge">INT3</code> handlers per instruction ran. CPU out-of-order pipelines <em>really</em> don’t like taking an interrupt every few instructions, even if the context switch stays at the same privilege level. I am happy that the idea of using userspace interrupt handlers under <code class="language-plaintext highlighter-rouge">kvm</code> works; I did some benchmarks and it’s like 50% faster than using “proper” ring 3 -&gt; ring 0 interrupts instead! But even 50% faster than doing it the slower way is not enough for this to really be usable when QEMU is so much faster.</p>

<p>Technically, there are some neat things that this would enable you to do which you can’t with QEMU or Intel PT based coverage. For one, because we’re trapping to a single coverage hook which is actually implemented in native code, it’s really easy to add custom coverage hooks: for some fuzzing targets you want to introduce some extra state into the coverage bitmap calculation in order to get the fuzzer to treat identical control flow traces as new program states to continue exploring from, like if you’re fuzzing a language runtime with a virtual PC or some other data-driven loop. If you want to do this with QEMU you have to either write your own TCG hook (hard), or set code callbacks which end up taking a software-VMExit from the JIT code (slow).</p>

<p>Because we’re hooking code in place we also have the ability to <em>unhook</em> code, which I think you could do something cool with. With QEMU the codegen will emit direct branches between JIT blocks to be fast, but the instrumentation being emitted in TCG and thus part of the translated block instructions means that you can’t <em>remove</em> the hook in a block afterwards as you’ll still have those incoming edges. With <code class="language-plaintext highlighter-rouge">tinycov</code> we could remove the coverage hook from branches which we decided “aren’t worth it”, or that we have seen both the <code class="language-plaintext highlighter-rouge">true</code> and <code class="language-plaintext highlighter-rouge">false</code> exits for and so know that it can’t ever lead to new code - meaning that those branches can be rewritten back to their original bytes, and so execute at literally native speed again.</p>

<p>In practice, I think this is mostly just a toy and cute idea, although I am pleased that it works at all. I’ll probably try plugging it in to be used as the coverage collection implementation for <a href="https://dl.acm.org/doi/abs/10.1145/3548606.3560602">LibAFL</a> just to see how much worse it does than QEMU as part of a full fuzzer, but unfortunately no one should really try to use this in production. It was pretty fun to get working though, and I was pleasantly surprised how easy it was to hack up <code class="language-plaintext highlighter-rouge">tinykvm</code> to support the functionality I wanted. Any excuse to do very cursed x86 kernel dev things in a side-project, and staying small enough to actually finish, is a win in my books.</p>

<hr />
<p>Footnote:</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fuzzing">
      <p>Some people would tell you there was a third option, which was “formally prove your program”. They’re lying to you. <a href="#fnref:fuzzing" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ngram">
      <p>In reality it’s a bit more complicated; most fuzzers now actually use N-gram coverage, where you combine the last N branches together to calculate the bitmap entry to set in order to expose more path-dependent information. Having some weird coverage metric is still, to a first approximation, recording “what code was hit” though, so the distinction doesn’t matter that much - and if you have some way of recording branch history, you can probably also compute N-gram coverage instead. <a href="#fnref:ngram" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:hvci">
      <p>In fact, if you’re using Windows 11, the “bare metal” Windows kernel you think you’re running has actually been running as a virtualized guest <a href="https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-hvci-enablement">this entire time</a>. Isn’t that kind of neat? <a href="#fnref:hvci" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:varnish">
      <p>Yes, that Varnish. The HTTP cache. <a href="#fnref:varnish" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Charlie</name></author><summary type="html"><![CDATA[Collecting code coverage for blackbox binaries the wrong way.]]></summary></entry><entry><title type="html">Lunacy</title><link href="https://redvice.org/2026/lunacy/" rel="alternate" type="text/html" title="Lunacy" /><published>2026-05-28T00:00:00+00:00</published><updated>2026-05-28T00:00:00+00:00</updated><id>https://redvice.org/2026/lunacy</id><content type="html" xml:base="https://redvice.org/2026/lunacy/"><![CDATA[<h1 id="the-virtue-of-laziness">The virtue of laziness</h1>

<p><a href="https://github.com/chc4/lunacy">Lunacy</a> is a Lua 5.1 interpreter I’ve been working on as a side-project in Rust. Unlike the average Lua interpreter side-project, Lunacy does type specialization of bytecode operations via <a href="https://arxiv.org/abs/1411.0352">Lazy Basic Block Versioning</a> (LBBV), plus includes a Just-in-Time compiler.</p>

<p>LBBV is a pretty cool compilation technique that came out of Maxime Chevalier-Boisvert’s research while at University of Montreal, culminating in her <a href="https://pointersgonewild.com/2022/05/23/minimalism-in-programming-language-design/phdthesis.pdf">PhD thesis</a>, around developing a JavaScript JIT, Higgs. Critically for me, her approach focuses a lot on simplicity: the goal of LBBV is that it is an effective compilation strategy that can be implemented by just one PhD student in a reasonable amount of time, or one engineer as a side-project.</p>

<p>The original LBBV paper is very approachable, and I highly recommend reading it. To summarize, it enables optimizing bytecode operations, such as <code class="language-plaintext highlighter-rouge">ADD</code>, which naively would need to branch on the types of its operands to figure out if it’s adding two numbers or two strings or what have you. Whenever the operation would inspect the type of an operand and check if it matches a type, it can statically be given a pass/fail based on a <em>specialization context</em> for the basic block. If the context doesn’t <em>know</em> the type of the operand, instead of needing to conservatively fall back to a dynamic operation which can handle any value, compilation can instead be suspended to a thunk: the thunk will eventually be forced by execution hitting it at runtime, at which point you can continue compiling with the context now filled with the <em>runtime</em> type of the value promoted to a static type, guarded by a runtime check to validate that later executions of the same codepath use the same type. In the failure case of the guard, you can suspend another thunk that will specialize the operation for the next runtime type you see.</p>

<p>This is very powerful! The end result is that you have blocks which seemingly by magic have type guards only for the types of values that actually were observed at runtime, and for things like loops “for free” unroll one iteration which contains all the type guards, and then have entirely known types inside the hot loop (because that block reached a fixpoint of the specialization context for the loop header, and so reuses the block to form a loop). Because you have the powerful primitive of blocks specialized for a static context based off observed runtime information, you can stuff whatever information in the static context you want and always have a correct “guess” for the value of speculative information. And if you ever end up with too many blocks, no matter what reason, you can fallback to running generic code instead of having exponential block blowup.</p>

<p><img src="/assets/images/lunacy-example-1.jpg" alt="Lunacy IR for sum" />
<img src="/assets/images/lbbv-example-1.png" alt="LBBV paper IR for sum" /></p>

<blockquote>
  <p>Comparison of Lunacy and Higgs specialized blocks for <code class="language-plaintext highlighter-rouge">sum</code>. Notice how the inner loop body, block <code class="language-plaintext highlighter-rouge">7</code>, ends up with fully specialized <code class="language-plaintext highlighter-rouge">ADD</code> operations for the accumulator and loop induction variable. Lunacy doesn’t do small-integer optimization and so has no overflow checks.</p>
</blockquote>

<p>Lunacy does a few things different from Higgs. For one, Higgs is a <em>pure</em> JIT: it has no interpreter, and immediately compiles all bytecode operations directly to assembly. Lunacy instead is implemented as an interpreter first and a JIT second: bytecode operations are implemented as Rust coroutines, which yield effects. Those effects may be things like “guard on stack slot X being type Y”, which are resumed with results like “the slot matches/fails the guard” - and like LBBV, if the type isn’t currently known, we instead suspend the coroutine inside a thunk via a closure. It may also yield an effect like “add operands A and B together”, which can be implemented so it only cares about how to add numbers since the coroutine would only reach the yield if the required guards matched.</p>

<p>Runtime behavior is emitted as separate residual operations, separate from the actual Lua bytecode operations which are implemented as coroutines that are driven at compile time. In order to avoid having to define and implement runtime behavior for every combination of operand type etc., Lunacy also uses a <a href="https://www.iro.umontreal.ca/~feeley/papers/FeeleyLapalmeCL87.pdf">closure generating</a> interpreter: complex runtime behavior, like adding two numbers, is instead implemented by a closure that captures the bytecode operands such as the stack slot indexes it uses, and at runtime we execute the closure in order to perform the operation. The interpreter repeatedly executes the residual operations that were the result of compiling blocks by running all of the coroutines for the Lua bytecode.</p>

<p>This approach means that the bytecode coroutines in Lunacy were very easy to implement. All of the behavior for an operation, including the different <code class="language-plaintext highlighter-rouge">Residual::Exec</code> variants, are located in a single coroutine which intermixes the static compilation portion via yielded effects and the runtime execution portion via closures which can capture arbitrary static information. Because Rust coroutines even implement <code class="language-plaintext highlighter-rouge">Clone</code>, when we suspend a coroutine to a thunk in order to discover extra information from the runtime behavior we can even push a clone of the same coroutine suspended at the same yieldpoint down into the failure case - essentially forking it, and each fork independently being resumed to compile the remainder of the operation and block with a different observed type. The interpreter itself is very small and simple, because it only has a small set of simple residual operations to handle.</p>

<p>This residual scheme also made implementing the JIT side of the interpreter very easy as well, which was a large portion of why I chose to use closure generation as well: because our set of residual operations is very small, only things like type guards or executing a closure or executing a thunk (itself also a closure), for the JIT we only need to handle how to compile those same small set of residual operations. Instead of needing to maintain a second assembly implementation for all bytecode operations, we can emit a static call to the closure function pointer address in the JIT, which crucially eliminates dynamic dispatch and other things that make modern OoO processors sad. Meanwhile, the remainder of the residual operations such as runtime typeguards we can implement in assembly in ~4 instructions as a native branch, which also can get their own branch predictor entry. The actual JIT is tiny, and mostly consists of some <a href="https://github.com/CensoredUsername/dynasm-rs">DynASM-rs</a> assembly in a <code class="language-plaintext highlighter-rouge">match</code>.</p>

<p>Because Lunacy is a template JIT where most of our operations are function calls, it doesn’t do any register allocation. It just pins the interpreter state to a register in order to move into the function argument ABI for the closure call repeatedly. However, because the closure call ABI has a number of preserved registers, it does leave open opportunities to do <a href="https://arxiv.org/abs/2011.13127">Copy-and-Patch</a> style register windowing using those preserved registers in the future and different residual operations, or use multiple different copies of closures which expect arguments in different registers we can select from during JIT emission.</p>

<p>Unlike Higgs, which immediately JITs all code, we only trigger the JIT after a block hits a hotness threshold because it’s been executed enough times. This means that we will have forced all of the thunks on executed codepaths, unlike Higgs: Lunacy currently doesn’t invalidate or modify JIT code at all, which to me is much easier to reason about, because of this. Because the interpreter and JIT are both running the same residual operations for blocks, bailing out of the JIT back to the interpreter is very easy: the PC for the JIT bailout is the same PC that the interpreter should continue execution at. Thunks which weren’t yet forced when we trigger JIT compilation are handled as bailouts back to the interpreter, which will run the thunk as normal and rewrite it to a jump residual to a new block: if the new block is ran enough jumping to it may end up triggering JIT compilation again. In the future if the JIT-&gt;interpreter-&gt;JIT transition for thunks ends up being a bottleneck we could fixup the bailout to jump to the newly JIT’d block as well.</p>

<p><img src="/assets/images/example1-gdb.png" alt="Lunacy JIT code for sum" /></p>
<blockquote>
  <p>JIT code for <code class="language-plaintext highlighter-rouge">sum</code>. Each residual exec operation turned into a native <code class="language-plaintext highlighter-rouge">CALL</code> to a static address, with a static closure object pointer. Despite this looking pretty bad for a JIT, it’s still several times faster than the interpreter!</p>
</blockquote>

<h1 id="more-context">More context</h1>

<p>Lunacy doesn’t only specialize the types of stack slots in the specialization context, but also will 1) track the concrete function value for function values 2) specialize table keys to static types and constant offsets.</p>

<p>The first one is fairly basic: just like we can suspend execution in order to discover the runtime type of a value, we can also suspend execution to discover the function pointer of things that we know are functions, again emitting another guard on the pointer value matching our expection. We can do this for both native functions, such as <code class="language-plaintext highlighter-rouge">print</code>, as well as Lua functions: this not only lets us in the JIT emit a native call to the C function directly, but also avoid needing to dynamically check the value and switch behavior depending on if its a native call or not, and lets us emit nested calls to more JIT code using the native processor stack instead of the interpreter call stack if the Lua call target is also already JIT’d. Because we can use all of the existing LBBV infrastructure in order to promote dynamic values to static context, this was surprisingly easy to implement.</p>

<p>For two, it was slightly more difficult. V8 implements table specialization in the form of hidden classes with shapes and transition maps: tables are assigned a pointer to a shape, which describes the layout and type of the table, and then at runtime you can emit a shape guard <code class="language-plaintext highlighter-rouge">if table.shape != expected: fail; success</code> in order to then emit loads from the table, which result in typed values, at the offset described by the shape in order to avoid hashmap lookups in the JIT. Tables are assigned shapes incrementally based on the order they are built. Code such as</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>local t = {}
t.a = 1
t.b = 2
</code></pre></div></div>

<p>would repeatedly transition the shape of the table according to a series of maps saying the next shape for each key that was added, such as</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{} -&gt; {a: int} -&gt; {a: int, b: int}
</code></pre></div></div>

<p>I didn’t want to implement this in Lunacy, however. Because shapes describe the entire layout of a table, it means that any inheritance-like behavior must be compiled as polymorphic blocks: <code class="language-plaintext highlighter-rouge">Dog</code> and <code class="language-plaintext highlighter-rouge">Cat</code> have different shape pointers, and you could only insert a guard that they are both <code class="language-plaintext highlighter-rouge">Animal</code>s by implementing more logic that finds the common ancestor and walks shape pointers at runtime, and figuring out when to do that as part of LBBV where all compilation has to progress incrementally (unlike a method or tracing JIT, which looks at multiple blocks at a time upfront) appeared hazardous. Shapes also requires following the transition maps at runtime if you have dynamic keys, and V8 also implements things like shape invalidation which requires being able to migrate JIT stack frames back to the interpreter (which itself necessitate emitting stack maps) to handle some cases.</p>

<p>Because Lua function environments, which contain non-local variables, are themselves tables I would also have the issue of shape pointers transitioning even from unrelated keys potentially invalidating the shape guards for every function just because a new global was added late, requiring a separate scheme to also be added to handle environments.</p>

<p>Instead, Lunacy implements something closer to LuaJIT’s <a href="http://lua-users.org/lists/lua-l/2009-11/msg00089.html">hash slot specialization</a> where the static context only contains the individual keys that are accessed in the block. A function which only accesses properties for an <code class="language-plaintext highlighter-rouge">Animal</code> would specialize on the hash slots for those properties being the first discovered type and offset, and for a <code class="language-plaintext highlighter-rouge">Dog</code> and <code class="language-plaintext highlighter-rouge">Cat</code> which were constructed in the same order for those shared prefix of keys be able to use the same specialized blocks. Likewise, adding a new variable to an environment wouldn’t cause existing guards to fail because the type and offset for all the previous keys stay intact.</p>

<p><img src="/assets/images/lunacy-example-2.jpg" alt="Lunacy IR showing gettable" /></p>

<blockquote>
  <p>LBBV IR for <code class="language-plaintext highlighter-rouge">local t = {} t.a = 1 print(t.a)</code>. <code class="language-plaintext highlighter-rouge">gettable_href</code> does <code class="language-plaintext highlighter-rouge">O(1)</code> access with the hash witness index that was initialized by <code class="language-plaintext highlighter-rouge">href_init</code>, and the stack slot containing <code class="language-plaintext highlighter-rouge">print</code> was specialized to the concrete function pointer.</p>
</blockquote>

<p>I did slightly tweak the LuaJIT scheme, adding a runtime “hash witness” table that is populated with the dynamic index of the key instead of including a constant index based on the first observed key index; the idea was that <code class="language-plaintext highlighter-rouge">animal.age</code> might be at different indexes even for subclasses because the hashmap storage resized based on different number of keys, and so initializing the offset the first time you see the key in each function would maintain the desired behavior. The hash witness table also includes a “table epoch”: like the shape transitions, when Lunacy sets the key on a table and can’t prove via static information that the value stays the same type, it increments a monotonic epoch counter, and the initial value of that epoch counter is saved when we initialize the hash witness entry. The epoch is needed in order to catch invalidation of table keys, where <code class="language-plaintext highlighter-rouge">t.a = 1; t[dynamic] = "string"; t.a + 1</code> can’t assume that <code class="language-plaintext highlighter-rouge">t.a</code> stayed a string, and we must emit a guard that <code class="language-plaintext highlighter-rouge">if t.epoch != witness[0].epoch: bail; success</code> to verify the table stays the same. If the epoch increments, because the table had a new key added, we can do an “epoch repair” operation which do a full check that the value is still the expected type and either update the witness epoch value to the new table epoch now that we’re sure it’s still safe, or fallback to compiling a new block with the newly observed type.</p>

<p>But Lunacy now instead just uses <code class="language-plaintext highlighter-rouge">IndexMap</code> to implement the hash part of Lua tables, which have stable indexes based off insertion order, and removes the issue of indexes potentially being unstable based off resizing; I will probably remove the index from the witness and emit the constant index in the JIT directly, which will remove one data-dependent load from table property accesses while only introducing a few redundant blocks. The epoch scheme may also not be needed once I move to a better value representation (<a href="https://github.com/WebKit/WebKit/blob/4335716855bb893798267c14053068e6c8ad010e/Source/JavaScriptCore/runtime/JSCJSValue.h#L390">NuN boxing</a>) which would be able to check the types of values in a single instruction, and so be as simple as our epoch guard is.</p>

<p><img src="/assets/images/lunacy-example-3.png" alt="Lunacy IR for say" /></p>

<blockquote>
  <p>JIT IR for: <code class="language-plaintext highlighter-rouge">local function say(animal) print(animal.name .. " is ", animal.age, "years old") if animal.barks ~= nil then print("bowwow") end end</code>. Despite calling it with <code class="language-plaintext highlighter-rouge">local dog = {name = "Fido", age = 5, barks = "a lot"}</code> and <code class="language-plaintext highlighter-rouge">local cat = {name = "Mittens", age = 3, meows = "sometimes"}</code>, the first <code class="language-plaintext highlighter-rouge">print</code> is monomorphic instead of needing to specialize per-shape.</p>
</blockquote>

<p>The other wrinkle with doing table shape optimization is around aliasing. If you have <code class="language-plaintext highlighter-rouge">x.a = 1; y.a = "string"; x.a + 1</code>, even though you never saw a store invalidate <code class="language-plaintext highlighter-rouge">x.a</code> it may now have the wrong type if <code class="language-plaintext highlighter-rouge">x == y</code>. The initial thing Lunacy was doing was just emit the epoch guard on every table access to catch these types of aliasing hazards; I eventually discovered the PyPy does a “type-based aliasing optimization”, where you can infer that <code class="language-plaintext highlighter-rouge">y.b = "string"</code> can’t invalidate <code class="language-plaintext highlighter-rouge">x.a</code>, because they’re different keys or different types - V8 is likewise able to do the same thing based on two variables having different know shapes I believe. With this we’re able to elide a lot of redundant epoch guards. Next up I want to try implementing caching of table property values in registers in the JIT, in order to remove redundant loads and perform store-to-load forwarding. Ideally this would allow code like <code class="language-plaintext highlighter-rouge">t.a = t.a + 1</code> to stay entirely in a register without needing to touch memory inside a hot loop. This likewise has aliasing hazards like we had above for type stability, where <code class="language-plaintext highlighter-rouge">print(other.a)</code> potentially may observe the delayed store to <code class="language-plaintext highlighter-rouge">t.a</code> if <code class="language-plaintext highlighter-rouge">other == t</code>: I need to mess around to see how actually feasible this is, and how often these kinds of aliasing hazards would negatively affect benchmarks.</p>

<h1 id="performance">Performance</h1>

<p>It’s not great?</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Summary
  ./target/unsafe/bench nbody.bin 10 ran
    1.01 ± 0.02 times faster than lua5.1 bench.lua -- lua_benchmarking/benchmarks/nbody/bench 10
    1.11 ± 0.02 times faster than ./target/release/bench nbody.bin 10
    2.77 ± 0.04 times faster than ./target/interpreter/release/bench nbody.bin 10
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Summary
  lua5.1 bench.lua -- lua_benchmarking/benchmarks/life/bench 1000 ran
    1.66 ± 0.07 times faster than ./target/unsafe/bench life.bin 1000
    2.03 ± 0.06 times faster than ./target/release/bench life.bin 1000
    2.62 ± 0.05 times faster than ./target/interpreter/release/bench life.bin 1000
</code></pre></div></div>

<p>Currently, Lunacy runs about the same speed as PUC Lua 5.1 on <code class="language-plaintext highlighter-rouge">nbody</code>. nbody is heavy on table property accesses, and so we sure would hope we do better than PUC Lua, which will do actually-for-real hashmap lookups each time. Other benchmarks are a bit slower. This is very modest performance - but it is in fact occasionally faster than vanilla Lua, which was my initial end goal and I consider an achievement.</p>

<p>More notably, Lunacy’s interpreter-only mode is about 2.75x slower than Lua, which means the JIT does actually work in being substantially faster than the interpreter, which was my second big goal!</p>

<p>Why is it slower? Profiling is difficult, but it appears to be a mix of:</p>

<ul>
  <li>
    <p>Our value representation is bad, and is literally just a Rust enum. Lunacy doesn’t do any pointer tagging, NaN boxing, or (the goal) NuN boxing currently - so our values are 2x more bytes than they ideally would be, which makes everything slower and precludes trying to keep them in registers as each value would consume two of them.</p>
  </li>
  <li>
    <p>The LBBV specialization is kinda slow (~20% of runtime), in particular because the hash slot specialization creates a lot of thunks despite many of them being immediately forced. It is probably possible to avoid repeatedly switching back and forth between running and compiling for some of those thunks.</p>
  </li>
  <li>
    <p>Likewise, the LBBV specialization clones a bunch of contexts and coroutines everywhere, and uses a lot of HashMaps. These can probably be optimized a lot as well to bring down the 20% overhead number.</p>
  </li>
  <li>
    <p>The table hash slot specialization does a full hashmap lookup to initialize the hash witness index, and needs to load the index from the hash witness for accesses.</p>
  </li>
  <li>
    <p>Lunacy doesn’t implement the function entrypoint specialization described in the sequel paper to LBBV, which means each function has no known types at its entrypoint again - and so has to re-initialize hash witnesses with the slow hashmap lookup. Oops! Performing entrypoint specialization with the continuation specialization for return values as well requires being able to invalidate continuations in the JIT, which is unfortunately kind of complicated.</p>
  </li>
  <li>
    <p>Some of the residual operations which aren’t easy to implement in assembly (because they e.g. call Rust methods) are implemented by emitting calls to <code class="language-plaintext highlighter-rouge">JitHelper</code> functions. These calls require shuffling values into ABI registers and can’t bake constants into the assembly, instead needing them to be passed as arguments. This isn’t actually a big percentage of wallclock time but it still feels bad.</p>
  </li>
</ul>

<p>And, of course, the big caveat of I’m mostly doing this as a fun side-project, and so haven’t been focusing on actual Lua feature compatibility in favor of doing neat JIT things. Notably, Lunacy doesn’t actually implement metatables yet, although I will need to implement them soon in order to run more benchmarks. Likewise it’s missing a couple bytecode operations, and there’s a decent amount of codepaths in existing bytecode operations that are <code class="language-plaintext highlighter-rouge">unimplemented!()</code>. I only recently bothered to even implement a basic mark-and-sweep garbage collector, and was relying on <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> for garbage collection up until then.</p>

<p>Overall however, I’m very happy with the current progress on Lunacy. I’ve wanted to write a “real” JIT compiler for an interpreter for a while now, so it’s great to finally get around to it and reach the point I can get some ok-ish times on benchmarks.</p>]]></content><author><name>Charlie</name></author><summary type="html"><![CDATA[A Lua 5.1 interpreter + JIT in Rust]]></summary></entry><entry><title type="html">The Social Costs of Dropping Out</title><link href="https://redvice.org/2025/college-socialization/" rel="alternate" type="text/html" title="The Social Costs of Dropping Out" /><published>2025-04-11T00:00:00+00:00</published><updated>2025-04-11T00:00:00+00:00</updated><id>https://redvice.org/2025/college-socialization</id><content type="html" xml:base="https://redvice.org/2025/college-socialization/"><![CDATA[<p>Dropping out was one of the best decisions I ever made. I’d mulled over doing so since high school, as I’d long been dissatisfied with the standard educational experience—a high school friend I caught up with years later remarked “I remember you talking about dropping out, but I can’t believe you actually did it”—but I didn’t pull the trigger until 2 years into college, 5-6 years after first considering it. I have generally poor experiential memory (probably as a consequence of my aphantasia), but the day I dropped out is one of my most vivid memories. After finally mustering up the resolve to commit to dropping out, I flopped on my bed and sobbed for an hour, not out of sadness but due to a feeling a sense of intense relief that I have not and probably never will experience again. I look back at that moment fondly; the instant I made the choice, I knew it was the right one. It has certainly worked out fine on every measure.</p>

<p>I share that story to make clear: this is a post about a cost of dropping out, but not regretful post, or a post discouraging the practice at all.</p>

<p>However, I believe it’s important to understand the tradeoffs you’re making with major life decisions. I certainly didn’t at the time, and from talking to other dropouts or people considering it, most of the tradeoff considerations center around professional life and their career progression. Those are not the only things college offers.</p>

<p>For most people, college is the foundational building block in their social circles for years to come. This seems to hold even for people who don’t really try at making friends, which is somewhat surprising to me, as is how enduring those social circles tend to be.</p>

<p>For most kids, college is their first opportunity for meaningful freedom from the yoke of their parents. I distinguish this from actually busy adults in the developmental sense of the word, as the modern college experience is akin to daycare and has few axes of success<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. Famously, many Asian teens will briefly become party animals, then settle down again after getting that out of their system (or not), but the effect exists more broadly. This intense socialization and increased freedom happens among a peer group that is more similar to you, and with more common interests, than any you’ve had before. With that framing, it’s not so surprising that by default even relatively antisocial people will find a friend group. The rotating groups in classes each semester means a huge amount of forced comingling between people who are likely to get along.</p>

<p>For many, this is also when they can develop a more serious long-term relationship, and get physical with it. The meme of college students fucking like rabbits in the dorms freshman year is, as far as I can tell, basically true. The freedom combined with increased opportunity for a relationship, on top of continued teens/early 20s hormones, also helps force people to become better socially adjusted and more apt at interacting with the other sex. This effect is particularly pronounced for guys. Many people stop wearing ill-fitting clothes, kick a nervous tic or two, and learn to flirt<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>.</p>

<p>Even after graduation, people often end up in similar cities to their existing friend group. This becomes their initial, primary social circle, and they discover the horrible truth: that the college social experience is basically unique, and you will never again be constantly exposed to different people who share your age and interests by default. And the people you now spend the most time around, your coworkers, are discouraged as objects of romantic interest<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p>

<p>When you drop out, you give this all up. And often the process leading up to dropping out consists of making this tradeoff to a lesser extent. I spent much of my high school and limited college years programming, and while that focus was great for skill acquisition, it definitely precluded the standard high school/college social experience. I’m not convinced this tradeoff is inherent, but it seems to be incredibly common. I suspect I would have tried to navigate it differently if I’d understood the full implications, but it’s hard to say in hindsight.</p>

<p>None of this is to say you can’t be social, make new friends, and find fantastic partners after college! It just doesn’t happen by default, especially for introverts, and requires a level of agency to make happen that is rarer than you’d think. Many people coast off their college circles for years, and look back on those as the best experiences of their life.</p>

<p>When you drop out, you suddenly have no choice: you must take responsibility for this aspect of your life. This is tough. It takes time, energy, and learning from failure, and you may not be comfortable pursuing it until you’re settled career-wise; I certainly didn’t fix this for a while. It’s also particular tough immediately after dropping out, as people your age are mostly still in school and their leisure time is dominated by school-centric social events. Even after they graduate, you end up at pretty different levels of life experience and often pretty different values, which makes dating weird. This goes away after a few years, and I’d generally encourage people to push past it, but you will probably end up with an older peer group and a few years of awkward dates.</p>

<p>For most of my friends, this all more or less worked itself out, so if you’re considering dropping out it’s not all gloom and doom. And the benefits of dropping out can be massive - it’s a variance-increasing decision, for good or for ill. But this is an important aspect to be aware of, and if you do drop out, to try and address as soon as you have some breathing room to do so.</p>

<p>If you’re thinking of dropping out, or recently pulled the plug and you want to chat, shoot me an email. I have limited generic advice to offer, but I’m usually happy to make time to sit down and talk things over.</p>

<hr />

<p><em>Thanks to Ryan Hill for reading a draft of this post.</em></p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>1: <a href="https://kipp.ly/no-not-good-enough/">Relevant post</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>2: I am told, amusingly, that most of them do not bother getting good at sex. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>3: Albeit unfairly; if you’re a white-collar worker in the US the downside is not that bad, especially if you work in tech, and the upside potentially huge. For your employer, it’s probably mostly downside, but you shouldn’t conflate their interests with yours. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Ryan</name></author><summary type="html"><![CDATA[Observations on an underconsidered aspect of the tradeoffs around dropping out: college socialization]]></summary></entry><entry><title type="html">Hacking My Coworker (In Minecraft)</title><link href="https://redvice.org/2025/in-minecraft/" rel="alternate" type="text/html" title="Hacking My Coworker (In Minecraft)" /><published>2025-04-01T00:00:00+00:00</published><updated>2025-04-01T00:00:00+00:00</updated><id>https://redvice.org/2025/in-minecraft</id><content type="html" xml:base="https://redvice.org/2025/in-minecraft/"><![CDATA[<object data="/assets/pdfs/minecraft2025.pdf" width="1000" height="1000" type="application/pdf"></object>

<p>This paper was submitted to <a href="https://sigbovik.org/2025/">SIGBOVIK 2025</a>.</p>]]></content><author><name>Charlie</name></author><summary type="html"><![CDATA[Finding a zero-day RCE vulnerability (in Minecraft)]]></summary></entry><entry><title type="html">RCE in Minecraft mod Integrated Scripting</title><link href="https://redvice.org/2025/integrated-scripting-rce/" rel="alternate" type="text/html" title="RCE in Minecraft mod Integrated Scripting" /><published>2025-03-10T00:00:00+00:00</published><updated>2025-03-10T00:00:00+00:00</updated><id>https://redvice.org/2025/integrated-scripting-rce</id><content type="html" xml:base="https://redvice.org/2025/integrated-scripting-rce/"><![CDATA[<h1 id="tldr-update-your-server-if-you-have-integrated-scripting-installed">TL;DR: UPDATE YOUR SERVER IF YOU HAVE INTEGRATED SCRIPTING INSTALLED</h1>

<p>I found a remote code execution vulnerability in the Minecraft mod <em>Integrated Scripting</em>. If you are running Integrated Scripting, and the version is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;=1.21.1-1.0.16
&lt;=1.21.4-1.0.9-224
&lt;=1.20.1-1.0.11
&lt;=1.19.2.-1.0.9
</code></pre></div></div>

<p>your server is vulnerable to being hacked by any player who is able to craft some items from Integrated Scripting. If you have a public or semi-public server, you should update the mod to a newer version.</p>

<p>Several popular modpacks contain Integrated Scripting. If you are running:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AllTheMods10 &lt;=2.36
Craftoria &lt;=1.14.0
FTB Presents Direwolf20 1.21 &lt;=1.9.0
(and many more)
</code></pre></div></div>

<p>your server is also vulnerable, and you should update.</p>

<h1 id="technical-details">Technical details</h1>
<p>Versions of Integrated Scripting released before February 24th, 2025 are vulnerable to a critical remote code execution vulnerability. Leveraging Java reflection it’s possible to escape the GraalJS sandbox and call arbitrary methods on arbitrary Java objects. The initial vulnerability disclosure, with more details, is available as <a href="https://github.com/CyclopsMC/IntegratedScripting/security/advisories/GHSA-2v5x-4823-hq77">GHSA-2v5x-4823-hq77</a>. The vulnerability has also been assigned <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-27107">CVE-2025-27107</a>.</p>]]></content><author><name>Charlie</name></author><summary type="html"><![CDATA[Remote code execution on modded Minecraft servers]]></summary></entry><entry><title type="html">Video Encoding Requires Using Your Eyes</title><link href="https://redvice.org/2025/encoding-requires-eyes/" rel="alternate" type="text/html" title="Video Encoding Requires Using Your Eyes" /><published>2025-02-27T00:00:00+00:00</published><updated>2025-02-27T00:00:00+00:00</updated><id>https://redvice.org/2025/encoding-requires-eyes</id><content type="html" xml:base="https://redvice.org/2025/encoding-requires-eyes/"><![CDATA[<p>In multimedia, the quality engineers are optimizing for is <em>perceptual</em>. Eyes, ears, and the brain processing their signals are enormously complex, and there’s no way to replicate everything computationally. There are no “objective” metrics to be had, just various proxies with difficult tradeoffs. Modifying video is particularly thorny, since like I’ve mentioned before on this blog there are various ways to subtly bias perception that are nonetheless undesirable, and are impossible to correct for.</p>

<p>This means there’s no substitute for actually looking at the results. If you are a video engineer, you <strong>must</strong> look at sample output and ask yourself if you like what you see. You should do this regularly, but especially if you’re considering changing anything, and even more so if ML is anywhere in your pipeline. You cannot simply point at metrics and say “LGTM”! In this particular domain, if the metrics and skilled human judgement are in conflict, the metrics are usually wrong.</p>

<p>Netflix wrote a <a href="https://netflixtechblog.com/for-your-eyes-only-improving-netflix-video-quality-with-neural-networks-5b8d032da09c">post</a> on their engineering blog about a “deep downscaler” for video, and unfortunately it’s rife with issues. I originally saw the post due to someone citing it, and was incredibly disappointed when I clicked through and read it. Hopefully this post offers a counter to that!</p>

<p>I’ll walk through the details below, but they’re ultimately all irrelevant; the single image comparison Netflix posted looks like this (please ‘right-click -&gt; open image in new tab’ so you can see the full image and avoid any browser resampling):</p>

<p><img src="/assets/images/netflix-downscaler-compare.png" alt="Downscaler comparison" /></p>

<p>Note the ringing, bizarre color shift, and seemingly fake “detail”. If the above image is their best example, this should not have shipped – the results look awful, regardless of the metrics. The blog post not acknowledging this is embarrassing, and it makes me wonder how many engineers read this and decided not to say anything.</p>

<h2 id="the-post">The Post</h2>

<p>Okay, going through this section by section:</p>

<h3 id="how-can-neural-networks-fit-into-netflix-video-encoding">How can neural networks fit into Netflix video encoding?</h3>

<blockquote>
  <p>There are, roughly speaking, two steps to encode a video in our pipeline:</p>
</blockquote>

<blockquote>
  <p>1. Video preprocessing, which encompasses any transformation applied to the high-quality source video prior to encoding. Video downscaling is the most pertinent example herein, which tailors our encoding to screen resolutions of different devices and optimizes picture quality under varying network conditions. With video downscaling, multiple resolutions of a source video are produced. For example, a 4K source video will be downscaled to 1080p, 720p, 540p and so on. This is typically done by a conventional resampling filter, like Lanczos.</p>
</blockquote>

<p>Ignoring the awful writing<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, it’s curious that they don’t clarify what Netflix was using previously. Is Lanczos an example, or the current best option<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>? This matters because one would hope they establish a baseline to later compare the results against, and that baseline should be the best reasonable existing option.</p>

<blockquote>
  <p>2. Video encoding using a conventional video codec, like AV1. Encoding drastically reduces the amount of video data that needs to be streamed to your device, by leveraging spatial and temporal redundancies that exist in a video.</p>
</blockquote>

<p>I once again wonder why they mention AV1, since in this case I know it’s not what the majority of Netflix’s catalog is delivered as; they definitely care about hardware decoder support. Also, this distinction between preprocessing and encoding isn’t nearly as clean as this last sentence implies, since these codecs are lossy, and in a way that is aware of the realities of perceptual quality.</p>

<blockquote>
  <p>We identified that we can leverage neural networks (NN) to improve Netflix video quality, by replacing conventional video downscaling with a neural network-based one. This approach, which we dub “deep downscaler,” has a few key advantages:</p>
</blockquote>

<p>I’m sure that since they’re calling it a deep downscaler, it’s actually going to use deep learning, right?</p>

<blockquote>
  <p>1. A learned approach for downscaling can improve video quality and be tailored to Netflix content.</p>
</blockquote>

<p>Putting aside my dislike of the phrase “a learned approach” here, I’m very skeptical of “tailored to Netflix content” claim. Netflix’s catalog is pretty broad, and video encoding has seen numerous attempts at content-based specialization that turned out to be worse than focusing on improving things generically and adding tuning knobs. The encoder that arguably most punched above its weight class, x264, was mostly developed on Touhou footage.</p>

<blockquote>
  <p>2. It can be integrated as a drop-in solution, i.e., we do not need any other changes on the Netflix encoding side or the client device side. Millions of devices that support Netflix streaming automatically benefit from this solution.</p>
</blockquote>

<p>Take note of this for later: Netflix has <em>many</em> different clients and this assumes no changes to them.</p>

<blockquote>
  <p>3. A distinct, NN-based, video processing block can evolve independently, be used beyond video downscaling and be combined with different codecs.</p>
</blockquote>

<p><img src="/assets/images/doubt-meme.png" alt="Doubt" /></p>

<blockquote>
  <p>Of course, we believe in the transformative potential of NN throughout video applications, beyond video downscaling. While conventional video codecs remain prevalent, NN-based video encoding tools are flourishing and closing the performance gap in terms of compression efficiency. The deep downscaler is our pragmatic approach to improving video quality with neural networks.</p>
</blockquote>

<p>“Closing the performance gap” is a rather optimistic framing of that, but I’ll save this for another post.</p>

<h3 id="our-approach-to-nn-based-video-downscaling">Our approach to NN-based video downscaling</h3>

<blockquote>
  <p>The deep downscaler is a neural network architecture designed to improve the end-to-end video quality by learning a higher-quality video downscaler. It consists of two building blocks, a preprocessing block and a resizing block. The preprocessing block aims to prefilter the video signal prior to the subsequent resizing operation. The resizing block yields the lower-resolution video signal that serves as input to an encoder. We employed an adaptive network design that is applicable to the wide variety of resolutions we use for encoding.</p>
</blockquote>

<p><img src="/assets/images/netflix-downscaler-architecture.png" alt="Downscaler comparison" /></p>

<p>I’m not sure exactly what they mean by the adaptive network design here. A friend has suggested that maybe this just means fixed weights on the preprocessing block? I am, however, extremely skeptical of their claim that the results will generate to a wide variety of resolutions. Avoiding overfitting here would be fairly challenging, and there’s nothing in the post that inspires confidence they managed to overcome those difficulties. They hand-wave this away, but it seems critical to the entire project.</p>

<blockquote>
  <p>During training, our goal is to generate the best downsampled representation such that, after upscaling, the mean squared error is minimized. Since we cannot directly optimize for a conventional video codec, which is non-differentiable, we exclude the effect of lossy compression in the loop. We focus on a robust downscaler that is trained given a conventional upscaler, like bicubic. Our training approach is intuitive and results in a downscaler that is not tied to a specific encoder or encoding implementation. Nevertheless, it requires a thorough evaluation to demonstrate its potential for broad use for Netflix encoding.</p>
</blockquote>

<p>Finally some details! I was curious how they’d solve the lack of a reference when training a downscaling model, and this sort of explains it; they optimized for PSNR when upscaled back to the original resolution, post-downscaling. My immediate thoughts upon reading this:</p>

<ol>
  <li>Hrm, PSNR isn’t great<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</li>
  <li>Which bicubic are we actually talking about? This is not filling me with confidence that the author knows much about video.</li>
  <li>So this is like an autoencoder, but with the decoder replaced with bicubic upscaling?</li>
  <li>Doesn’t that mean the second your TV decides to upscale with bilinear this all falls apart?</li>
  <li>Does Netflix actually reliably control the upscaling method on client devices<sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>? They went out of their way to specify earlier that the project assumed no changes to the clients, after all!</li>
  <li>I wouldn’t call this intuitive. To be honest, it sounds kind of dumb and brittle.</li>
  <li>Not tying this to a particular encoder is sensible, but their differentiability reason makes no sense.</li>
</ol>

<p>The weirdest part here is the problem formulated in this way actually has a closed-form solution, and I bet it’s a lot faster to run than a neural net! ML is potentially good in more ambiguous scenarios, but here you’ve simplified things to the point that you can just do some math and write some code instead<sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>!</p>

<h3 id="improving-netflix-video-quality-with-neural-networks">Improving Netflix video quality with neural networks</h3>

<blockquote>
  <p>The goal of the deep downscaler is to improve the end-to-end video quality for the Netflix member. Through our experimentation, involving objective measurements and subjective visual tests, we found that the deep downscaler improves quality across various conventional video codecs and encoding configurations.</p>
</blockquote>

<p>Judging from the example at the start, the subjective visual tests were conducted by the dumb and blind.</p>

<blockquote>
  <p>For example, for VP9 encoding and assuming a bicubic upscaler, we measured an average VMAF Bjøntegaard-Delta (BD) rate gain of ~5.4% over the traditional Lanczos downscaling. We have also measured a ~4.4% BD rate gain for VMAF-NEG. We showcase an example result from one of our Netflix titles below. The deep downscaler (red points) delivered higher VMAF at similar bitrate or yielded comparable VMAF scores at a lower bitrate.</p>
</blockquote>

<p>Again, what’s the actual upscaling filter being used? And while I’m glad the VMAF is good, the result looks terrible! This means the VMAF is wrong. But also, the whole reason they’re following up with VMAF is because PSNR is not great and everyone knows it; it’s just convenient to calculate. Finally, how does VP9 come into play here? I’m assuming they’re encoding the downscaled video before upscaling, but the details matter a lot.</p>

<blockquote>
  <p>Besides objective measurements, we also conducted human subject studies to validate the visual improvements of the deep downscaler. In our preference-based visual tests, we found that the deep downscaler was preferred by ~77% of test subjects, across a wide range of encoding recipes and upscaling algorithms. Subjects reported a better detail preservation and sharper visual look. A visual example is shown below. [note: example is the one from above]</p>
</blockquote>

<p>And wow, coincidentally, fake detail and oversharpening are common destructive behaviors from ML-based filtering that unsophisticated users will “prefer” despite making the video worse. If this is the bar, just run Warpsharp on everything and call it a day<sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>; I’m confident you’ll get a majority of people to say it looks better.</p>

<p>This example also doesn’t mention what resolution the video was downscaled to, so it’s not clear if this is even representative of actual use-cases. Once again, there are no real details about how the tests with conducted, so I have no way to judge whether the experiment structure made sense.</p>

<blockquote>
  <p>We also performed A/B testing to understand the overall streaming impact of the deep downscaler, and detect any device playback issues. Our A/B tests showed QoE improvements without any adverse streaming impact. This shows the benefit of deploying the deep downscaler for all devices streaming Netflix, without playback risks or quality degradation for our members.</p>
</blockquote>

<p>Translating out the jargon, this means they didn’t have a large negative effect on compressability. This is unsurprising.</p>

<h3 id="how-do-we-apply-neural-networks-at-scale-efficiently">How do we apply neural networks at scale efficiently?</h3>

<blockquote>
  <p>Given our scale, applying neural networks can lead to a significant increase in encoding costs. In order to have a viable solution, we took several steps to improve efficiency.</p>
</blockquote>

<p>Yes, which is why the closed-form solution almost certainly is faster.</p>

<blockquote>
  <p>The neural network architecture was designed to be computationally efficient and also avoid any negative visual quality impact. For example, we found that just a few neural network layers were sufficient for our needs. To reduce the input channels even further, we only apply NN-based scaling on luma and scale chroma with a standard Lanczos filter.</p>
</blockquote>

<p>OK cool, so it’s not actually deep. Why should words have meaning, after all? Only needing a couple layers is not too shocking when, again, <em>there’s a closed-form solution available</em>.</p>

<p>Also, while applying this to only the luma is potentially a nice idea, if it’s shifting the brightness around you can get very weird results. I imagine this is what causes the ‘fake detail’ in the example above.</p>

<blockquote>
  <p>We implemented the deep downscaler as an FFmpeg-based filter that runs together with other video transformations, like pixel format conversions. Our filter can run on both CPU and GPU. On a CPU, we leveraged oneDnn to further reduce latency.</p>
</blockquote>

<p>OK sure, everything there runs on FFmpeg so why not this too.</p>

<h3 id="integrating-neural-networks-into-our-next-generation-encoding-platform">Integrating neural networks into our next-generation encoding platform</h3>

<blockquote>
  <p>The Encoding Technologies and Media Cloud Engineering teams at Netflix have jointly innovated to bring Cosmos, our next-generation encoding platform, to life. Our deep downscaler effort was an excellent opportunity to showcase how Cosmos can drive future media innovation at Netflix. The following diagram shows a top-down view of how the deep downscaler was integrated within a Cosmos encoding microservice.</p>
</blockquote>

<p><img src="/assets/images/netflix-downscaler-diagram.png" alt="Downscaler comparison" /></p>

<p>Buzzword buzzword buzzword buzzword buzzword. I especially hate “encoding stratum function”.</p>

<blockquote>
  <p>A Cosmos encoding microservice can serve multiple encoding workflows. For example, a service can be called to perform complexity analysis for a high-quality input video, or generate encodes meant for the actual Netflix streaming. Within a service, a Stratum function is a serverless layer dedicated to running stateless and computationally-intensive functions. Within a Stratum function invocation, our deep downscaler is applied prior to encoding. Fueled by Cosmos, we can leverage the underlying Titus infrastructure and run the deep downscaler on all our multi-CPU/GPU environments at scale.</p>
</blockquote>

<p>Why is this entire section here? This should all have been deleted. Also, once again, buzzword buzzword buzzword buzzword buzzword.</p>

<h3 id="what-lies-ahead">What lies ahead</h3>

<blockquote>
  <p>The deep downscaler paves the path for more NN applications for video encoding at Netflix. But our journey is not finished yet and we strive to improve and innovate. For example, we are studying a few other use cases, such as video denoising. We are also looking at more efficient solutions to applying neural networks at scale. We are interested in how NN-based tools can shine as part of next-generation codecs. At the end of the day, we are passionate about using new technologies to improve Netflix video quality. For your eyes only!</p>
</blockquote>

<p>I’m not sure a downscaler that takes a problem with a closed-form solution and produces terrible results paves the way for much of anything except more buzzword spam. I look forward to seeing what they will come up with for denoising!</p>

<hr />

<p>Thanks to Roger Clark and Will Overman for reading a draft of this post. Errors are of course my own.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>1: Okay, I can’t help myself but at least I confined it to a footnote. That second sentence is awful writing, and even more bizarre is the third and fourth ones following it, which read like they were written by an entirely different person. I suspect this post went through too many rounds of edits and along the way no one sat back and gave the whole thing a clean read. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>2: It’s not. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>3: PSNR is an error metric that doesn’t take into account the quirks of human perception at all. It’s easy to compute, and “objective”, but unfortunately human brains are weird. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p>4: I’m very, very doubtful. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p>5: Of course, I suspect doing this would reveal that the results are not actually very good. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p>6: One day someone is going to take me seriously when I joke about this and I’m going to regret everything. <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Ryan</name></author><summary type="html"><![CDATA[HAAAAAAANK LOOK AT THE FUCKING IMAGE WHEN ENCODING VIDEO HAAAAAAAAAAAAAAAAAAAANK]]></summary></entry><entry><title type="html">Git for Fansubbing: Probably a Mistake</title><link href="https://redvice.org/2025/git-subbing-was-a-mistake/" rel="alternate" type="text/html" title="Git for Fansubbing: Probably a Mistake" /><published>2025-02-26T00:00:00+00:00</published><updated>2025-02-26T00:00:00+00:00</updated><id>https://redvice.org/2025/git-subbing-was-a-mistake</id><content type="html" xml:base="https://redvice.org/2025/git-subbing-was-a-mistake/"><![CDATA[<p>Fansubbing has, since I stopped actively subbing, heavily migrated from an FTP-focused workflow to a Git-based one. I have long harbored suspicions that this was a bad idea on net, but having a recent opportunity to watch a group sub a show convinced me that a longer explanation of my position might be useful.</p>

<h2 id="history">History</h2>

<p>First, I have to explain the historical workflow. Everyone in a group has access to an FTP server, where the workraw<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> and premux<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> are both uploaded to a folder for the episode as they are encoded. The translator will generally type out the initial script into an Etherpad clone<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>, along with all the signs and timestamps. The typesetter(s) will complete the signs in the pad, marking them off as they go, and uploading the file containing the signs to the FTP when completed with a filename like <code class="language-plaintext highlighter-rouge">showname_06[ts-ryan].ass</code> and all the fonts they used. The timer will take the dialog from the pad and time it in Aegisub, splitting where appropriate, and upload  a <code class="language-plaintext highlighter-rouge">showname_06[time].ass</code> file along with their chosen dialog fonts, and usually copy over the OP and ED files from previous episodes as part of this. Editing can happen either in the pad (my preference) or after timing, in which case there’d also be a <code class="language-plaintext highlighter-rouge">showname_06[edit].ass</code> file dumped in the folder. Translation checking was also handled both ways, when applicable. Finally, the QC will merge the typesetting and finalized dialog and upload a <code class="language-plaintext highlighter-rouge">showname_06[merged].ass</code>, and they or others will upload <code class="language-plaintext highlighter-rouge">showname_06[qc].ass</code> and potentially successive versions with changes in response to the QC pass<sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup><sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>. Finally, someone took the final version, muxed everything together in mkvtoolnix, made a torrent, and threw it up on Nyaa. Show released!</p>

<h2 id="changes">Changes</h2>

<p>Since then, there have been two major changes to the workflow.</p>

<p>The first major change is migrating to Git for a lot of the process. This was, as best I can tell, done in response to the host of different versioned files that would be dumped on the FTP. Some people felt that version control was a better solution, and Git was chosen for that purpose as it was by this point effectively the standard in software engineering.</p>

<p>The second is automating the actual release process much more heavily, in an effort to stamp out issues like forgetting to mux fonts, incorrect naming, and merging the wrong scripts. This workflow change seems conceptually positive to me; even if I have gripes with some of the specific tools used for it<sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>, we were previously at a suboptimal level of release automation. I want to be clear that this is theoretically <em>separate</em> from the migration to Git, and the benefits of automated releases can be accrued independently. The existing tools tie the two together, but I consider them overcomplicated and don’t believe the marriage is intrinsic. There’s nothing stopping people from using a standardized naming scheme on FTP and a more straightforward tool to simplify the merge, muxing, and release processes.<sup id="fnref:7"><a href="#fn:7" class="footnote" rel="footnote" role="doc-noteref">7</a></sup></p>

<p>My core contention is that because the same person made the general tooling improvements <em>and</em> some of the tooling that enabled a Git workflow, people mentally link them and adopt the less useful portion (Git) unnecessarily. I also claim that the existing tools are overly generic and difficult for most people to use.</p>

<h2 id="strengths">Strengths</h2>

<p>I’ll start with what Git’s theoretical strengths: it offers a single latest version of any file going into a release, with a clear history of changes to it, and enables concurrent editing to a file. If you’re using Github, the pull request process also offers a way to review QC changes, creating a middle ground between the QC purely taking notes and the QC applying changes directly. It also creates nice web-based diffs automatically, which makes it generally easier to review others’ work.</p>

<p>Unfortunately, I’m not convinced these are particularly great benefits. Having a single latest version of a file is useful, but wasn’t a big issue in practice on the FTP, even if <code class="language-plaintext highlighter-rouge">showname_ep06[qc][final][v3][no but for real this time].ass</code> looks goofy, and could have been dealt with by standardizing naming (enforced by a release bot). Similarly, the concurrent editing is nice in theory but in practice does not come up very often if you’re separating dialog and TS.</p>

<p>I’m also not sold that the version history matters much at all for subbing. I’ve almost never seen it referenced due to the fundamentally short-term nature of these releases. In the scenario you’re looking to change something for the Blu-rays, it’s usually not long after the project and the staff can just search in the channel to read the discussion, if one ensued. You do ensure that the version history is never accidentally overwritten, which is much easier to do on accident with an FTP, but if it doesn’t matter much either way I don’t consider the occasional mistakes to be of much consequence.</p>

<p>Finally, the pull request process seems mildly beneficial but again not all that great. Most of the changes aren’t actually discussed in context on the site, and instead are discussed in the staff channel in Discord, and even a competent QC will probably leave some changes to the original typesetter, so you’ll end up with the good ‘ol process of pinging the typesetter to push changes anyway. This, and similar workflows borne of the diffs being intrinsic to git, is probably the strongest argument for Git, though I personally know a lot of people that don’t like it and don’t take advantage of this at all. It seems great for some groups, particularly ones composed heavily of programmers comfortable with Github, but probably not great for the median project.</p>

<h2 id="problems">Problems</h2>

<p>Now that I’ve talked about the benefits and why I consider them minimal, what about the downsides?</p>

<p>The biggest one is that Git is <em>really difficult to use</em>. <a href="https://x.com/danluu/status/1356056197963554816">If you think otherwise, you probably don’t understand it very well.</a> Software engineers are overrepresented in fansubbing, so basic familiarity is not uncommon for many of the staff, but even then in practice every group has someone whose part-time job is unfucking the inevitable messes Git produces. And for people who don’t work in software, which includes many of the translators<sup id="fnref:8"><a href="#fn:8" class="footnote" rel="footnote" role="doc-noteref">8</a></sup>, Git means punching in arcane commands or dealing with an extremely complicated GUI, and occasionally getting into a bad state and begging a team member to help<sup id="fnref:9"><a href="#fn:9" class="footnote" rel="footnote" role="doc-noteref">9</a></sup>.</p>

<p>It also necessitates further mandatory tooling setup to contribute to a project. In practice, everyone still uses an FTP for the premuxes<sup id="fnref:10"><a href="#fn:10" class="footnote" rel="footnote" role="doc-noteref">10</a></sup>, so getting Git installed locally along with a GUI, making a Github account, and getting this all running is an extra step for every non-programmer to muddle through. You’re also putting your questionably legal fan translations on Github, which has so far been fine but I personally have reservations about. Also <em>unlike</em> when typesetting gained increased tooling requirements, these offer no improvement (hopefully) to the final output. It’s theoretical minor efficiency, and minor consistency, improvements, in exchange for a more complicated toolchain pushed onto the entire group. The tradeoff is quite different.</p>

<p>It also makes it surprisingly easy to leak personal information if a group wants to make a repo public. There’s an extra manual step of checking to see if anyone used identifying information in their commits, and rewriting the history if so.</p>

<p>Finally, as mentioned earlier, Git adoption seems to have been tied originally to <a href="https://github.com/TypesettingTools/SubKt">SubKt</a><sup id="fnref:11"><a href="#fn:11" class="footnote" rel="footnote" role="doc-noteref">11</a></sup>, and more recently also <a href="https://github.com/Jaded-Encoding-Thaumaturgy/muxtools">muxtools</a>. Both of these are monsters, and are so generic that they force every group to have someone comfortable in Kotlin or Python, respectively, to set up and maintain them for every project. In practice this means that similar to Git, every project ends up needing someone whose part-time job is tech support. These tools are difficult to set up and CLI-only, so good luck to anyone who has to interact with them that isn’t a programmer!</p>

<h2 id="looking-forward">Looking forward</h2>

<p>I think the situation has gotten sufficiently bad that groups have actually ended up in a worse place than the historical FTP-based workflow. I view the current situation as programmers responding to their dislike of manual processes by pushing incredibly complex tools on non-technical users for very marginal benefits (and very likely negative time saved).</p>

<p>What would a better world look like? A more opinionated tool, with YAML or some equivalent for configuration, running as a bot on a server with an FTP and enforcing a standardized naming scheme, and with less magical mega-merging and more checks to make sure everything necessary is present as part of a release, alongside the more rote aspects like muxing and uploading. It should opt for manual processes over automated ones for anything too complicated, and try hard to be straightforward for a nontechnical user. This means it has to be a single binary on Windows/MacOS, packaged in an installer, and have a GUI for editing the configuration and generating a release. No more “just learn python lol”. There’s a world where we have tooling that’s strictly better than the old workflow, but we aren’t there today.</p>

<hr />

<p><em>Thanks to Petzku for reading a draft of this post.</em></p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>1: Initial encode designed to be run off quickly for the subbers in the earlier parts of the process <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>2: Proper encode, ready to be muxed into the finished product <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>3: Etherpad is a minimal collaborative online editor that highlights changes. publishwith.me was historically used for this purpose <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p>4: Some groups had the QC apply changes directly, and others didn’t allow it at all. You’d end up with greater variations on the file name depending on how many people had to touch it to make the changes suggested by QC. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p>5: For the purposes of this piece, I’m counting stuff like running ASSWipe as part of the merge/QC process. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p>6: To be addressed later <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7">
      <p>7: This actually existed in the form or <a href="https://github.com/RHExcelion/Servrhe">Servrhe</a>, which was a Commie-specific bot for automating status updates on the site and releases. However, it was never actually made suitable for public consumption, so adoption outside of Commie was nonexistent <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8">
      <p>8: Which I will note is also the hardest role to fill, by a significant margin <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:9">
      <p>9: This is not some hypothetical issue; I don’t sub anything and I’ve been in multiple project channels where this happened, to the obvious frustration of the poor TL <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:10">
      <p>10: Github’s LFS is an obviously poor option for pirated content <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:11">
      <p>11: Made by the wonderful Myaamori, whose choices I am implicitly complaining about in this article but who I cannot stress enough is absolutely the best <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Ryan</name></author><summary type="html"><![CDATA[Thoughts on the modern fansub workflow, mainly the transition to git and Subkt, and why they haven't worked out well]]></summary></entry><entry><title type="html">JIT Compilers 102</title><link href="https://redvice.org/2023/template-jits/" rel="alternate" type="text/html" title="JIT Compilers 102" /><published>2023-05-03T00:00:00+00:00</published><updated>2023-05-03T00:00:00+00:00</updated><id>https://redvice.org/2023/template-jits</id><content type="html" xml:base="https://redvice.org/2023/template-jits/"><![CDATA[<h1 id="magic-boxes">Magic boxes</h1>
<p>If you have a thing that looks like <code class="language-plaintext highlighter-rouge">source code-&gt;magic box-&gt;program you run</code>, compilers are the <code class="language-plaintext highlighter-rouge">magic box</code>. If you pass source code around everywhere and run the magic box late enough<sup id="fnref:0"><a href="#fn:0" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> people call it “just-in-time” and then act very impressed. If the magic box just runs the source code and doesn’t output a program you run then they call it an interpreter, though, and are no longer impressed anymore.</p>

<p>Sometimes those interpreter magic boxes have another magic box inside them that do <code class="language-plaintext highlighter-rouge">bytecode-&gt;second, smaller magic box-&gt;program you run</code>. If you run the second, smaller magic box on snippets of the program that you notice you’re running a lot, then you call it a hotspot JIT that you integrated into an interpreter, and I’m pretty sure Google or Oracle immediately offers you $300k/yr to suffer through absolutely unhinged CS PhD code for a living<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>.</p>

<p>We’re gonna write a magic box today.</p>

<h1 id="someone-elses-magic-box">Someone else’s magic box</h1>
<p>I could write a JIT background introduction post here, but really there are a bunch of them already and I don’t think I would add very much writing another near-identical one. Instead, I’ll be referring to <a href="https://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-2-an-x64-jit/">this blog post</a>: none of this post will make sense unless you read it. I’ll wait.</p>

<p>Ok cool you’re back. Good blog post, right? That’s basically JIT compilers 101: you can JIT compile an interpreter by abstractly interpreting what operations the program would do if you started at the first bytecode operation and ran each sequential one, but instead of doing the operations immediately you instead <em>record</em> what the operation <em>would</em> do, and then at the end have one linear set of operations that, if you jump to the entry, will have the same result of executing all the operations in sequence. This final set of operations is compiled assembly code, and so Blazing Fast(tm).</p>

<p>Or, well. That’s the hope. The final set of operations is only as fast as what you generate: you could in fact record every single instruction that was executed while you were doing the interpreter part, and then run all those instructions again for your “JIT compiled output”, and while it would <em>technically</em> be JIT compiled it wouldn’t be a very good magic box and probably wouldn’t actually be much faster or different<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p>

<h1 id="a-smarter-magic-box">A smarter magic box</h1>
<p>Smart JITs record what information they were able to glean statically from the first pass, in order to emit final output code that doesn’t have to do operations that are redundant or useless for the output. Very smart JITs like V8 or HotSpot have so many of these that they are basically as complex, or more complex, internally as LLVM or GCC are for normal compilation, because the job is essentially the same: optimizing code. That’s too hard so we’re going to mostly ignore it for now.<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">4</a></sup> There is one thing we can make slightly smarter, though.</p>

<p>That BF JIT from the blog post you read (you did read it, right?) put all the operands for each operation at a constant location each time. BF is simple enough that this isn’t a problem for it at all, since it’s really a 1-register machine: <code class="language-plaintext highlighter-rouge">&gt;</code> implicitly depends on the BF data pointer being in register <code class="language-plaintext highlighter-rouge">r13</code> for it to manipulate, and there are no other live values in the VM other than values stored in the data array. The problem you then have if you were, say, making a calculator, is how would you JIT compile <code class="language-plaintext highlighter-rouge">+</code>, which has two operands and not just a static “current register”? For this example, we’ll be using the expression <code class="language-plaintext highlighter-rouge">(0+1)+(2+3)</code>, which you can evaluate with a simple program in Rust:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">enum</span> <span class="n">Expr</span> <span class="p">{</span>
    <span class="nf">Num</span><span class="p">(</span><span class="nb">i64</span><span class="p">),</span>
    <span class="nf">Add</span><span class="p">(</span><span class="nb">Box</span><span class="o">&lt;</span><span class="n">Expr</span><span class="o">&gt;</span><span class="p">,</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="n">Expr</span><span class="o">&gt;</span><span class="p">),</span>
    <span class="c1">// ... more operations</span>
<span class="p">}</span>
<span class="k">type</span> <span class="n">Value</span> <span class="o">=</span> <span class="nb">i64</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">eval</span><span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">Expr</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Value</span> <span class="p">{</span>
    <span class="k">match</span> <span class="n">e</span> <span class="p">{</span>
        <span class="nn">Expr</span><span class="p">::</span><span class="nf">Num</span><span class="p">(</span><span class="n">u</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="o">*</span><span class="n">u</span><span class="p">,</span>
        <span class="nn">Expr</span><span class="p">::</span><span class="nf">Add</span><span class="p">(</span><span class="n">left</span><span class="p">,</span> <span class="n">right</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">left</span> <span class="o">=</span> <span class="nf">eval</span><span class="p">(</span><span class="n">left</span><span class="p">);</span>
            <span class="k">let</span> <span class="n">right</span> <span class="o">=</span> <span class="nf">eval</span><span class="p">(</span><span class="n">right</span><span class="p">);</span>
            <span class="n">left</span> <span class="o">+</span> <span class="n">right</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>First, a possibly-obvious observation: a recursive evaluation of a tree can be turned into an equivalent set of linear operations, by instead recording what operation you need to evaluate in order<sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> to a linear list. <code class="language-plaintext highlighter-rouge">(0+1)+(2+3)</code> becomes <code class="language-plaintext highlighter-rouge">0, 1, +, 2, 3, +, +</code> in this way. I’ve always called it “linearization”, presumably after stealing it from a paper I read once upon a time; other people call it “flattening” an AST or (if they’re nerds) “A-Normal Form conversion”. This is part-and-parcel of JIT compilation of expression trees, or any non-JIT compiler: you can think of the expression tree as identical to this instead:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>produce 0
produce 1
evaluate +
produce 2
produce 3
evalaute +
evaluate +
</code></pre></div></div>
<p>And, instead of first recording your traversal and then processing the linear list later, you can just do your processing in the recursive traversal directly.</p>

<p>If the equivalent code you are emitting for your JIT compiler for <code class="language-plaintext highlighter-rouge">+</code> expects that the left and right operands and the resulting value are always in the same register each time (say, <code class="language-plaintext highlighter-rouge">rax</code> and <code class="language-plaintext highlighter-rouge">rdi</code>, and the output in <code class="language-plaintext highlighter-rouge">rax</code>), then you also have to make sure that trees of operations correctly put the operands in the correct place in <em>between</em> operations, and none of them ever clobber values that you put in those registers and depend on being kept the same across operations. Consider how you would evaluate <code class="language-plaintext highlighter-rouge">(0+1)+(2+3)</code> if each <code class="language-plaintext highlighter-rouge">+</code> was emitting identically: you evaluate the left tree <code class="language-plaintext highlighter-rouge">0+1</code> which come in <code class="language-plaintext highlighter-rouge">rax</code> and <code class="language-plaintext highlighter-rouge">rdi</code>, and set the output as <code class="language-plaintext highlighter-rouge">rax</code> = <code class="language-plaintext highlighter-rouge">1</code> as a result, and now have to execute <code class="language-plaintext highlighter-rouge">2+3</code> which…needs <code class="language-plaintext highlighter-rouge">rax</code> = <code class="language-plaintext highlighter-rouge">2</code> and <code class="language-plaintext highlighter-rouge">rdi</code> = <code class="language-plaintext highlighter-rouge">3</code> when it evaluates. Messy!</p>

<p>Here a lot of simple JIT blog posts cheat. They just use the processor stack. <code class="language-plaintext highlighter-rouge">+</code> takes two operands and outputs one result; you could write it in assembly as <code class="language-plaintext highlighter-rouge">pop rax; pop rcx; lea rax, [rax+rcx]; push rax</code> so the operands are results need to be at <em>the top of the stack</em> for each assembly snippet, and then whatever <em>registers</em> it uses are anything you want because none of them have to be kept untouched across operations. You only have to make sure that you don’t clobber any <em>stack</em> values, which is pretty easy - your stack when running the right subtree of the above example is then <code class="language-plaintext highlighter-rouge">[3][2][1]</code> and the <code class="language-plaintext highlighter-rouge">+</code> only twiddles the top of the stack and leaves <code class="language-plaintext highlighter-rouge">1</code> untouched, producing <code class="language-plaintext highlighter-rouge">[5][1]</code>.</p>

<p>Here’s a neat trick, which someone told me is technically a variant of “Sethi–Ullman local register allocation” but I can never remember and so call “Oberon-0 abstract stack machine register windowing stuff”<sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">6</a></sup> or similar every time I mention it: Keep track of what stack offset you <em>would</em> be running at, if you did the push/pop stack stuff for operation inputs and outputs. And then map values at each stack offset to a register, and instead of needing to shuffle values to different registers to keep them alive, you emit a <em>different</em> assembly snippet for each operation depending on what offset it would be executing at, if it was using the stack.</p>

<p>Here’s an example, using the <a href="https://crates.io/crates/yaxpeax-x86">yaxpeax_86</a> and <a href="https://crates.io/crates/dynasm">dynasm-rs</a> crates for dynamic assembly creation.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">dynasmrt</span><span class="p">::</span><span class="nn">x64</span><span class="p">::</span><span class="n">Assembler</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">dynasmrt</span><span class="p">::{</span><span class="n">dynasm</span><span class="p">,</span> <span class="n">DynasmApi</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">yaxpeax_x86</span><span class="p">::</span><span class="nn">long_mode</span><span class="p">::</span><span class="n">RegSpec</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">once_cell</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="n">Lazy</span><span class="p">;</span>

<span class="k">static</span> <span class="n">regmap</span><span class="p">:</span> <span class="n">Lazy</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">RegSpec</span><span class="o">&gt;&gt;</span> <span class="o">=</span> <span class="nn">Lazy</span><span class="p">::</span><span class="nf">new</span><span class="p">(||</span> <span class="nd">vec!</span><span class="p">[</span><span class="nn">RegSpec</span><span class="p">::</span><span class="nf">rax</span><span class="p">(),</span> <span class="nn">RegSpec</span><span class="p">::</span><span class="nf">rdi</span><span class="p">(),</span> <span class="nn">RegSpec</span><span class="p">::</span><span class="nf">rsi</span><span class="p">()]);</span>
<span class="k">fn</span> <span class="nf">compile</span><span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">Expr</span><span class="p">,</span> <span class="n">offset</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">ops</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Assembler</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">usize</span> <span class="p">{</span>
    <span class="k">match</span> <span class="n">e</span> <span class="p">{</span>
        <span class="nn">Expr</span><span class="p">::</span><span class="nf">Num</span><span class="p">(</span><span class="n">u</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
            <span class="nd">dynasm!</span><span class="p">(</span><span class="n">ops</span>
                <span class="p">;</span> <span class="n">mov</span> <span class="nf">Rq</span><span class="p">(</span><span class="n">regmap</span><span class="p">[</span><span class="n">offset</span><span class="p">]</span><span class="nf">.num</span><span class="p">()),</span> <span class="n">QWORD</span> <span class="o">*</span><span class="n">u</span>
            <span class="p">);</span>
            <span class="n">offset</span> <span class="o">+</span> <span class="mi">1</span> <span class="c1">// abstract stack push</span>
        <span class="p">},</span>
        <span class="nn">Expr</span><span class="p">::</span><span class="nf">Add</span><span class="p">(</span><span class="n">left</span><span class="p">,</span> <span class="n">right</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">after_left</span> <span class="o">=</span> <span class="nf">compile</span><span class="p">(</span><span class="n">left</span><span class="p">,</span> <span class="n">offset</span><span class="p">,</span> <span class="n">ops</span><span class="p">);</span>
            <span class="k">let</span> <span class="n">after_right</span> <span class="o">=</span> <span class="nf">compile</span><span class="p">(</span><span class="n">right</span><span class="p">,</span> <span class="n">after_left</span><span class="p">,</span> <span class="n">ops</span><span class="p">);</span>
            <span class="k">let</span> <span class="n">pop_right</span> <span class="o">=</span> <span class="n">after_right</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span> <span class="c1">// abstract stack pop</span>
            <span class="k">let</span> <span class="n">pop_left</span> <span class="o">=</span> <span class="n">pop_right</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span> <span class="c1">// abstract stack pop</span>
            <span class="nd">dynasm!</span><span class="p">(</span><span class="n">ops</span>
                <span class="p">;</span> <span class="n">add</span> <span class="nf">Rq</span><span class="p">(</span><span class="n">regmap</span><span class="p">[</span><span class="n">pop_left</span><span class="p">]</span><span class="nf">.num</span><span class="p">()),</span> <span class="nf">Rq</span><span class="p">(</span><span class="n">regmap</span><span class="p">[</span><span class="n">pop_right</span><span class="p">]</span><span class="nf">.num</span><span class="p">())</span>
            <span class="p">);</span>
            <span class="n">pop_left</span> <span class="o">+</span> <span class="mi">1</span> <span class="c1">// abstract stack push</span>
        <span class="p">},</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">jit_and_run</span><span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">Expr</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Value</span> <span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">ops</span> <span class="o">=</span> <span class="nn">Assembler</span><span class="p">::</span><span class="nf">new</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">start</span> <span class="o">=</span> <span class="n">ops</span><span class="nf">.offset</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">res</span> <span class="o">=</span> <span class="nf">compile</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">ops</span><span class="p">);</span>
    <span class="nd">assert_eq!</span><span class="p">(</span><span class="n">res</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span> <span class="c1">// all tree of operations should boil down to one result</span>
    <span class="nd">dynasm!</span><span class="p">(</span><span class="n">ops</span>
        <span class="p">;</span> <span class="n">ret</span>
    <span class="p">);</span>
    <span class="k">let</span> <span class="n">end</span> <span class="o">=</span> <span class="n">ops</span><span class="nf">.offset</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">buf</span> <span class="o">=</span> <span class="n">ops</span><span class="nf">.finalize</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">compiled</span><span class="p">:</span> <span class="k">extern</span> <span class="s">"C"</span> <span class="k">fn</span><span class="p">()</span><span class="k">-&gt;</span><span class="n">Value</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nn">core</span><span class="p">::</span><span class="nn">mem</span><span class="p">::</span><span class="nf">transmute</span><span class="p">(</span><span class="n">buf</span><span class="nf">.ptr</span><span class="p">(</span><span class="n">start</span><span class="p">))</span> <span class="p">};</span>
    <span class="nf">compiled</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If we had the stack-&gt;register map as <code class="language-plaintext highlighter-rouge">rax, rdi, rsi</code> for example<sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">7</a></sup>, then the final assembly for <code class="language-plaintext highlighter-rouge">(0+1)+(2+3)</code> would instead be</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mov rax, 0; // 0
mov rdi, 1; // 1
add rax, rdi; // +
mov rdi, 2; // 2
mov rsi, 3; // 3
add rdi, rsi; // +
add rax, rdi; // +
ret
</code></pre></div></div>

<p>Does that set of operations look familiar? It’s the same as the linearized traversal of the tree from one of the above paragraphs. This program doesn’t use any stack, only registers, and is Blazing Fast(tm). If you run out of registers because you abstract stack offset gets too high, then you can fall back to normal <code class="language-plaintext highlighter-rouge">push</code> and <code class="language-plaintext highlighter-rouge">pop</code> like the “cheating” example - or implement some dumb last-recent-used cache for the registers and insert spilling when you need values at abstract stack offsets that don’t map to registers and you’ve accidentally re-created <a href="https://dl.acm.org/doi/abs/10.1145/330249.330250">Linear Scan Register Allocation</a> (which is a good thing).</p>

<p>This is a specific case of optimizing an interpreter when JITting, where you are able to pre-compute some unique location for each operation inputs and outputs and emitting code tailored for those specific locations, instead of emitting extraneous operations shuffling data around unneeded - and a case that isn’t quite touched on in the original BF JIT series of blog posts, which more talks about optimizing sequences of operations <em>themselves</em> via peephole optimizations, and then skipping to using LLVM which handles global register allocation and removing extraneous moves for entire functions, removing a need for doing this style of optimization yourself. Most other JIT-related blog posts I’ve seen online also don’t handle anything more complex than BF, or implement anything more than the <code class="language-plaintext highlighter-rouge">push</code>/<code class="language-plaintext highlighter-rouge">pop</code> machinery.</p>

<p>A cute paper that extends this idea is “<a href="https://arxiv.org/abs/2011.13127">Copy-and-Patch Compilation: A fast compilation algorithm for high-level languages and bytecode</a>” by Haoran Xu, Fredrik Kjolstad. It uses C++ to generate several different versions of functions which implement bytecode operations, using templating in order to create at compile-time “stencil” versions of the function which pad the argument and result values with dummy values. This has the result of essentially tricking the compiler into generating the various shifted assembly snippets itself, with the register used for each abstract stack index being the corrosponding register in the function ABI.<sup id="fnref:7"><a href="#fn:7" class="footnote" rel="footnote" role="doc-noteref">8</a></sup></p>

<p>This is, in my head at least, also closely related to the “<a href="https://www.researchgate.net/profile/Ian-Piumarta-2/publication/242406574_DELAYED_CODE_GENERATION_IN_A_SMALLTALK80_COMPILER/links/59578a550f7e9ba95e0fd89f/DELAYED-CODE-GENERATION-IN-A-SMALLTALK80-COMPILER.pdf">Delayed Code Generation in Smalltalk-80</a>” paper which not only allocates a unique location of values while doing code generation, but also delays emitting the code to evaluate those values until they are observed by some use, which itself has where it would <em>like</em> the value to be put at the end - and thus can emit code that computes values in that desired destination location directly. This technique was used in the V8 JavaScript JIT for their <a href="https://github.com/eatonphil/one-pass-code-generation-in-v8/blob/main/One-pass%20Code%20Generation%20in%20V8.pdf">one-pass code generation</a> at one point in time: they had a JIT that was “being stupid”, and they replaced it with something “not being stupid”, and reaped some performance gains despite being fairly conceptually simple. And, hopefully, now that you know this exists, you can too.</p>

<hr />
<p>Footnote:</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:0">
      <p>Instead of only running the magic box once, and then distributing the output program to people via unhinged gzip tarball infrastructure, which is the source of numerous headaches for everyone involved and leads to worse things, like learning NixOS. <a href="#fnref:0" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:1">
      <p>I can’t confirm this though. If Google or Oracle people are reading this, feel free to prove me right by offering me $300k/yr. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>I of course am a fool and have <a href="https://redvice.org/2022/lineiform-rust-meta-jit/">another side-project</a> that basically does exactly this. It probably isn’t a good idea, but it’s also kind of a <em>fun</em> idea, which is what really matters. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>The sequel of the blog post I just made you read has a very nice gradual introduction to what kind of optimizations you can do with a JIT compiler, if you’re interested. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p>Technically, the reverse post-order traversal, since you record which operations are <em>completed</em> first, since you want <code class="language-plaintext highlighter-rouge">0</code> and <code class="language-plaintext highlighter-rouge">1</code> to be recorded before <code class="language-plaintext highlighter-rouge">+</code> which use the results is. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5">
      <p>In loving reference to the paper for the <a href="https://people.inf.ethz.ch/wirth/CompilerConstruction/CompilerConstruction2.pdf">Oberon-0 compiler</a>, section 10.1, which uses this technique. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6">
      <p>These sound like they were picked out of a hat, but are fairly intentional: The SystemV ABI that we will be telling Rust the resulting function follows has a list of registers you are allowed to overwrite, and a set of register you aren’t, and all of these registers are allowed to be overwritten. Additionally, <code class="language-plaintext highlighter-rouge">rax</code> is the register for the output of our function: since we always evaluate to one register-sized value, we have the first abstract stack slot be that register so the result of running the expression leaves the result in where Rust expects the output to be. <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7">
      <p>I tried to <del>steal</del> lightly adapt the paper to a Rust implementation, and hit Several Problems. They use a continuation-passing-style method of having the stencils fit together, with the functions ending in a tailcall to the next operation (which they then patch via ELF object crimes when manually stitching them together in the JIT code buffer). Rust doesn’t have guaranteed tailcalls, unlike C, however - in fact, Rust even in the extremely trivial cases where anyone with half a brain could look at the source code and reason “oh yeah you could tailcall here” will not, in fact, have LLVM emit tailcalls for functions. Which means you can’t really stitch together a bunch of function bodies in Rust, because each function will be creating stack frames and only tearing down those stack frames after the next function returns and basically it’s a giant mess. Rust also doesn’t even expose the GHC ABI that Copy-and-Patch uses to get Bonus Registers from the function ABI: the closest thing is an <code class="language-plaintext highlighter-rouge">"unadjusted"</code> ABI that has absolutely zero documentation for what it does or why it exists and also a big unstable feature warning saying “DO NOT USE THIS EVER” which I of course ignored in the course of the experiment. But it doesn’t have GHC ABI, which is kinda important for tricking the compiler into giving you good codegen from the abstract stack offset stencils. <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Charlie</name></author><summary type="html"><![CDATA[There are lots of JIT Compilers 101 blog posts. What's next?]]></summary></entry><entry><title type="html">Samsara, a safe Rust concurrent cycle collector</title><link href="https://redvice.org/2023/samsara-garbage-collector/" rel="alternate" type="text/html" title="Samsara, a safe Rust concurrent cycle collector" /><published>2023-01-02T00:00:00+00:00</published><updated>2023-01-02T00:00:00+00:00</updated><id>https://redvice.org/2023/samsara-garbage-collector</id><content type="html" xml:base="https://redvice.org/2023/samsara-garbage-collector/"><![CDATA[<p>Rust is a great language. It gives you a lot of control over memory layout of objects and allocations, but enforces that your use of those objects’ lifetimes are managed correctly via its borrow checking system: if you take a pointer to an object, you have to statically prove to the compiler that the pointer can never outlive the object.</p>

<p>In the majority of cases, this is approximately trivial: you create some objects, get pointers to them and do some set of operations, complete the operations and all the pointers go away, and then destroy all the objects. More complex programs have problems, however, especially if the answer to the question “who is in charge of destroying this object?” doesn’t have a great answer, and requires a bit more work.</p>

<p>C++ has a <code class="language-plaintext highlighter-rouge">shared_ptr&lt;T&gt;</code> type for a <em>reference counted</em> pointer to an object. Every time you copy the <code class="language-plaintext highlighter-rouge">shared_ptr</code>, it increment a counter; every time you destruct a <code class="language-plaintext highlighter-rouge">shared_ptr</code>, you decrement the counter. Once the counter hits zero, you can destroy the object the <code class="language-plaintext highlighter-rouge">shared_ptr</code> is pointing to because you know you were the last person that has access to the object, and can lock the door on your way out.</p>

<p>Rust has the same thing in its <code class="language-plaintext highlighter-rouge">Rc&lt;T&gt;</code> and <code class="language-plaintext highlighter-rouge">Arc&lt;T&gt;</code> types (with <code class="language-plaintext highlighter-rouge">Rc</code> being “reference counted” and <code class="language-plaintext highlighter-rouge">Arc</code> being “atomically reference counted” - <code class="language-plaintext highlighter-rouge">Arc</code> is safe to share between threads, with the count increment/decrement using atomic operations). The difference is that C++ doesn’t fix all memory safety bugs with shared_ptr: dereferencing the <code class="language-plaintext highlighter-rouge">shared_ptr</code> gives you a raw pointer, which you can erroneously hold past destroying the <code class="language-plaintext highlighter-rouge">shared_ptr</code> it came from, which may destroy the object. Rust’s <code class="language-plaintext highlighter-rouge">Rc</code> smartpointers instead give you a <em>borrow</em> to its contents, and thus are guaranteed by the compiler not to outlive the object they’re borrowing from (i.e. the Rc).</p>

<p>Rust also prevents data races through its <code class="language-plaintext highlighter-rouge">Arc</code> type: <code class="language-plaintext highlighter-rouge">shared_ptr</code> is like <code class="language-plaintext highlighter-rouge">Arc</code> in that incrementing and decrementing the count across different threads that are pointing to the same object are safe for both of them, but <code class="language-plaintext highlighter-rouge">shared_ptr</code> is still vulnerable to data races, for example if two threads are attempting to write to the object at the same time, or one is writing while another is reading the same memory location. Rust only gives you a read-only pointer to the contents of an <code class="language-plaintext highlighter-rouge">Rc</code> or <code class="language-plaintext highlighter-rouge">Arc</code> pointer - in order to mutate an object behind an Arc, you need to use one of Rust’s “interior mutability” types, such as <code class="language-plaintext highlighter-rouge">RefCell&lt;T&gt;</code>, which does a <em>dynamic</em> check that there is either several read-only pointers to the object, or one read-write pointer, but not both, or else it will panic and crash the program. Usually Rust enforces the read xor write access invariant using its borrow checker system, but because you have non-trivial ownership for objects you can’t use the borrow checker, and have to have those checks at runtime instead of compile time.</p>

<p>This is all well and good. With <code class="language-plaintext highlighter-rouge">shared_ptr</code>s or <code class="language-plaintext highlighter-rouge">Arc</code> you can make big tangled data structures all pointing to one another. You can make a doubly-linked list, or a graph with edge that are pointers to nodes, or a red-black tree with “parent” links. The problem is that it will leak memory, and eventually your program will get very slow and then die. What went wrong?</p>

<h1 id="cycles">Cycles</h1>
<p><code class="language-plaintext highlighter-rouge">shared_ptr</code> and <code class="language-plaintext highlighter-rouge">Arc</code> have the same problem: they have a counter, which they maintain as the number of existent pointers to this object. If you have a cycle of objects like A&lt;-&gt;B, where A and B are two different objects but both have pointers to eachother, then both have a count of 1 and thus can’t be deleted, because they’re still accessible from something. If your program has destroyed every <em>other</em> reference to A or B, so they are unreachable except through the internal pointers to eachother, than they can safely be destroyed! They’re garbage, because there is no possible (correct) way for your program to access that memory. But they won’t be, because the reference counting isn’t smart enough to know the difference between pointers that are <em>reachable</em> and <em>unreachable</em>, and just sees that both objects have a reference-counted pointer <em>somewhere</em> and can’t be destroyed yet.</p>

<p>This is a pretty bad problem to have. The normal solution for this in Rust or C++ is to use an arena allocator, where you allocate a memory region upfront, and then can have objects within that region that can point to other arena objects, and then at the end you free the entire arena. This isn’t a <em>great</em> solution, since you are essentially still leaking memory, it’s just eventually you will free that leaked memory when you teardown the entire arena, and not any time sooner. You can also use a <em>real</em> garbage collector, like <a href="https://github.com/Others/shredder">shredder</a> or the <a href="https://www.hboehm.info/gc/">boehm-demers-weiser</a> GC. These will occasionally do a scan of all reachable objects, freeing everything else - they don’t care about cycles, because if the cycles of objects aren’t reachable from any threads they won’t be marked as reachable, and thus freed because they don’t keep precise reference counts. You can really think of an arena allocator as a bad version of a semi-space garbage collector, where you can only ever do one collection and it always frees everything: this is obviously still <em>useful</em> in that it guarantees that all your memory is reclaimed, but has obvious downsides.</p>

<p>My problem with shredder, or rust-gc, or any of the several other Rust <a href="https://manishearth.github.io/blog/2021/04/05/a-tour-of-safe-tracing-gc-designs-in-rust/">garbage collector libraries</a> is that they’re <em>complex</em>. A lot of them have novel schemes to make sure that your program maintains a set of initial reachable objects (“roots”) correctly at all times, since if it doesn’t you will incorrectly free memory and crash (or worse), and don’t have very high confidence that they are managing those roots <em>correctly</em> and thus have warnings about not being production ready. All(?) of them also have stop-the-world phases where they have to pause every running thread in your process in order to scan all the roots from a coherent view (or don’t support multithreading at all!).</p>

<h1 id="collection">Collection</h1>
<p>Turns out there’s a neat variant of garbage collection called <em>cycle</em> collection. I was ruminating on how complex all the Rust garbage collectors were a few months ago, and how there isn’t just an Arc-but-doesnt-leak replacement smartpointer you can drop-in to programs that need it, and my addled brain dredged up half-remembered thoughts of when I read <a href="https://www.researchgate.net/publication/2528969_Concurrent_Cycle_Collection_in_Reference_Counted_Systems">Concurrent Cycle Collection in Reference Counted Systems, 2002</a> a few years before. I then brashly and confidently tweeted about how you could make a very simple Arc-but-doesnt-leak replacement smartpointer library in Rust, and banged out a design based on the general principals I remembered. And then I actually re-read the paper and realized it had object color state transition diagrams that look like this.
<img src="/assets/images/bacon-rajan-colors.png" alt="object color state transitions" />
Which I had, by pure coincidence, been using as the punchline for jokes about how complex concurrent garbage collection systems since, uh, probably the same several years timespan since I last read the paper. Woops.</p>

<p>The basic idea with the cycle collection scheme is that by default, objects are the same as reference-counting. The difference is that when your count is <em>decremented</em> to a number other than 0, you add the object to a list, and then when doing a garbage collection you scan all the objects in your list (recursively visiting all objects reachable from each object) to see if any of them form graphs of objects where all the objects’ references are accounted for with only internal pointers to eachother. The idea is that in order to form a cycle, you have to have a mutator thread:</p>
<ul>
  <li>Create an object A</li>
  <li>Create an object B</li>
  <li>Increment A’s refcount and create A2, and set B.ptr = A2</li>
  <li>Set A.ptr = B (the original B)</li>
  <li>Destroy A, which is the last external pointer into the cycle of objects.</li>
</ul>

<p>Cycle collection handles this by making the “destroy A” step, that is the point where you make a group of objects unreachable, instead essentially mark the objects as <strong>potentially</strong> unreachable. The scan step then finds all objects that <strong>are</strong> unreachable, because you know none of the objects in some graph have external pointers, and thus nothing can acquire a new pointer to any of them, and <strong>not</strong> unreachable, because your scan didn’t find all the pointers, and thus something <em>else</em> by definition has at least one pointer and reference count.</p>

<p>The nice thing about this is that, in the best-case of all your objects just being created with a reference count of 1, and then being used a bit and then decremented from 1-&gt;0, objects are just the same as <code class="language-plaintext highlighter-rouge">Arc</code> and freed immediately, with no extra work being needed. Even if the Arc’s count was incremented and then decremented twice instead (1-&gt;2-&gt;1-&gt;0) you can still immediately free the object: you know you had the last reference to the object, and thus it can’t possibly be leaked via cycle, and so you can just remove the object from the list and then free it immediately. The recursive scan is only ever needed to free objects that were <em>actually</em> leaking memory via cycles.</p>

<p>We visit objects in order to count how many pointers to them we’re able to find, because if we can find an equal number of pointers to an object as it’s reference count then we know the mutator <em>doesn’t</em> have any pointers, and if we find a disconnected group of those objects then we can free all of them at once since they’re collectively unreachable.</p>

<p>There’s a bunch of complex stuff that you have to handle in order to have correct visiting in the face of concurrrency, unfortunately - if you’re scanning a graph of objects, by the time you scan the last object that first object’s count may have changed. We use a scheme where objects have a bitmap of flags, and are marked VISITED when we first see it, and then if the object is decremented while VISITED it is transitioned to DIRTY. When checking to see if an object is leaked or not we can then also see if it is DIRTY or not, where we know that a mutator <em>had</em> a live reference to the object, and thus it and all reachable objects can be marked not leaked. The object is queued to be added to the next collection’s list instead (since the decrement may have been for the last external reference).</p>

<p>Additionally, consider what happens if we visit <code class="language-plaintext highlighter-rouge">struct Foo { a: Mutex&lt;Option&lt;Gc&lt;Bar&gt;&gt;&gt;, b: Mutex&lt;Option&lt;Gc&lt;Bar&gt;&gt;&gt; }</code> concurrently with a mutator swapping the contents of <code class="language-plaintext highlighter-rouge">foo.a</code> and <code class="language-plaintext highlighter-rouge">foo.b</code> - we could count <code class="language-plaintext highlighter-rouge">a</code> as one pointer to whatever it is referencing, and then the mutator swaps the field, and then we could count <code class="language-plaintext highlighter-rouge">b</code> as a second pointer to…the exact same object as before, accidentally double-counting the first pointer’s object and under-counting the second’s, which would be very bad. The collector normally takes a read-lock on the <code class="language-plaintext highlighter-rouge">Gc&lt;T&gt;</code> objects while it scans them, which either <strong>fails</strong> if object has an outstanding <code class="language-plaintext highlighter-rouge">&amp;mut</code>-reference from a mutator (in which case we obviously know it’s still reachable from something) or <strong>succeeds</strong> and blocks mutators from acquiring <em>new</em> <code class="language-plaintext highlighter-rouge">&amp;mut</code>-references which are required to swap around fields of objects. The entire point of interior mutability, however, is being able to modify fields with only a <code class="language-plaintext highlighter-rouge">&amp;</code>-reference! The simple solution, and what I did, is just require users to mark any type that has interior mutable fields containing <code class="language-plaintext highlighter-rouge">Gc&lt;T&gt;</code> references, and take a <strong>write-lock</strong> instead of a <strong>read-lock</strong> if so, which guarantees the object is completely inaccessible even via <code class="language-plaintext highlighter-rouge">&amp;</code>-reference while we visit it.</p>

<h1 id="samsara">Samsara</h1>
<p><a href="https://github.com/chc4/samsara">Samsara</a> is my library that provides a <code class="language-plaintext highlighter-rouge">Gc&lt;T&gt;</code> type which is a reference counted with cycle collection smartpointer. It is a <strong>concurrent</strong> garbage collector, with the potentially-unreachable list scanning happening on another thread (only via manual trigger, not automatically via heuristiscs for now). And, unlike every(?) other Rust garbage collector that I know of, it doesn’t have a global stop-the-world phase: it will occasionally block mutator threads if they try to acquire a read-write reference from a Gc object while the collector is scanning it, but that should be very short (since it only holds the read-lock while visting that object, and not the entire subgraph). It doesn’t interrupt threads via signals or anything for safepoints, either, unlike some other garbage collectors.</p>

<p>Right now it’s actually implemented with only safe Rust, plus some unsafe libraries (<code class="language-plaintext highlighter-rouge">im</code> for an immutable datastructure, and <code class="language-plaintext highlighter-rouge">qcell</code> for an ergonomic get/set API to try and prevent user deadlocks). It is, literally, just Rust’s <code class="language-plaintext highlighter-rouge">Arc&lt;T&gt;</code> behind the scenes, plus an <code class="language-plaintext highlighter-rouge">RwLock&lt;T&gt;</code> for the interior mutability, with the collector list holding <code class="language-plaintext highlighter-rouge">Weak&lt;T&gt;</code> references to objects that need to be scanned. Even if the collector incorrectly frees a set of objects that it thinks aren’t reachable any more, it would panic the user via an <code class="language-plaintext highlighter-rouge">Option::unwrap</code> instead of giving a use-after-free.</p>

<p>It isn’t the most efficient currently. When doing a cycle collection it will probably use a lot of memory, especially if every live Gc object is reachable from one of the objects on the list, since it creates a big HashMap of objects and adjacency list of edges. It also currently has the problem where objects are sent to the list, or removed from the list, via sending a message on a channel to the collector thread, which means that if you trigger a collection and then decrement a bunch of Gc objects before it finishes, it will block on the channel since it isn’t being drained while a collection happens. I think that the “list” could instead be maintained via an instrusive doubly-linked-list in the <code class="language-plaintext highlighter-rouge">Gc&lt;T&gt;</code> header, in order to allow the mutators to add or remove their objects from the list without needing the collector to do anything; unfortunately it would be hilariously unsafe and a lot of work to make sure it’s sound, and without a concrete usecase that is held back by the current design I probably won’t get around to trying that scheme out for a while.</p>

<p>I don’t claim to be doing anything novel here, for the record! For one, I’m obviously basing this off the paper from 2002, though doing things a little bit different. For another, there are several other Rust crates that <em>also</em> implement the paper! <a href="https://github.com/fitzgen/bacon-rajan-cc">fitzgen’s bacon-rajan-cc</a> and <a href="https://github.com/artichoke/cactusref">cactusref</a> both are probably equally good if not better fits for anything you’d want to use samsara for - I mostly just nerdsniped myself and thought it would be neat to write this, and so I did.</p>]]></content><author><name>Charlie</name></author><summary type="html"><![CDATA[Reference counting is great, but what if you need something better?]]></summary></entry><entry><title type="html">Lineiform, a meta-JIT library for Rust interpreters</title><link href="https://redvice.org/2022/lineiform-rust-meta-jit/" rel="alternate" type="text/html" title="Lineiform, a meta-JIT library for Rust interpreters" /><published>2022-02-14T00:00:00+00:00</published><updated>2022-02-14T00:00:00+00:00</updated><id>https://redvice.org/2022/lineiform-rust-meta-jit</id><content type="html" xml:base="https://redvice.org/2022/lineiform-rust-meta-jit/"><![CDATA[<p>Lineiform is my attempt at writing a JIT as a <em>library</em>, so that it can be used by various programming language interpreters in an agnostic way.</p>

<p>JIT stands for <a href="https://en.wikipedia.org/wiki/Just-in-time_compilation">just-in-time compiler</a>. When programming languages are written, they usually first start off as an interpreter. Once they reach a critical amount of usage and support (and people complaining about how slow they are), they sometimes adopt JIT compilers. The interpreter side is usually written as a big case/switch over language operation types (Add, If, Break, Function call, etc.) which run a specific part of code; the JIT side, for code that is ran a sufficient amount of time, instead <em>abstractly</em> run a snippet of code, and record what operations it would run if it was being interpreted…and instead emit the equivalent operations as assembly in some compiler-ish way, so that jumping to the start of the assembly is equivalent to running the list of operations all at once without the overhead of having to switch and dispatch to the correct interpreter operation handler at each step.</p>

<p>There are a few problems to this.</p>

<p>For one, JITs are <strong>really fucking hard</strong> to write. They are the subject of PhD theses, like Maxime’s <a href="https://pointersgonewild.com/2013/10/31/higgs-the-first-jit-compiler-of-its-kind/">Higgs</a> VM. Very good JITs like Java’s HotSpot or Javascript’s V8 have possibly <em>millions</em> of man-hours dedicated to them to try and make them as fast as possible, and LuaJIT (which is regarded as one of the best and “simplest” JITs ever made) is so extremely impressive it is a running joke that Mike Pall, its author, is a robot sent back in time from the future because it is so unbelievable that a normal human being wrote it himself.</p>

<p>Because they are so hard, the second problem falls out: not many languages reach that step. Most languages only ever had an interpreter, because the amount of effort to write a JIT outweighs the benefits that could be reaped by just trying to squeeze out some speed from the existing interpreter, or adding new features.</p>

<p>And the third problem is that the actual act of writing a JIT is fraught with peril: you have to make sure that the assembly you are emitting is <strong>exactly</strong> equivalent, from an external point of view, as running the interpreter operations naively. And the <em>process</em> of writing the assembly emission via abstract interpretation usually looks an awful lot like the normal interpreter, but slightly different - you have a giant case/switch over operations, and the operations do something that kinda looks like an If or a Break or a Function call, but just defer the actual act for a bit later. It’s tedious work, and because the JIT and interpreter operations are essentially 1:1, it’s work to keep them in sync whenever you touch interpreter code. But even worse is that it’s <em>bespoke</em>. V8 is one of the most complex programs ever written, and Google has poured more money than God into it, but all that effort is almost completely unusable for any other JIT because of how extremely specialized the work is to Javascript and V8 itself.</p>

<p>Lineiform is my attempt to solve this at least a little bit. The idea is that instead of having an interpreter, and eventually having to start nearly from scratch implementing an equivalent JIT, you write an interpreter in a slightly special way and make sure to call some of Lineiform’s library functions at certain points. Lineiform will then <em>automatically</em> do JIT-esque things: optimize hot code into runs of operations without dispatch overhead, or getting rid of extraneous work that an intelligent JIT compiler would’ve been able to elide. This is probably the utter height of hubris; the closest equivalent is something like Truffle/Graal or RPython, wherein you write an interpreter in some subset of Java or Python and, through just normal JITting, it also optimizes <em>your</em> interpreter: your language semantics are in terms of Java or Python operations, and it knows how to emit assembly and optimize those, so it comes along for the ride.</p>

<p>I don’t want to write an interpreter in Java or Python though, and they have some annoying constraints (<del>such as Truffle/Graal requiring a commercial license from <em>Oracle</em> in order to use, and most people don’t trust Oracle with a 20ft pole…</del> edit: Woops, that’s a lie - the Community version of Graal is GPL, with an Enterprise version with extra features). I want to write an interpreter in Rust. And Rust doesn’t have a bytecode interpreter that it targets, so you can’t just write a JIT for that and JIT arbitrary interpreters “for free”. So what do you do?</p>

<h1 id="closure-generation-compilers">Closure generation compilers</h1>
<p>Well, one piece of the puzzle is something called a “closure generation compiler”. They don’t seem to be very well-known; I was introduced to the concept by <a href="https://www.youtube.com/watch?v=V8dnIw3amLA">a random Facebook engineer’s compiler presentation</a>, and most of the prior art for it is by a <a href="https://www.sciencedirect.com/science/article/abs/pii/0096055187900129">single paper</a>’s author, Marc Feeley. A normal tree-walking interpreter flow for an <code class="language-plaintext highlighter-rouge">evaluate</code> function is something like switch on Increment -&gt; [recurse on argument -&gt; switch on Constant(3) -&gt; return 3] -&gt; Increment 3, return 4.  A <em>closure generation</em> interpreter would instead be a <code class="language-plaintext highlighter-rouge">build</code> function, which does switch on Increment -&gt; [recurse on argument -&gt; switch on Constant(3) -&gt; return <em>a closure</em> that when evaluated returns 3] -&gt; return <em>a closure</em> that when evaluated runs the constant 3 closure and adds 1. And then at the very end, to <code class="language-plaintext highlighter-rouge">evaluate</code> instead of <code class="language-plaintext highlighter-rouge">build</code>, you call the returned closure, which just runs the increment closure and then the argument closure: it separates the interpreter step into two parts, generating a big tree of closures that call other closures for each operation, and actually evaluating which only run the closures, which jump directly into each language operation runtime behavior. This reduces the interpreter overhead, since you don’t have the case/switch in the operations you have to do run the language.</p>

<p>I read that and thought it was neat, and then I thought a bit <em>more</em> and went: Huh, that sounds a little like a JIT. You are <em>deferring</em> the actual evaluation of language operations until later, and thus requiring the evaluation do less work because you were able to do some things ahead of time (specializing the runtime behavior to a closure you can just call). And how closures work in Rust is that they are essentially structs, which have a <strong>function pointer</strong> you jump to in order to run it, and a <strong>closed environment</strong> which is some list of values in memory which is passed to the function pointer as an argument: this is why <code class="language-plaintext highlighter-rouge">let three = ||(return 3); ||(three() + 1)()</code> works, because <code class="language-plaintext highlighter-rouge">three</code> is put in the closure environment and fetch out and called when it is run.</p>

<h1 id="forth-and-direct-threading">Forth and direct threading</h1>
<p>Another piece of the puzzle is the programming language Forth. Forth is kinda neat, in that it has an interpreter design that isn’t used very often outside of it: it uses a <em>direct threading</em> interpreter. This is designed so that instead of a big case/switch of operations, where it jumps to the implementation of the operation and then jumps back to the case/switch for the next one, you instead build a big table of function pointers to call, each one its own operation. And critically, <em>each operation ends with a jump to the next entry in the table</em>. Running the interpreter is then just jumping to the first entry in the table, which runs its body and then it jumps to the next entry, which runs its body, which…etc. Constants, for example, are just snippets that return the constant value, and Forth is a stack based language, where operations store values on the stack or pop them to use as values. <code class="language-plaintext highlighter-rouge">3 5 +</code> then is a program that jumps to a snippet that pushes 3 to the stack and then runs the next operation, which is a snippet that pushes 5 to the stack and then runs the next operation, which is to pop two values (3 and 5) from the stack and then add them and return.</p>

<p>One insight that direct threaded interpreters use is that you can <em>compile</em> direct threaded functions, however! If you have the function <code class="language-plaintext highlighter-rouge">3 5 +</code>, you can just stitch all the functions together into one big function, and not need tailcalls at the end of operations. If you have a list of operations, then the sequence of instructions that it will end up running will by definition be all of them sequentially, and you could just emit and run that directly.
If you have an optimizing compiler in front, then maybe it would be able to notice when it is emitting code that there’s no reason to push 3 and then pop 3 from the stack into a register for the add later when it can just use the constant 3 in a register directly, and the same for 5, and then maybe it can notice that it can just fold 3+5 to 8 directly and emit <code class="language-plaintext highlighter-rouge">8</code> instead. Because you had <em>extra knowledge</em>, namely the full list of operations that the function would run, you could do global optimizations and get rid of some of them.</p>

<p>This is one of the key insights that interpreters are missing, and why they are slow in a lot of cases: if you have a sequence of language operations that, say, add values together, those operations might have to check if the value they are adding is an int or a float, or an object with an overloaded <code class="language-plaintext highlighter-rouge">+</code> operator, and so has to do some work to figure it out when it is evaluating. If you have a <em>concrete</em> operation, such as <code class="language-plaintext highlighter-rouge">3 5 +</code>, then you know what work is unneeded: your values are always numbers, and so you can <strong>specialize</strong> the add to only add together integers, and maybe even further to just compile down to a constant 8. Language primitives have to be <em>maximally permissive</em>, because they have to cover all possible inputs; if you know inputs, even only some of them, then you can reduce the range of possible cases they need to cover, and do global optimizations to remove redundant things. You can think of this as a special case of <a href="https://en.wikipedia.org/wiki/Partial_application">partial application</a>, where you are fixing the function pointer that the tailcall at the end of snippets will run to concrete values, and so can apply it at compile time instead of having to do it at runtime.</p>

<h1 id="putting-them-together">Putting them together</h1>
<p>Lets return to Rust closures. When you run the closure generation interpreter, you are taking some dynamic input and returning a tree of closures that have other closures they call in their environment; if you have the rust closure <code class="language-plaintext highlighter-rouge">let three = ||(3); let five = ||(5); let add = ||{ three() + five() }; add()</code> then it is one closure which has two entries in its environment (three and five). It jumps to their implementation, which return values. If you squint, this is <em>very similar</em> to how a direct threaded interpreter works, but the calls happen in the middle of the function instead of before the add, and so leave their data in immediates in the function instead of on a global stack. You still have <em>concrete implementations</em> of snippets of code you know you will be running if you call the closure, it’s just harder to figure out what they are since it’s not a linear array. In fact, in order to figure out what closures the add closure would be calling, you pretty much have to just run it, since the closure can be doing <em>arbitrary x86 instructions</em>, not just maybe some list of interpreter operations that you could special-case on how to put together and optimize. If you aren’t crazy, this is about when you start thinking about how rustc emits LLVM IR and how you can maybe leverage that, and invent <a href="https://github.com/nbp/holyjit">holyjit</a>. Unfortunately, I’m crazy, and don’t want to do things like vendor a custom rustc fork to build programs with.</p>

<p>Well, it turns out there is maybe hope. There is a concept of a <a href="https://en.wikipedia.org/wiki/Dynamic_recompilation">dynamic recompilation engine</a>. QEMU uses this, for example: when you run a QEMU aarch64 VM on an x86 host, behind the scenes it is translating those ARM instructions into an internal representation that keeps the same operational semantics as the ARM program, and then emitting x86 that from the outside has the same runtime behavior. QEMU is JIT compiling arbitrary ARM code <em>from ARM to x86</em>. And this isn’t restricted to only QEMU; things like <a href="https://dynamorio.org/">DynamoRIO</a> also use dynamic recompilation in order to JIT <em>from x86 to x86</em>, so that you can do neat things like add code before and after any branch instruction in a binary: it <strong>lifts</strong> x86 instructions to an IR, adds whatever operation you want done before branches, and then <strong>lowers</strong> the IR back to x86, making sure to fixup memory accesses so that nothing can tell that it was moved and modified.</p>

<p>And now that we have the background, we have Lineiform: Lineiform is just a wrapper function that takes a closure as input, and returns a closure as output. In the <em>middle</em> however, it will <strong>dynamically inline</strong> calls to other closures that the input might be doing, using dynamic recompilation to lift, optimize, and then lower x86 instructions to a set of x86 instructions with the same operational semantics, but hopefully doing less work such as inlining small closure snippets so there isn’t call/ret and ABI overhead, or folding constants like 3+5. We can have a performant closure generation interpreter, which is already plenty fast, and hopefully optimize out whatever work we can from hot closures, such as commonly called methods or loops, to avoid things like type checks, in order to incrementally JIT code faster and faster. And critically, this should all be very easy to integrate with an existing codebase or arbitrary new interpreter, since it doesn’t depend on any interpreter specific concepts or require re-implementing of your interpreter as an entirely new JIT like existing projects: you only have the one interpreter case/switch dispatch, which builds a reified tree of snippets it jumps between, which Lineiform collapses down into a single code block and optimizes.</p>

<p>One of the inspirations for this project is stumbling upon, years ago, a cool GitHub repo called <a href="https://github.com/matthewfl/redmagic">redmagic</a> that is kind of similar: it’s a C library that you would annotate a normal interpreter with, and it would automatically build program traces for a tracing JIT by just stitching together the assembly instructions that singlestepping would hit. In practice, that’s very different than how Lineiform work, but it’s also <em>really cool</em> and I’ve been thinking about it on and off ever since; it’s unfortunately also not very useful (with the quoted performance gains ~10% in very specific cases, and development stalled), and I’m hopeful that being a lot more structured will help Lineiform do a lot better.</p>

<p>And there are lots of cases where this could be useful - a hot interpreter loop that indexes into an array, for example, would just be calling an Index operation that needs to do bounds checks each time, because it has no way of knowing across interpreter operations that it has already done a check, or calling a method on some dynamic object multiple times having no way of knowing if the method exists on the object or not at each invocation. With Lineiform, the hope is that it exposes the multiple Index operations to a single function optimizer, which can remove the redundant bounds checks for example. And the pie-in-the-sky goal is to do some really tricky stuff with persistent caching of the IR in order to extract an <em>ahead of time</em> compiler for interpreters too, so that you only have to pay for the dynamic recompiling and optimization once, ever, for a program.</p>

<p>The goal is to provide a modest speedup for whatever interpreter is able or willing to use Lineiform, with a <em>fraction</em> of the effort of writing a normal JIT. It’s not going to be as performant as an artisanal hand-rolling JIT…but hopefully it will be close, and nearly free.</p>

<p>So far it doesn’t do much: it can inline some simple closures and fold simple math operations across them, such as:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">a</span><span class="p">:</span> <span class="n">Wrapping</span><span class="o">&lt;</span><span class="nb">usize</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nf">Wrapping</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">jit</span> <span class="o">=</span> <span class="k">crate</span><span class="p">::</span><span class="nn">Lineiform</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="k">use</span> <span class="nn">core</span><span class="p">::</span><span class="nn">hint</span><span class="p">::</span><span class="n">black_box</span><span class="p">;</span>
<span class="k">let</span> <span class="n">clos</span> <span class="o">=</span> <span class="n">jit</span><span class="nf">.speedup</span><span class="p">(</span><span class="k">move</span> <span class="p">|()|</span> <span class="p">{</span> <span class="nf">black_box</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="o">+</span> <span class="nf">Wrapping</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="p">});</span>
</code></pre></div></div>
<p>optimizing down to</p>
<pre><code class="language-assembly">push rbp
mov rbp, rsp
mov eax, 0x3
mov rsp, rbp
pop rbp
ret
</code></pre>

<h1 id="forward-from-here">Forward from here</h1>
<p>I was trying to use <a href="https://github.com/bytecodealliance/wasmtime/tree/main/cranelift">Cranelift</a>, a small and fast codegen backend that is mostly used by Fastly for their WebAssembly JIT, for this but the usecase that I’m targeting is sufficiently weird that it didn’t work all that well. I’ve been working on an alternate codegen backend, handrolled specifically for Lineiform’s constraints, that I’m fairly hopeful will work out ok - the scope is a lot more limited than a normal compiler in various ways, so it’s not like I’m going to have to reimplement all of LLVM. I’ll probably implement a simple closure generation Scheme interpreter, and then try to implement operations as needed to make it work until I have a PoC; I don’t know how long this will take, but it’s probably a while. In the mean time, if you’re interested you can check out the (extremely work in process) <a href="https://github.com/chc4/lineiform">GitHub repo</a>.</p>

<p>And if it works, and if it’s fast, and if it’s useful? Who knows. I’d like if Apple or Facebook paid me a lot of money for this library in order to make things fast (think being able to JIT compile build scripts or DSLs), so I’m not sure if I’ll opensource or only source-available it, just in case. If you’re the type of person that cares about licensing (or just want to chat about compilers or systems programming things!), reach out to me <a href="https://twitter.com/sickeningsprawl">on Twitter</a>.</p>]]></content><author><name>Charlie</name></author><summary type="html"><![CDATA[Interpreters, JITs, and a hopefully novel attempt at building one as a library]]></summary></entry></feed>