The method and the figures below state the compiler as of release 7.7.7.

Method first, because a performance number without a method is a marketing claim.

How we measure

Benchmarks that compare against C have a matched C reference implementing the same algorithm — not a different algorithm that happens to produce the same output. Not every kernel in the suite currently has a live C counterpart; the comparison set is smaller than the benchmark set.

Every release is measured natively on both architectures. The release’s benchmark run builds every kernel with the packaged compiler at O2, every Mica safety check on, beside its C twin at gcc -O2, on one documented machine per architecture that anyone can rent, and the facts page renders its tables. There the wall clock leads, because it is what a person waits for, with the retired instructions and the peak resident memory beside it; how we verify says what the run validates before it writes a single number.

The figure sets on this page are native arm64 measurements, each from the record the line at the end of the page names. Instruction counts have differed between the two architectures by up to 50% per kernel, so no number here should be read as architecture-independent.

They count retired instructions under Valgrind. Retired instruction counts are deterministic and reproducible on any machine, so a result you read here is one you can reproduce rather than approximate. The same count guards every change: every benchmark kernel’s count is pinned, the ladder measures it with each compiler it builds, and a kernel over its pin stops the change before it merges.

What the optimizer achieved

An optimizer package, measured across the whole benchmark corpus before and after it:

Corpus total4,624M → 2,453M retired instructions
Corpus reduction46.9%
BubbleSort−66%
MatrixMultiply−52%
ArraySweep−54%

Bounds-check elimination contributes to the MatrixMultiply improvement: every guard in the kernel is proven away. The array index domain is known to the type system, so value-range propagation can prove indices in range and remove the check rather than execute it.

What optimized code does

At O1 and O2 the code generator works from what the program states:

  • A value belongs to its type. A conversion into a subrange, an enumeration, a boolean or a unicode scalar is checked on every tier, and so is every such value that enters from C, so optimized code reads a declared range as a proof. A value of a subrange, an enumeration or an error domain lives in a register like the integer it is stored as, not in a frame slot stored and loaded around every call, and a literal stored into one is carried as the constant it is. After a proven bounds check, an array index is read from its variable, and no copy of it is stored.
  • Constants cost nothing at run time. An integer constant is the immediate of the instruction that reads it, on both architectures, and occupies no register or stack slot; a bitwise and, or or xor with a literal takes it as the instruction’s immediate on both processors, and a constant a call receives, a runtime routine’s or an ordinary routine’s, is built in its argument register at the call. A constant sum, difference or product is folded, wrapped at its type’s width, a variable every branch sets to one constant holds it after the branches meet, an if whose arms fold away leaves no test behind, and an operation whose result nothing reads leaves no instruction. A small local array read only at fixed places lives in registers, so three scores and their sum fold to one mov esi, 60, and a counted loop whose constant bounds never let its body run leaves no code.
  • Division by a constant is a multiplication. An integer division or remainder by a literal that is not a power of two multiplies by the divisor’s reciprocal on both processors, as GCC does, every quotient and remainder the one the divider gives, the smallest signed value included. A divisor of minus one keeps its guard, and a signed value the optimizer knows is never negative takes the shorter unsigned sequence. On AArch64 a remainder by a literal in a loop keeps its divisor in a register, a product that feeds an addition or a subtraction is one madd or msub, and a factor a loop multiplies by is built once ahead of the loop. The debug tier keeps the divide instruction.
  • A store nothing reads is removed. A store into a frame slot, an array element or a record field of the routine’s frame that no path reads before it is written again or the routine returns goes, in routines with run-time checks too and where a jump lands on the store, and a nil guard the program’s own test already decided goes as well.
  • Small routines disappear. A routine that lends a field behind its pointer parameter is inlined at O2 and tests the pointer for nil once, and a routine inlined at every call keeps no copy of its own.
  • Prologues and alignment follow GCC. An AArch64 prologue saves its frame record and its callee-saved registers in pairs with stp, and its epilogue restores them with ldp, every pair still described for gdb, perf and every unwinder. A parameter’s first read takes the register it arrived in on AArch64, and a parameter that waits across a call is stored straight from that register on x86-64. Optimized builds start every function on a 16-byte boundary on x86-64, and every function and loop head on a 32-byte boundary on AArch64 where at most 15 bytes of padding reach it, so unrelated code does not move a kernel’s time.
  • A walk over a utf-8 string stays linear. A read of s[i] in any while, repeat or counted loop, whatever its index expression, walks from the nearest place it already knows instead of the string’s first byte, at every tier, whether the string is the routine’s own, a program variable or an enclosing routine’s, so a walk over a long string takes time in proportion to its length.
  • Floating-point code does not wait without reason. On x86-64 a conversion from an integer, a conversion between float widths and a square root start at once instead of waiting on instructions they do not depend on, a float copy between registers writes the whole register, and a float literal is read from the constant pool by the instruction that uses it, after a call and in a loop that calls too.
  • Arrays far from zero stay cheap. An array or a set whose lower bound lies far from zero, out to the least int64, is rebased by one instruction on AArch64.
  • Small cells are cleared in place. A small zeroed heap cell is cleared by its own few stores, without a call to memset, so a program that allocates many small values runs faster.
  • A program carries what it uses. A program that runs tasks but uses no stream and no select carries none of the stream ring’s code, one whose tasks on processes read no stream carries none of a worker’s reading code, and one that arms no deadline links none of the deadline’s code. On a pool of one carrier, making a waiting task ready costs no system call while no carrier sleeps, so streams and selects pass values faster there, and a pull reads its running task once behind each wait, so a value through a full or an empty ring takes fewer instructions. A program carries no dynamic symbol for a runtime routine it never links, so the dynamic loader looks none of them up at its start.

Vectorization and carriers

Lane-level SIMD, f642.33x over scalar
Lane-level SIMD, f324.03x over scalar
Data-parallel, 8 coresup to 4.0x

All of it bit-identical to the serial result at every carrier count — see determinism.

Compile throughput

A 100,000-line program compiles fully optimized in about 8.57 seconds as part of the gate run, and about 5.4 seconds when it is the only thing running.

Where gcc still leads

On general-purpose code, gcc -O2 is ahead of us, and we are not going to suggest otherwise. Decades of investment in an optimizer is a real thing.

Specific favourable numeric loops reach parity, and the facts page’s tables mark each kernel the release’s run measured in the parity class, per architecture; a kernel that stands there on one architecture need not on the other, which is exactly the kind of difference the architecture caveat above is warning about. The region allocator does beat our own per-cell plug-in substantially on an allocation-bound workload, because bulk reclaim beats per-object free — that comparison is in-house and we trust it more than the cross-toolchain one.

We publish the gap rather than selecting benchmarks around it, and it is the number we work to reduce each release.

Figures from the 7.5.0 release record, 2026-11-10.