<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <updated>2026-10-16T03:59:58Z</updated>
  <generator>https://njump.me</generator>

  <title>Nostr notes by lemon (Unofficial)</title>
  <author>
    <name>lemon (Unofficial)</name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://njump.me/npub1z5cnxa4m6msdhl7d0654wz5pldk56luajy5hdz45yy3a8dhn54us5n9c2p.rss" />
  <link href="https://njump.me/npub1z5cnxa4m6msdhl7d0654wz5pldk56luajy5hdz45yy3a8dhn54us5n9c2p" />
  <id>https://njump.me/npub1z5cnxa4m6msdhl7d0654wz5pldk56luajy5hdz45yy3a8dhn54us5n9c2p</id>
  <icon>https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f994.png</icon>
  <logo>https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/1f994.png</logo>




  <entry>
    <id>https://njump.me/nevent1qqsw6l5wq5hypz6aaf7xznun3a2mx6alcj2d976wkcadfw3jcc8mm7qzyq2nzvmkh0twpklle4l2j4c2s8ak6ntlnkgjja52kssj85ak7wjhj3f3rha</id>
    
      <title type="html">Chasing a -O2 pessimization in antcc Recently I was benchmarking ...</title>
    
    <link rel="alternate" href="https://njump.me/nevent1qqsw6l5wq5hypz6aaf7xznun3a2mx6alcj2d976wkcadfw3jcc8mm7qzyq2nzvmkh0twpklle4l2j4c2s8ak6ntlnkgjja52kssj85ak7wjhj3f3rha" />
    <content type="html">
      Chasing a -O2 pessimization in antcc&lt;br/&gt;&lt;br/&gt;Recently I was benchmarking the generated code efficiency for my C compiler&lt;br/&gt;antcc, and I noticed something concerning in a simple Lua benchmark:&lt;br/&gt;benchmark opt runtime&lt;br/&gt;lua 1.048s&lt;br/&gt;lua -O1 1.077s&lt;br/&gt;lua -O2 1.410s&lt;br/&gt;The &amp;#34;optimized&amp;#34; builds were actually slower. Now, my compiler doesn&amp;#39;t implement&lt;br/&gt;many optimizations at the moment so I wouldn&amp;#39;t expect a huge improvement, but&lt;br/&gt;being 1.4x slower should definitely not happen.[1] One of the optimizations&lt;br/&gt;that antcc does have is inlining, which generally speaking is one of the most&lt;br/&gt;impactful compiler optimizations and it usually improves benchmarks all around.&lt;br/&gt;Inlining?&lt;br/&gt;So my first thought was, okay my inlining heuristics and thresholds are very simple&lt;br/&gt;and not really tuned, so perhaps it&amp;#39;s being too aggressive and blowing up the code&lt;br/&gt;size, leading to very bad instruction cache thrashing. If I disabled inlining, the pessimization&lt;br/&gt;seemed to go away, which could support that theory. That&amp;#39;s not a very satisfactory&lt;br/&gt;explanation though, and I would like to fix it, so I looked into it more.&lt;br/&gt;The main culprit was in the function luaV_execute, whose code blew up to&lt;br/&gt;~43KB with -O2, ~2.4x bigger than the -O1 build. Now, this function&lt;br/&gt;is the main bytecode interpreter loop for the Lua VM, and these are one of the&lt;br/&gt;worst kinds of functions for a compiler to deal with: it&amp;#39;s a huge function with&lt;br/&gt;a hot loop, many cases/control flow branches, distinct hot/cold paths nested in&lt;br/&gt;the graph where a wrong optimizer or register-allocation decision can pessimize&lt;br/&gt;the whole loop, etc...[2] So the baseline function is a tricky one, but it&lt;br/&gt;doesn&amp;#39;t explain this.&lt;br/&gt;Tweaking the inlining thresholds didn&amp;#39;t seem to help: either few things got&lt;br/&gt;inlined (like -O1) and binary size &amp;amp; performance stayed roughly the same, or&lt;br/&gt;small functions start getting inlined and the pessimization happens. In&lt;br/&gt;particular, these functions were getting inlined (some in multiple instances):&lt;br/&gt;floatforloop, lessequalothers, lessthanothers, luaV_idiv, luaV_mod,&lt;br/&gt;luaV_modf, luaV_shiftl, luaV_tointegerns, plus some of their respective&lt;br/&gt;callees. In the Lua source code link above you can see those are all pretty&lt;br/&gt;small simple functions.&lt;br/&gt;Looking at the final codegen is not that helpful, but examining the IR[3]&lt;br/&gt;revealed something interesting: after all optimizations, the -O2 IR was only&lt;br/&gt;11% bigger than -O1. How could that be amplified to a 2.4x difference in&lt;br/&gt;the binary?&lt;br/&gt;Code generation?&lt;br/&gt;The final passes that run after optimizations and before code generation are&lt;br/&gt;instruction selection and register allocation. Instruction selection&lt;br/&gt;mostly concerns itself with ensuring instruction operands are ready to be&lt;br/&gt;lowered into machine code, but keeps temporaries as virtual registers. For&lt;br/&gt;example, for x86-64, it models memory operands and will turn a sequence of IR&lt;br/&gt;instructions like:&lt;br/&gt;;; int *ptr, res;&lt;br/&gt;i64 %t2 = shl %index, 2&lt;br/&gt;ptr %t3 = add %ptr, %t2&lt;br/&gt;;; res = ptr[index]&lt;br/&gt;i32 %res = loads32 %t3&lt;br/&gt;into&lt;br/&gt;i32 %res = loads32 addr [%ptr &#43; %index * 4]&lt;br/&gt;It does other things like introducing register constraints for certain operations, but anyway&lt;br/&gt;because it only expands into small sequences of instructions like this, it couldn&amp;#39;t be the&lt;br/&gt;cause.&lt;br/&gt;Register allocation&lt;br/&gt;Register allocation is the process of mapping virtual temporaries to the target architecture&amp;#39;s&lt;br/&gt;physical registers. For the previous snippet, that might result in&lt;br/&gt;i32 RAX = loads32 addr [RSI &#43; RDX * 4]&lt;br/&gt;which is compiled to the x86 instruction mov eax, DWORD PTR [rsi &#43; rdx * 4].&lt;br/&gt;The problem is that there are infinite virtual temporaries but a small number of registers,&lt;br/&gt;so you have to choose which variables get which registers and when there are not enough&lt;br/&gt;registers for all of them, some of them have to go on the stack, which is called spilling.&lt;br/&gt;Most operations require values to be in registers, so spilled variables&lt;br/&gt;often have to be moved back and forth between registers. Or even worse, they might have&lt;br/&gt;to be moved between two different stack slots in some situations.&lt;br/&gt;antcc has a basic implementation of linear scan regalloc. If there is&lt;br/&gt;a lot of register pressure and many temporaries get spilled, potentially a lot&lt;br/&gt;of instructions can be created just to shuffle values between stack and&lt;br/&gt;registers.&lt;br/&gt;And indeed, comparing relative IR size increase before and after regalloc at -O2, the &#43;11%&lt;br/&gt;difference was turning into &#43;70% blowup after regalloc... looking at the disassembly,&lt;br/&gt;&amp;gt;40% of instructions in luaV_execute were just stack⇄register movs. That&amp;#39;s really bad,&lt;br/&gt;something being inlined must be resulting in this dreadful register pressure and spilling,&lt;br/&gt;but it&amp;#39;s not obvious what.&lt;br/&gt;So let&amp;#39;s examine the pre-regalloc IR:&lt;br/&gt;&amp;lt;&amp;lt; Before regalloc &amp;gt;&amp;gt;&lt;br/&gt;function luaV_execute : void(struct lua_State *, struct CallInfo *)&lt;br/&gt;abi: (RDI, RSI)&lt;br/&gt;....&lt;br/&gt;ptr %4613 = add %0, 56&lt;br/&gt;ptr %5340 = add %0, 40&lt;br/&gt;b @2&lt;br/&gt;@2: ; preds: @1, @892 ; loop depth: 1&lt;br/&gt;i64 %4489 = phi @1 undef, @892 %3124&lt;br/&gt;i64 %3030 = phi @1 undef, @892 %3065&lt;br/&gt;i64 %4636 = phi @1 undef, @892 %2287&lt;br/&gt;i64 %4764 = phi @1 undef, @892 %3869&lt;br/&gt;i64 %2874 = phi @1 undef, @892 %3929&lt;br/&gt;i64 %2694 = phi @1 undef, @892 %3981&lt;br/&gt;i64 %4872 = phi @1 undef, @892 %3804&lt;br/&gt;i64 %2297 = phi @1 undef, @892 %3497&lt;br/&gt;i64 %5062 = phi @1 undef, @892 %3158&lt;br/&gt;i64 %2208 = phi @1 undef, @892 %3145&lt;br/&gt;i64 %2161 = phi @1 undef, @892 %4191&lt;br/&gt;i64 %5275 = phi @1 undef, @892 %4225&lt;br/&gt;i64 %5290 = phi @1 undef, @892 %4252&lt;br/&gt;i64 %1791 = phi @1 undef, @892 %4370&lt;br/&gt;i64 %3813 = phi @1 undef, @892 %3195&lt;br/&gt;i64 %3807 = phi @1 undef, @892 %4403&lt;br/&gt;....&lt;br/&gt;Oh. That looks strange.&lt;br/&gt;SSA and phis&lt;br/&gt;My compiler like most modern backends uses a Static Single-Assignment representation.&lt;br/&gt;In this form, straight-line code such as (I&amp;#39;m using C-like pseudocode to simplify):&lt;br/&gt;x = a &#43; b&lt;br/&gt;x = x &#43; y # reassigning x&lt;br/&gt;use(x)&lt;br/&gt;Becomes:&lt;br/&gt;x1 = a &#43; b&lt;br/&gt;x2 = x1 &#43; y # fresh definition&lt;br/&gt;use(x2)&lt;br/&gt;That is, there are no variable mutation instructions. The reason this&lt;br/&gt;representation is great for compilers is that it simplifies all kinds of&lt;br/&gt;data-flow analysis and permits some more powerful optimizations than the&lt;br/&gt;non-SSA counterpart.&lt;br/&gt;With control flow, data flow merges are explicit in the IR:&lt;br/&gt;if (cond) {&lt;br/&gt;x = a &#43; 1&lt;br/&gt;} else {&lt;br/&gt;x = b - 1&lt;br/&gt;}&lt;br/&gt;use(x)&lt;br/&gt;In SSA form is represented as:&lt;br/&gt;if (cond) B0: {&lt;br/&gt;x1 = a &#43; 1&lt;br/&gt;} else B1: {&lt;br/&gt;x2 = b - 1&lt;br/&gt;}&lt;br/&gt;x3 = phi(B0: x1, B1: x2)&lt;br/&gt;use(x3)&lt;br/&gt;This magic &amp;#34;phi&amp;#34; instruction[4] has as its result the value of x1 or x2 depending on which&lt;br/&gt;control flow branch was taken. Same approach can be applied for variables modified in loops:&lt;br/&gt;i = 0&lt;br/&gt;while (cond) {&lt;br/&gt;use(i)&lt;br/&gt;i &#43;= 1&lt;br/&gt;}&lt;br/&gt;in SSA:&lt;br/&gt;i0 = 0&lt;br/&gt;Entry: while (cond) {&lt;br/&gt;LoopBegin:&lt;br/&gt;i_l = phi(Entry: i0, LoopEnd: i_n)&lt;br/&gt;use(i_l)&lt;br/&gt;i_n = i_l &#43; 1&lt;br/&gt;LoopEnd:&lt;br/&gt;}&lt;br/&gt;Textually, it looks like the phi function references i_n before its definition, but&lt;br/&gt;importantly that value is only taken in the loop back-edge (LoopEnd -&amp;gt; LoopBegin),&lt;br/&gt;and at that point the value of i_n is available.&lt;br/&gt;A key thing to realize here is that this i_l temporary, being a loop phi, is live&lt;br/&gt;across the whole loop, since its value is used to compute the next iteration.&lt;br/&gt;Going back to that IR dump from Lua:&lt;br/&gt;@2: ; preds: @1, @892 ; loop depth: 1&lt;br/&gt;i64 %4489 = phi @1 undef, @892 %3124&lt;br/&gt;i64 %3030 = phi @1 undef, @892 %3065&lt;br/&gt;...&lt;br/&gt;Ignore the undefs for now, and notice the ; preds line indicating the&lt;br/&gt;predecessors of that block: @1, the beginning of the function, and @892,&lt;br/&gt;hundreds of blocks later: the end of the main interpreter loop. Meaning, we&lt;br/&gt;have 16 phis that are live across the whole loop. This is what is creating a&lt;br/&gt;lot of register pressure, wrecking register allocation and resulting in that&lt;br/&gt;big code size blowup from stack spilling traffic. So, what are these phis,&lt;br/&gt;where do they come from, and why the undefs...&lt;br/&gt;mem2reg&lt;br/&gt;C programs don&amp;#39;t start in SSA form, so they have to go through a transformation&lt;br/&gt;pass. I borrowed the terminology and design from clang/LLVM in my compiler:&lt;br/&gt;Variables always start as virtual stack slots, defined with an alloca pseudo-instruction.[5]&lt;br/&gt;Since local variables in C can have their address taken[6], this&lt;br/&gt;directly models the C abstract machine&amp;#39;s definition of automatic variables: a physical addressable stack slot that can be read from&lt;br/&gt;or written to.&lt;br/&gt;Variable reads turn into memory load instructions, and writes turn into store instructions.&lt;br/&gt;Later, the mem2reg pass collects stack allocas whose address&lt;br/&gt;does not escape (meaning they are only used for direct reads and writes&lt;br/&gt;that the compiler can observe), and &amp;#34;promotes&amp;#34; them into SSA temporaries,&lt;br/&gt;inserting phi instructions where necessary.&lt;br/&gt;So C code like:&lt;br/&gt;int x;&lt;br/&gt;x = foo();&lt;br/&gt;...&lt;br/&gt;if (x &amp;lt; 0) { x = -x; }&lt;br/&gt;return x;&lt;br/&gt;Initially generates an IR that looks like:&lt;br/&gt;@1:&lt;br/&gt;ptr %x = alloca 4 bytes&lt;br/&gt;i32 %t1 = call foo()&lt;br/&gt;store i32 %x, %t1&lt;br/&gt;...&lt;br/&gt;i32 %t2 = load i32 %x&lt;br/&gt;bool %cond = lth %t2, 0&lt;br/&gt;br %cond, true -&amp;gt; @2, false -&amp;gt; @3&lt;br/&gt;@2:&lt;br/&gt;i32 %t3 = load i32 %x&lt;br/&gt;i32 %t4 = neg %t3&lt;br/&gt;store i32 %x, %t4&lt;br/&gt;b @3&lt;br/&gt;@3:&lt;br/&gt;i32 %t5 = load i32 %x&lt;br/&gt;ret %t5&lt;br/&gt;And mem2reg turns it to:&lt;br/&gt;@1:&lt;br/&gt;i32 %x1 = call foo()&lt;br/&gt;...&lt;br/&gt;bool %cond = lth %x1, 0&lt;br/&gt;br %cond, true -&amp;gt; @2, false -&amp;gt; @3&lt;br/&gt;@2:&lt;br/&gt;i32 %x2 = neg %x1&lt;br/&gt;b @3&lt;br/&gt;@3:&lt;br/&gt;i32 %x3 = phi @1 %x1, @2 %x2&lt;br/&gt;ret %x3&lt;br/&gt;I&amp;#39;m glossing over the trickiest part of this, which is knowing when and where&lt;br/&gt;to insert these phi instructions in the general case. There&amp;#39;s basically two&lt;br/&gt;algorithms to do it, and the one I&amp;#39;m using is from the paper &amp;#34;Simple and&lt;br/&gt;Efficient Construction of SSA Form (Braun et al, 2013)&amp;#34;. The gist of&lt;br/&gt;it is: walk the control-flow graph (CFG) in reverse post-order (RPO); when you&lt;br/&gt;see a write, memoize the current definition keyed by the block; when you see a read,&lt;br/&gt;look for that definition or query the predecessors&amp;#39; definitions recursively and&lt;br/&gt;insert phis when that lookup finds distinct definitions. Loops are trickier:&lt;br/&gt;because the back-edge predecessors haven&amp;#39;t been visited yet, you have to insert&lt;br/&gt;a provisional phi and resolve it later.&lt;br/&gt;Something about C that almost every language that came after it removed as a Bad Idea is&lt;br/&gt;the ability to leave variables uninitialized. For example:&lt;br/&gt;int x;&lt;br/&gt;if (foo()) {&lt;br/&gt;int y = bar();&lt;br/&gt;x = y;&lt;br/&gt;}&lt;br/&gt;use(x);&lt;br/&gt;After the control flow merge, x is phi(uninitialized, y). That is what&lt;br/&gt;the undef in my IR is. The way it&amp;#39;s introduced in my mem2reg is that if the&lt;br/&gt;recursive &amp;#34;read current definition&amp;#34; function reaches all the way to the&lt;br/&gt;beginning of the function without finding a definition, it gives it undef.&lt;br/&gt;(Later on, moves and operations on undef can be effectively optimized away.)&lt;br/&gt;How this showed up in Lua&lt;br/&gt;So was the Lua interpreter reading uninitialized variables? Well, no. Look at&lt;br/&gt;some of the affected code from the interpreter function:&lt;br/&gt;vmcase(OP_SHRI) {&lt;br/&gt;TValue *rb = vRB(i);&lt;br/&gt;int ic = GETARG_sC(i);&lt;br/&gt;lua_Integer ib;&lt;br/&gt;if (tointegerns(rb, &amp;amp;ib)) {&lt;br/&gt;pc&#43;&#43;; setivalue(s2v(ra), luaV_shiftl(ib, -ic));&lt;br/&gt;}&lt;br/&gt;vmbreak;&lt;br/&gt;}&lt;br/&gt;// several other instructions follow that same template&lt;br/&gt;The key is the tointegerns macro, which in turn calls the function luaV_tointegerns:&lt;br/&gt;int luaV_tointegerns (const TValue *obj, lua_Integer *p, F2Imod mode) {&lt;br/&gt;if (ttisfloat(obj))&lt;br/&gt;return luaV_flttointeger(fltvalue(obj), p, mode); // &amp;lt;- this gets inlined&lt;br/&gt;else if (ttisinteger(obj)) {&lt;br/&gt;*p = ivalue(obj);&lt;br/&gt;return 1;&lt;br/&gt;}&lt;br/&gt;else&lt;br/&gt;return 0;&lt;br/&gt;}&lt;br/&gt;In -O2, this function and luaV_flttointeger get inlined, resulting in an&lt;br/&gt;expansion like this (simplified):&lt;br/&gt;vmcase(OP_SHRI) {&lt;br/&gt;...&lt;br/&gt;lua_Integer ib; // &amp;lt;--- declaration&lt;br/&gt;int tointegerns_ret;&lt;br/&gt;if (ttisinteger(rb)) {&lt;br/&gt;ib = ivalue(rb); // &amp;lt;--- write&lt;br/&gt;tointegerns_ret = 1;&lt;br/&gt;} else if (ttisfloat(rb)) {&lt;br/&gt;...&lt;br/&gt;ib = (lua_Integer)fvalue(f); // &amp;lt;--- write&lt;br/&gt;tointegerns_ret = 1;&lt;br/&gt;} else {&lt;br/&gt;tointegerns_ret = 0;&lt;br/&gt;}&lt;br/&gt;if (tointegerns_ret) {&lt;br/&gt;... (use ib)&lt;br/&gt;}&lt;br/&gt;vmbreak;&lt;br/&gt;}&lt;br/&gt;mem2reg is always ran after every inlining pass. The undef in question&lt;br/&gt;comes from the variable ib which I&amp;#39;ve marked. You can see that the local&lt;br/&gt;variable is initialized in every path where the inlined function returns 1,&lt;br/&gt;but mem2reg cannot correlate that with the later if (tointegerns_ret) guard,&lt;br/&gt;so it sees a possibly uninitialized read.&lt;br/&gt;But that does not tell the whole story! It explains the undef, but not why&lt;br/&gt;the phi is live across the whole loop. After all, this local is scoped to a&lt;br/&gt;block inside a branch of the loop, it does not live across loop iterations...&lt;br/&gt;The problem&lt;br/&gt;Eventually I figured out the root cause: I was treating the idea of &amp;#34;variables&lt;br/&gt;start as uninitialized&amp;#34; too literally in mem2reg. When it encountered an&lt;br/&gt;alloca, it didn&amp;#39;t give it a definition for the current block. This sounds&lt;br/&gt;correct, and the recursive lookup in the algorithm terminates at the function&lt;br/&gt;entry with an undef. But recall the catch with loops being that when you&lt;br/&gt;encounter a backedge, you don&amp;#39;t know if there&amp;#39;s a later definition, because you&lt;br/&gt;haven&amp;#39;t visited the whole loop yet, so you insert a provisional phi...&lt;br/&gt;In essence, my mem2reg was behaving as if code like:&lt;br/&gt;int foo() {&lt;br/&gt;while (...) {&lt;br/&gt;int x;&lt;br/&gt;if (...) {&lt;br/&gt;x = bar();&lt;br/&gt;}&lt;br/&gt;use(x);&lt;br/&gt;}&lt;br/&gt;}&lt;br/&gt;was actually&lt;br/&gt;int foo() {&lt;br/&gt;int x;&lt;br/&gt;while (...) {&lt;br/&gt;if (...) {&lt;br/&gt;x = bar();&lt;br/&gt;}&lt;br/&gt;use(x);&lt;br/&gt;}&lt;br/&gt;}&lt;br/&gt;In the 2nd function, the value of x is kept across iterations, whereas in the&lt;br/&gt;1st, the value of x is a &amp;#39;fresh&amp;#39; uninitialized every time the if branch is&lt;br/&gt;not taken... This is not a miscompilation: as far as C is concerned, this is&lt;br/&gt;fine because uninitialized variables&amp;#39; values are not legally usable anyway. And&lt;br/&gt;if you frame it in terms of stack-allocated variables, it&amp;#39;s a sensible&lt;br/&gt;approach: the slot for x is fixed for the whole function and doesn&amp;#39;t get&lt;br/&gt;reset between loop iterations, so it keeps the previous iteration&amp;#39;s whatever&lt;br/&gt;it was and it doesn&amp;#39;t matter. But the problem is that in the context of SSA&lt;br/&gt;form, this generates something like:&lt;br/&gt;while (...) {&lt;br/&gt;%x = phi(undef, %x2)&lt;br/&gt;if (...) {&lt;br/&gt;%x1 = bar();&lt;br/&gt;}&lt;br/&gt;%x2 = phi(%x, %x1)&lt;br/&gt;use(%x2);&lt;br/&gt;}&lt;br/&gt;And it is these loop phis that result in the pathological register allocation.&lt;br/&gt;The fix was one line:&lt;br/&gt;--- a/src/ir_mem2reg.c&lt;br/&gt;&#43;&#43;&#43; b/src/ir_mem2reg.c&lt;br/&gt;@@ -244,6 &#43;244,8 @@ mem2reg(Function *fn) {&lt;br/&gt;v-&amp;gt;cls = k;&lt;br/&gt;v-&amp;gt;i = var;&lt;br/&gt;sb.vars[var] = v;&lt;br/&gt;&#43; v-&amp;gt;curdefs[blk-&amp;gt;id] = UNDREF;&lt;br/&gt;} else {&lt;br/&gt;sb.vars[var] = DEAD;&lt;br/&gt;}&lt;br/&gt;Initializing the var as undefined (a nice oxymoron) at the place of the&lt;br/&gt;alloca ensures that the recursive lookup doesn&amp;#39;t cross loop backedges and&lt;br/&gt;insert phis in the loop header. This is enough to fix this case, and that&lt;br/&gt;change made the mysterious pessimization disappear.&lt;br/&gt;Closing notes&lt;br/&gt;This problem can still bite for C code that is old-style declaring all&lt;br/&gt;variables at the top of the function. It&amp;#39;s possible to fix this general case by&lt;br/&gt;using Tarjan&amp;#39;s algorithm to find the graph SCCs of the phi data-flow to&lt;br/&gt;detect cycles, and making use of these undef semantics when classifying&lt;br/&gt;phis as redundant; but I haven&amp;#39;t implemented that yet.&lt;br/&gt;Jump threading an inlined function used directly as a branch condition, or more&lt;br/&gt;generally SSA-aware phi jump threading, could&amp;#39;ve also fixed the branch&lt;br/&gt;correlation issue that led to mem2reg not seeing the dominating write of&lt;br/&gt;those variables and introducing undefs.&lt;br/&gt;I made use of AI agents to help debug this issue. It saved me a lot of time in&lt;br/&gt;investigating, filtering through disassemblies and IR dumps, running the compiler&lt;br/&gt;with different inlining thresholds and measuring, etc; but it was not good at&lt;br/&gt;trying to find the root cause. It proposed probably around 10 convoluted&lt;br/&gt;&amp;#34;fixes&amp;#34; before I eventually arrived at the conclusion myself. Still &amp;lt;$1 well&lt;br/&gt;spent for the sake of reducing tedium.&lt;br/&gt;Benchmarks&lt;br/&gt;Lua 5.4.7, bench.lua, AMD Ryzen 5 5600G. Take&lt;br/&gt;these with a grain of salt.&lt;br/&gt;Pre-fix:&lt;br/&gt;antcc was worse than even tcc, but that&amp;#39;s mainly because my frontend was&lt;br/&gt;lowering switch statements to linear if-else chains. I implemented a simple&lt;br/&gt;binary search codegen, which was one of those things that I just hadn&amp;#39;t gotten&lt;br/&gt;around to yet; and a few other minor optimizations; after this fix.&lt;br/&gt;Current numbers:&lt;br/&gt;Do note that antcc performs mem2reg by default when passed no optimization&lt;br/&gt;flags, unlike gcc. It might seem counterintuitive, but from my testing, running&lt;br/&gt;mem2reg improves compile times too; presumably due to the reduced number of&lt;br/&gt;stack store/load instructions for the backend to churn through.&lt;br/&gt;And compile times:&lt;br/&gt;I omitted the bars for clang because they are all very similar to gcc.&lt;br/&gt;The only difference between -O1 and -O2 is in inlining thresholds and&lt;br/&gt;-O2 running some optimization passes more than once. ↩&lt;br/&gt;This is why LuaJIT&amp;#39;s interpreter in hand-written assembly beats almost every other&lt;br/&gt;bytecode interpreter, and it&amp;#39;s not just about the while-switch loop ↩&lt;br/&gt;Intermediate representation ↩&lt;br/&gt;variously known as phi function, φ, /faɪ/, /fi/, phony instruction ↩&lt;br/&gt;Not to be confused with the dynamic alloca() function. ↩&lt;br/&gt;The register storage class exists, but it doesn&amp;#39;t really matter in&lt;br/&gt;modern architectures so you (almost) never see it used. ↩&lt;br/&gt;&lt;br/&gt;&lt;a href=&#34;https://386s.org/w/lua-antcc-pessimization/&#34;&gt;https://386s.org/w/lua-antcc-pessimization/&lt;/a&gt;&lt;br/&gt; &lt;img src=&#34;https://386s.org/asset/antcc-lua-bench-1.png&#34;&gt; &lt;br/&gt; &lt;img src=&#34;https://386s.org/asset/antcc-lua-bench-2.png&#34;&gt; &lt;br/&gt; &lt;img src=&#34;https://386s.org/asset/antcc-lua-compiletime.png&#34;&gt; 
    </content>
    <updated>2026-10-16T03:30:27Z</updated>
  </entry>

</feed>