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 total | 4,624M → 2,453M retired instructions |
| Corpus reduction | 46.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,ororxorwith 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, anifwhose 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 onemov 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
maddormsub, 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 withldp, 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 anywhile,repeator 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, f64 | 2.33x over scalar |
| Lane-level SIMD, f32 | 4.03x over scalar |
| Data-parallel, 8 cores | up 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.