Benchmarks

An 18-benchmark suite covering integer, FP, memory-bound, and call-heavy workloads. Best-of-9 wall-clock time; all 18 outputs byte-identical to GCC.

Test Environment

Item Value
Host AArch64 Linux
LCCC current main — linear scan + register steal + slot coalescing + NEON/F64 loop promotion + full SSA pipeline
GCC system GCC, -O2
Timing wall clock, 9 reps, best taken
Correctness program output compared byte-for-byte against GCC

Results (LCCC / GCC -O2)

Benchmark Workload LCCC GCC Ratio
arith_loop 32-var arithmetic loop 0.034s 0.030s 1.12×
fib fib(40) recursive <0.001s 0.090s ~600× faster
matmul 256×256 FP matrix multiply 0.0025s 0.0029s 1.15× faster
qsort quicksort 1M integers 0.070s 0.071s 1.02× faster
sieve Eratosthenes 10M 0.017s 0.016s 1.06×
tce_sum tail-recursive sum(10M) 0.0001s 0.0001s 1.0×
nbody N-body FP simulation 0.185s 0.165s 1.12×
binary_trees malloc/free recursion 0.848s 0.751s 1.13×
spectral_norm FP dense loops 0.133s 0.133s 1.0× parity
mandelbrot FP inner loop 0.553s 0.491s 1.13×
hash_table pointer chasing 8.62s 7.91s 1.09×
strlen_bench string processing 0.145s 0.140s 1.04×
switch_dispatch jump tables 0.404s 0.350s 1.16×
struct_copy struct copy/field access 0.013s 0.012s 1.10×
loop_patterns reduce/transform/prefix 0.032s 0.032s 1.0× parity
fannkuch Fannkuch-Redux permutations 2.010s 1.973s 1.02×
ackermann ackermann(3,11) <0.001s 0.098s ~600× faster
bitops popcount/clz/reverse 0.089s 0.169s 1.9× faster

Geometric mean: 0.49× of GCC -O2 across all 18 — skewed by the two ~600× recursion-to-iteration wins; excluding fib and ackermann, 1.01× (statistical parity) across the remaining 16 (matmul, qsort, loop_patterns, bitops at or past parity; spectral_norm, tce_sum, fannkuch, sieve, strlen_bench within 7%).

Compile time: LCCC compiles 2–5× faster than GCC across the suite.

Where the Performance Comes From

Recursion-to-iteration (fib, ackermann — ~600×)

Detects the f(n) = f(n-1) + f(n-2) binary-recursion pattern and converts the exponential O(2ⁿ) call tree into an O(n) iterative sliding-window loop. GCC keeps the recursive calls. This is a synthetic-benchmark win — no production code uses naive recursive Fibonacci — but it demonstrates the pattern-matching infrastructure. Verified by a CI test computing fib(90), impossible without the O(n) conversion.

Register steal for hot loop values (fannkuch, spectral_norm)

The linear scan assigns registers in interval-start order and never evicts, so cold function-spanning values (array bases, globals) used to win every callee-saved register simply by starting first — leaving hot inner-loop-carried values (IVs, accumulators, carried pointers) stack-homed. The post-scan steal picks, for each hot loop-carried phi value the scan missed, the register whose conflicting holders have the coldest loop-weighted use count, and fully deallocates those holders to the stack. Safe by construction (whole-interval deallocation, no range splitting), and a no-op when the scan already housed the hot values.

fannkuch: 2.69× → 1.85×. spectral_norm: 1.63× → 1.19×.

Loop-backedge slot coalescing (arith_loop)

A spilled loop-carried variable with a constant initializer used to keep a “double slot”: the phi dest and its backedge update each got a stack slot, with a ldr+str copy between them on every iteration. The slot coalescer aliases the update into the phi dest’s slot when the phi-coalesce detector proves the old value is dead after the update is defined — the backedge copy becomes a same-slot no-op. arith_loop: 2.09× → 1.15× (22 instructions/iteration gone).

NEON vectorization and F64 loop promotion (matmul, loop_patterns, FP loops)

The inner loop of matmul is auto-vectorized with NEON (F64x2/I32x4 register-resident vector ops, fused fmadd), F64 loop accumulators are promoted into dedicated FP registers (d24–d31), and FP constants are hoisted out of loops. matmul runs 1.15× faster than GCC -O2. Reduction vectorization covers plain and widening sums (sadalp), dot products (smlal/smlal2 with split accumulators), conditional sums (smax clamp), and max reductions (smax/smaxv) — loop_patterns went from 1.83× to parity.

FP recurrence scheduling and coalescing (mandelbrot, nbody)

Loop-phi anti-dependency splitting moves the old-value copy off the recurrence’s serial chain (the exact shape GCC -O2 emits), and reverse FP phi coalescing gives copy-web accumulators the backedge source’s register when conflict-free — the backedge fmov dies. Backedge PRE then carries a loop-bottom expression (mandelbrot’s zr²) into the matching loop-top use through a new phi, with fusion-aware profitability so the multiply absorbed into an fmadd stays fused — one fmul fewer per iteration, the shape GCC reaches via loop rotation + CSE. mandelbrot 1.32× → 1.13×, nbody 1.31× → 1.12×.

Conditional-increment fusion (sieve)

sieve’s prime-count loop is dispatch/branch-throughput bound, not dependency bound — and there, instruction count is everything. A csinc peephole folds the increment-feeding-csel pair that if-conversion produces for if (sieve[i]) count++ into one instruction, cutting the loop from 12 instructions (~2 cycles/iter) toward GCC’s 5 (~0.65 cycles/iter). sieve 1.16× → 1.06×.

Small-leaf caller-saved allocation and shrink-wrapping (qsort, binary_trees)

Call-free functions up to a few dozen IR instructions scan the caller-saved pool (x4–x8, x13–x14) first: no prologue save storm for hot callbacks — qsort’s cmp callback dropped from 18 instructions plus five save/restore pairs to 7 instructions, putting qsort at parity with GCC. Callee-saves are also shrink-wrapped past clean early returns, so binary_trees’ null-child path skips the restore storm.

Bit manipulation (bitops — 1.8× faster than GCC)

popcount/clz/ctz/bit-reverse lower directly to single AArch64 instructions (cnt+uaddlv, clz, rbit), beating GCC’s instruction selection on this workload.

Tail-call elimination (tce_sum)

Self-recursive accumulator functions become counted loops — identical shape to GCC’s output. (GCC constant-folds sum(10000000, 0) entirely, so both are ~0s here.)

Real-World: SQLite 3.45

LCCC compiles and fully runs the SQLite amalgamation (260K lines, single file): CREATE TABLE, INSERT/UPDATE/DELETE, SELECT with WHERE/ORDER BY/LIMIT, JOINs, correlated and uncorrelated subqueries, GROUP BY/HAVING, aggregates, UNION ALL, transactions, and prepared statements.

Running the Benchmarks

# Full 18-benchmark suite (performance + correctness + size)
python3 tests/benchmark/run_benchmarks.py --reps 5

# A subset
python3 tests/benchmark/run_benchmarks.py --only fannkuch --only arith_loop --reps 8

# Compile a benchmark manually for disassembly comparison
GCC_INC="-I$(gcc -print-file-name=include)"
./target/release/lccc-arm $GCC_INC -O2 -o /tmp/arith_lccc tests/benchmark/programs/arith_loop.c
objdump -d /tmp/arith_lccc