> blog // measurement

A million lines, eight compilers

October 2, 2026 · measured with benchmarks/scale/generate.py (sc1m)

Neper ships a scale benchmark that generates a million-line program and times the compiler on it. We ported that program to seven other languages and ran the same race in each: compile it, run it, check the answer. Neper finished with the fastest program and the smallest binary. Dart compiled it fastest. Zig never finished at all.

How we tested

The workload is sc1m, one of the standard sizes in benchmarks/baseline/, generated with the fixed seed so anyone can reproduce it byte for byte:

$ python benchmarks/scale/generate.py sc1m --modules 1000 --lines 1000000 --seed 1
# modules 1000, functions 82595, lines 920554; expected 108474335

It is a DAG of 1,000 modules holding 82,595 functions. Each function loops over a small integer range, branches on the running value, mixes in a field of a record, and calls one function from an earlier module, so calls cross module boundaries and nothing is dead. main folds every module's first function through a checksum and prints it. The generator also computes the answer independently in Python, so every binary is checked against expected.txt (108474335), not against each other.

Neper compiled that tree as it is, through the self-hosted compiler:

$ neper build src/main.e --arena 4g --output neper-4g.exe
# the compiler itself, with room to hold the program: 24 s
$ neper-4g emit-executable src/main.e REPO x64 windows sc1m.exe --release --unchecked --cpu x64-v3 -j 8

The other seven languages got flattened single-file ports of the same functions, constants, call graph and checksum (about 911,000 lines each; Python 829,000, denser syntax), produced by benchmarks/scale/port_sc1m.py. Every port's output was verified equal to expected.txt before its time counted. Four adaptations were needed to keep semantics exact:

The toolchains, on Windows x64 (16 logical cores, 16 GB RAM): Dart 3.12.2 (dart compile exe), Go 1.27.1 (go build), rustc 1.98.0 (-O), Zig 0.16.0 (build-exe -OReleaseFast), node 24.19.0 (--check), tsc 7.0.2 (es2020), CPython 3.12 (py_compile).

Two caveats before any numbers:

Results

LanguageLinesCompile (ms)Run (ms)Binary
Neper (warm, incremental)920,5545,700472.73 MiB
Neper (cold)920,55423,600472.73 MiB
Dart911,55216,0005007.8 MiB
Go911,55640,7003004.0 MiB
Rust911,55263,4004004.9 MiB
Zig 0.16.0911,554no binary after 65+ min——
JavaScript911,550400 (syntax check)40019.2 MiB source
TypeScript911,5507,8001,90024.5 MiB of JS
Python828,956300 (bytecode)6,10023.7 MiB of .pyc

Where Neper wins

Where Neper doesn't

The Zig row.

Zig 0.16.0 was given the file three times (20, 30 and 15 minutes) and never produced a binary. The monitored attempt pins one core at ~100% with a flat ~250 MB working set: a single-threaded CPU-bound spin in the frontend on 82,595 functions in one file, not thrashing and not LLVM slowness. Small files compile fine, including the stdout probes. The 911,554-line port is a ready-made repro; with 0.16's throughput on this shape at effectively zero, it is also the most useful result in the table for someone.

One asymmetry, disclosed

Neper compiled the true multi-module tree with real cross-module imports. Everyone else got one flat file with prefixed names and no module resolution to do. Single-file is the easier shape, so this comparison leans away from Neper on compile time — and Neper still takes the run column by 6×. We will take that trade.

Every number above comes from the generator and the porter in the repository:

$ python benchmarks/scale/generate.py sc1m --modules 1000 --lines 1000000 --seed 1
$ python benchmarks/scale/port_sc1m.py --src sc1m/src --out sc1m/ports

Run them with your own toolchains. If your numbers disagree with ours, we want to know.