Milestone 9 — Performance engineering (plan and report)
Status: complete (2026-10-25): 9a–9f done; the acceptance results are in 9f below. Roadmap acceptance: optimisations with preconditions; a benchmark suite vs C/C++/Rust/Zig/Goose; each optimisation: equivalence tests + measured gain; published methodology; no claims without data (roadmap.md). Master prompt: §16 (optimisation strategy: each optimisation needs safety preconditions, tests, correctness checks, debugging support and performance measurements, against a correct baseline), §19 (the benchmark list, the comparisons and the metrics), §21 Milestone 9 ("implement optimization passes only after their correctness foundations exist; measure each improvement"), §22 rule 3 (no unproven performance claims).
Where the time goes today (inputs from earlier milestones)
| Evidence | What it shows |
|---|---|
C6a LZ4 (benchmarks/lz4/) | compression 0.48–0.78 of C; decompression of literal runs 0.02–0.03 of C on x86-64, because a byte loop with two checked indexes per byte stands where C has memcpy; with memcpy (scratch experiment) Nova reached 0.94 of C |
C6b fbvalid (benchmarks/fbvalid/) | equal to C with clang, 1.76× C's time with gcc 12, cause not established |
| risk P-7 | checked Nova too slow for kernel hot paths would push code back to C or to unsafe |
| M5 | placement (non-escaping boxes on the stack) is opt-in, "until M9's benchmarks decide the default" |
The C compiler already does what most of §16's list asks for on Nova's C (constant folding, dead code and dead stores, copy propagation, CSE, inlining, loop optimisation, scalar replacement), so Nova's own passes go where it cannot prove what Nova knows. That means the checks Nova inserts (bounds, overflow), where values live (placement), what does not alias (exclusive borrows), and operations that C expresses in bulk.
Plan
| Increment | Content |
|---|---|
| 9a the suite and its method | benchmarks/suite/: the 15 workloads of §19, each written in Nova, C, C++ and Rust with the same algorithm and data structures (and in Zig and Goose where the toolchain is available and the workload expressible). A runner that measures wall time (median of repeated runs), peak resident memory, allocation count and bytes (one counting malloc interposer for every language, since all of them allocate through the C library here), compile time and binary size, on the M1 and the Ryzen box. docs/benchmarks.md publishes the method; the baseline results are recorded before any optimisation. |
| 9b checked bulk operations | copyFrom and fill on SliceMut (spec first): one range check, then memcpy/memset; the LZ4 port and the suite's copy workload use them. Done (below) |
| 9c bounds-check elimination | a NIR pass that removes an index check another check, or a loop's range, already proves (a slice's length cannot change while it is borrowed). Preconditions written down; --no-bce to compare; equivalence tests: corpus, fuzz campaign, interpreter agreement. Done (below) |
| 9d overflow checks and the gcc gap | value-range elimination of overflow checks that cannot fail (loop counters bounded by a length); the cause of fbvalid's gcc gap, measured, then fixed or recorded. Done (below) |
| 9e placement by default | M5's placement analysis made the default if the suite shows a gain and every equivalence test passes; --no-placement stays. Done (below) |
| 9e′ candidates from 9a | the niche layout for Box<T>? and borrow T? (F4: one pointer, null for null; spec first, type-system §22.1), and building Box.create's argument in the heap rather than on the stack (F6); each taken only if the baseline shows it matters. Done (below) |
| 9f acceptance | the suite rerun after 9b–9e, each optimisation's gain attributed with its switch off and on, and the results published. Done (below) |
Not planned: restrict from exclusive borrows (raw pointers and C code may alias what Nova thinks is exclusive; it needs its own analysis of unsafe code first), and devirtualisation (Nova's generics are monomorphised; no dynamic dispatch exists yet).
Results
9a — what writing the suite found in Nova (2026-10-25)
Writing the fifteen workloads in Nova, next to the same programs in C, C++, Rust, Zig and Goose, showed these gaps. They are recorded here, not fixed in 9a. Each one either cost a workaround in the suite's Nova code, which is commented where it occurs, or made a Nova row slower or larger.
| # | Finding | Where it shows | Kind |
|---|---|---|---|
| F1 | last = Some(arena.alloc(Node { … })!) inside a loop is rejected with N0240 ("cannot assign to a temporary value while a temporary value is borrowed"). The same expression moved into a helper function that returns borrow(r) Node<r> is accepted | arena/main.nova (push) | borrow checker false positive (inferred: the temporary Result is still considered borrowed at the assignment) |
| F2 | borrow T? means an optional reference. A reference to an optional cannot be written, and both forms print identically in diagnostics | — | syntax and diagnostics; closed in M11 (G6: borrow (T?)) |
| F3 | Some(b), with b: borrow Box<T>, does not coerce to borrow T?; a helper fn contents(b: borrow Box<Node>) -> borrow Node => b; does | linked-list/main.nova | coercion gap |
| F4 | Box<T>? and borrow T? take a tag word plus the pointer, 16 bytes rather than 8: the niche layout of language-spec.md §2 and memory-model.md is still future work (type-system.md §22.1, c-backend.md §2). A linked-list node is 24 bytes rather than 16 | linked-list allocates 48 MB where the others allocate 32 MB; on macOS, whose allocator rounds 24 bytes up to 32, its peak heap is twice C's | layout; fixed in 9e′ (the niche layout) |
| F5 | No byte or character literals ('k'); no number formatting into a String; no if expressions; no wrapping arithmetic; no floating point | parsing, string-building ("0123456789"[d..d+1]) | expressiveness; all but floating point closed in M11 (G4, G5; milestone-11.md) |
| F6 | Box.create([0u8; 65536]) zeroes a 64 KiB stack temporary with a byte loop, passes it by value to the box helper (nvB_…(nvA65536_u8 v)), and copies it into the heap: two 64 KiB copies where C has one memset | large-objects (generated C) | code generation; fixed in 9e′ (built in place) |
| F7 | The emitter wrote type names containing ?? (as in Box<Res>??, the result of Vector<Box<Res>?>.pop()) into _Static_assert messages unescaped. ??) is a C trigraph, so clang warned and -Werror builds failed | fixed in 9a: the messages go through CMessageLiteral, which writes ? as \? (not CStringLiteral's octal escapes, which clang rejects in an unevaluated string such as a _Static_assert message); run-pass/vectors.nova covers it | backend bug |
| F8 | The expression parser did not accept ? inside generic arguments (Vector<Box<Pair>?>.create()) | fixed in 9a (grammar.md, generic arguments in expressions) | parser |
9a — the baseline (run 2026-10-25)
Ryzen 5 PRO 3600, Debian 12, Linux 6.1; the Nova tree at the 9a change (on top of 227bec9); run.py --runs 5 under nice, once with clang 18.1.8 and once with gcc 12.2.0 as the C compiler (nova, c, cpp and goose use it; Rust and Zig use their own LLVM). Load average 0.8 before, 2.1–2.3 during and after: the machine is shared, and the median spread (maximum − minimum over the median) of the five runs is 9%, with a few rows up to 55%. Differences under about 15% are within that noise. All 234 rows printed the expected checksum. Reproduce with benchmarks/suite/run.py and report.py (benchmarks.md §4). The memory, allocation and compile tables are from the clang run; with gcc they differ only in the C-compiled rows, by under 1%.
Wall time, ms, and relative to C (x86_64 Linux, clang version 18.1.8, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 367 (1.22) | 300 (1.00) | 334 (1.12) | 373 (1.25) | 365 (1.22) | 291 (0.97) | 317 (1.06) | — |
large-objects | 275 (0.98) | 282 (1.00) | 282 (1.00) | 277 (0.98) | 291 (1.03) | 297 (1.05) | 316 (1.12) | — |
stack-alloc | 575 (1.17) | 492 (1.00) | 484 (0.98) | 538 (1.09) | 645 (1.31) | 381 (0.77) | 593 (1.21) | 499 (1.01) |
heap-alloc | 379 (1.20) | 316 (1.00) | 350 (1.11) | 539 (1.71) | 588 (1.86) | 193 (0.61) | 489 (1.55) | — |
arena | 1277 (1.55) | 824 (1.00) | 927 (1.13) | 528 (0.64) | 556 (0.68) | 839 (1.02) | 794 (0.96) | 224 (0.27) |
vector-growth | 1101 (1.87) | 590 (1.00) | 769 (1.30) | 545 (0.92) | 626 (1.06) | 593 (1.01) | 635 (1.08) | 231 (0.39) |
string-building | 831 (2.37) | 351 (1.00) | 500 (1.43) | 487 (1.39) | 484 (1.38) | 361 (1.03) | 505 (1.44) | 443 (1.26) |
recursion | 497 (1.63) | 305 (1.00) | 301 (0.99) | 315 (1.03) | 459 (1.51) | 294 (0.96) | 421 (1.38) | 301 (0.99) |
linked-list | 435 (1.12) | 389 (1.00) | 506 (1.30) | 525 (1.35) | 463 (1.19) | 384 (0.99) | 397 (1.02) | 64 (0.16) |
binary-trees | 671 (1.43) | 469 (1.00) | 614 (1.31) | 554 (1.18) | 571 (1.22) | 528 (1.12) | 527 (1.12) | 94 (0.20) |
hash-map | 432 (1.02) | 424 (1.00) | 431 (1.02) | 443 (1.05) | 508 (1.20) | 391 (0.92) | 412 (0.97) | 393 (0.93) |
parsing | 224 (1.56) | 144 (1.00) | 142 (0.99) | 187 (1.30) | 214 (1.48) | 140 (0.97) | 195 (1.35) | 179 (1.24) |
memory-copy | 4372 (17.33) | 252 (1.00) | 630 (2.49) | 290 (1.15) | 263 (1.04) | 516 (2.04) | 522 (2.07) | 497 (1.97) |
numeric | 352 (1.52) | 232 (1.00) | 239 (1.03) | 229 (0.99) | 254 (1.10) | 303 (1.31) | 247 (1.07) | 231 (1.00) |
data-oriented | 196 (0.86) | 229 (1.00) | 161 (0.71) | 161 (0.70) | 137 (0.60) | 209 (0.91) | 137 (0.60) | 140 (0.61) |
Peak resident memory, MiB (x86_64 Linux, clang version 18.1.8, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 1.4 | 1.5 | 2.9 | 2.0 | 1.9 | 1.9 | 2.0 | — |
large-objects | 1.6 | 1.5 | 3.0 | 2.4 | 2.4 | 1.9 | 2.0 | — |
stack-alloc | 1.3 | 1.4 | 2.9 | 1.8 | 1.9 | 1.7 | 1.9 | 1.8 |
heap-alloc | 1.3 | 1.4 | 2.9 | 1.9 | 2.0 | 1.7 | 1.9 | — |
arena | 5.6 | 4.1 | 5.4 | 6.4 | 6.4 | 4.5 | 4.6 | 4.1 |
vector-growth | 258.7 | 155.6 | 260.4 | 156.4 | 156.5 | 155.8 | 155.9 | 155.9 |
string-building | 16.1 | 10.0 | 17.1 | 10.5 | 10.5 | 12.2 | 12.3 | 9.5 |
recursion | 1.3 | 1.5 | 2.8 | 1.9 | 1.9 | 1.6 | 1.9 | 1.9 |
linked-list | 62.3 | 62.4 | 63.9 | 62.9 | 62.9 | 62.9 | 62.9 | 25.9 |
binary-trees | 25.3 | 17.4 | 18.9 | 18.0 | 17.9 | 17.8 | 17.9 | 8.3 |
hash-map | 65.0 | 65.0 | 66.3 | 65.7 | 65.8 | 65.4 | 65.5 | 68.9 |
parsing | 33.8 | 30.2 | 36.2 | 30.8 | 31.0 | 29.8 | 30.6 | 30.8 |
memory-copy | 32.8 | 32.9 | 34.3 | 33.9 | 33.8 | 33.2 | 33.9 | 36.4 |
numeric | 40.7 | 39.0 | 42.2 | 41.3 | 41.3 | 40.9 | 41.0 | 42.5 |
data-oriented | 16.2 | 16.3 | 17.5 | 17.0 | 16.9 | 16.6 | 16.7 | 22.3 |
Allocations; peak live heap, MiB (x86_64 Linux, clang version 18.1.8, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 32,000,002; 0.0 | 32,000,001; 0.0 | 32,000,002; 0.1 | 32,000,011; 0.0 | 32,000,011; 0.0 | 32,000,001; 0.0 | 32,000,001; 0.0 | — |
large-objects | 400,002; 0.6 | 400,001; 0.6 | 400,002; 0.6 | 400,011; 0.6 | 400,011; 0.6 | 400,001; 0.6 | 400,001; 0.6 | — |
stack-alloc | 1; 0.0 | 1; 0.0 | 2; 0.1 | 10; 0.0 | 10; 0.0 | 1; 0.0 | 1; 0.0 | 10; 0.1 |
heap-alloc | 10,000,001; 0.0 | 10,000,001; 0.0 | 10,000,002; 0.1 | 10,000,010; 0.0 | 10,000,010; 0.0 | 1; 0.0 | 10,000,001; 0.0 | — |
arena | 29,601; 4.6 | 19,601; 3.1 | 19,602; 3.1 | 21,610; 4.6 | 21,610; 4.6 | 19,601; 3.1 | 19,601; 3.1 | 10; 0.1 |
vector-growth | 241; 384.0 | 241; 256.0 | 262; 384.1 | 250; 256.0 | 250; 256.0 | 331; 170.0 | 331; 170.0 | 10; 0.1 |
string-building | 661; 12.0 | 631; 8.0 | 572; 11.3 | 640; 8.0 | 640; 8.0 | 781; 9.3 | 781; 9.3 | 10; 0.1 |
recursion | 1; 0.0 | 1; 0.0 | 2; 0.1 | 10; 0.0 | 10; 0.0 | 1; 0.0 | 1; 0.0 | 10; 0.1 |
linked-list | 20,000,001; 45.8 | 20,000,001; 45.8 | 20,000,002; 45.8 | 20,000,010; 45.8 | 20,000,010; 45.8 | 20,000,001; 45.8 | 20,000,001; 45.8 | 10; 0.1 |
binary-trees | 29,971,807; 20.0 | 29,971,807; 12.0 | 29,971,808; 12.1 | 29,971,816; 12.0 | 29,971,816; 12.0 | 29,971,807; 12.0 | 29,971,807; 12.0 | 10; 0.1 |
hash-map | 3; 64.0 | 3; 64.0 | 4; 64.1 | 12; 64.0 | 12; 64.0 | 3; 64.0 | 3; 64.0 | 10; 0.1 |
parsing | 25; 48.0 | 21; 32.0 | 28; 48.1 | 33; 32.0 | 33; 32.0 | 30; 31.5 | 30; 31.5 | 10; 0.1 |
memory-copy | 3; 32.0 | 3; 32.0 | 4; 32.1 | 12; 32.0 | 12; 32.0 | 3; 32.0 | 3; 32.0 | 10; 0.1 |
numeric | 4; 39.7 | 2; 38.1 | 5; 39.8 | 13; 39.7 | 13; 39.7 | 4; 39.7 | 4; 39.7 | 10; 0.1 |
data-oriented | 5; 15.3 | 5; 15.3 | 6; 15.3 | 14; 15.3 | 14; 15.3 | 5; 15.3 | 5; 15.3 | 10; 0.1 |
Compile time, s; binary size, KiB (x86_64 Linux, clang version 18.1.8, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 0.57; 32 | 0.06; 16 | 0.46; 16 | 0.10; 4432 | 0.09; 4432 | 11.28; 3512 | 12.12; 3371 | — |
large-objects | 0.55; 32 | 0.05; 16 | 0.52; 16 | 0.09; 4432 | 0.10; 4433 | 11.20; 3514 | 12.10; 3373 | — |
stack-alloc | 0.51; 32 | 0.05; 16 | 0.14; 16 | 0.08; 4431 | 0.08; 4432 | 0.31; 134 | 12.09; 3371 | 0.09; 68 |
heap-alloc | 0.55; 32 | 0.06; 16 | 0.46; 16 | 0.08; 4431 | 0.08; 4432 | 6.55; 260 | 12.28; 3374 | — |
arena | 0.56; 32 | 0.05; 16 | 0.08; 16 | 0.10; 4433 | 0.19; 4433 | 11.46; 3513 | 12.22; 3372 | 0.12; 68 |
vector-growth | 0.52; 32 | 0.05; 16 | 0.26; 16 | 0.19; 4432 | 0.08; 4432 | 11.24; 3515 | 12.22; 3376 | 0.10; 68 |
string-building | 0.54; 32 | 0.06; 16 | 0.35; 16 | 0.09; 4432 | 0.09; 4433 | 11.12; 3518 | 12.05; 3380 | 0.11; 68 |
recursion | 0.49; 32 | 0.05; 16 | 0.07; 16 | 0.12; 4431 | 0.07; 4431 | 0.14; 131 | 12.36; 3366 | 0.10; 68 |
linked-list | 0.60; 32 | 0.05; 16 | 0.54; 16 | 0.08; 4432 | 0.08; 4432 | 11.14; 3520 | 12.04; 3371 | 0.09; 68 |
binary-trees | 0.55; 32 | 0.05; 16 | 0.52; 16 | 0.08; 4433 | 0.09; 4433 | 10.91; 3515 | 12.31; 3375 | 0.11; 72 |
hash-map | 0.60; 32 | 0.05; 16 | 0.26; 16 | 0.09; 4431 | 0.09; 4432 | 11.33; 3513 | 12.09; 3373 | 0.11; 68 |
parsing | 0.54; 32 | 0.06; 16 | 0.25; 16 | 0.09; 4432 | 0.11; 4433 | 11.44; 3526 | 12.38; 3384 | 0.12; 68 |
memory-copy | 0.58; 32 | 0.06; 16 | 0.37; 16 | 0.10; 4432 | 0.09; 4433 | 11.37; 3511 | 12.19; 3371 | 0.13; 72 |
numeric | 0.56; 32 | 0.06; 16 | 0.23; 16 | 0.13; 4432 | 0.11; 4433 | 10.98; 3514 | 12.17; 3374 | 0.11; 68 |
data-oriented | 0.58; 32 | 0.06; 16 | 0.22; 16 | 0.10; 4432 | 0.09; 4433 | 11.19; 3514 | 12.21; 3374 | 0.12; 68 |
Wall time, ms, and relative to C (x86_64 Linux, gcc (Debian 12.2.0-14+deb12u1) 12.2.0, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 371 (1.24) | 299 (1.00) | 378 (1.26) | 367 (1.23) | 371 (1.24) | 291 (0.98) | 297 (0.99) | — |
large-objects | 931 (3.02) | 309 (1.00) | 276 (0.89) | 283 (0.92) | 288 (0.93) | 305 (0.99) | 275 (0.89) | — |
stack-alloc | 1134 (1.60) | 710 (1.00) | 723 (1.02) | 514 (0.72) | 625 (0.88) | 418 (0.59) | 552 (0.78) | 1241 (1.75) |
heap-alloc | 799 (1.82) | 440 (1.00) | 580 (1.32) | 550 (1.25) | 591 (1.34) | 192 (0.44) | 568 (1.29) | — |
arena | 1794 (2.27) | 791 (1.00) | 786 (0.99) | 547 (0.69) | 589 (0.75) | 815 (1.03) | 902 (1.14) | 209 (0.26) |
vector-growth | 2610 (4.41) | 592 (1.00) | 1051 (1.77) | 553 (0.93) | 621 (1.05) | 621 (1.05) | 608 (1.03) | 304 (0.51) |
string-building | 1216 (3.94) | 308 (1.00) | 527 (1.71) | 431 (1.40) | 483 (1.57) | 359 (1.16) | 489 (1.58) | 363 (1.18) |
recursion | 449 (3.04) | 148 (1.00) | 146 (0.99) | 313 (2.12) | 493 (3.34) | 287 (1.95) | 436 (2.95) | 176 (1.19) |
linked-list | 495 (1.18) | 419 (1.00) | 454 (1.08) | 471 (1.12) | 467 (1.12) | 389 (0.93) | 378 (0.90) | 60 (0.14) |
binary-trees | 630 (1.32) | 477 (1.00) | 571 (1.20) | 552 (1.16) | 583 (1.22) | 494 (1.04) | 557 (1.17) | 69 (0.15) |
hash-map | 872 (1.57) | 554 (1.00) | 458 (0.83) | 505 (0.91) | 493 (0.89) | 390 (0.70) | 499 (0.90) | 399 (0.72) |
parsing | 355 (1.96) | 181 (1.00) | 154 (0.85) | 186 (1.03) | 248 (1.37) | 139 (0.77) | 205 (1.13) | 143 (0.79) |
memory-copy | 4544 (12.96) | 351 (1.00) | 367 (1.05) | 266 (0.76) | 249 (0.71) | 533 (1.52) | 600 (1.71) | 1900 (5.42) |
numeric | 369 (1.56) | 236 (1.00) | 214 (0.91) | 231 (0.98) | 230 (0.97) | 231 (0.98) | 246 (1.04) | 233 (0.98) |
data-oriented | 254 (2.19) | 116 (1.00) | 138 (1.19) | 161 (1.39) | 136 (1.17) | 212 (1.83) | 137 (1.19) | 117 (1.01) |
What the baseline says about Nova (ratios are Nova's time over C's, clang / gcc):
| Gap | Cause | Increment |
|---|---|---|
memory-copy 17.3 / 13.0 | the block copy is a byte loop with two checked indexes per byte, where C has memcpy; and C's time is understated: clang merged its block copies into one (found in 9b, below) | 9b (bulk copy) |
vector-growth 1.87 / 4.41, string-building 2.37 / 3.94, parsing 1.56 / 1.96 | each push/append is a checked call through the Result path; the vector grows by allocate, copy byte by byte, free (nova_vec_grow, runtime/core/nova_rt_core.c:131-146), not realloc, so its peak heap is 1.5× C's (384 against 256 MiB, 48 against 32 MiB in parsing) | 9b (the copy as memcpy), 9c/9d (checks in the loop); growth through the allocator's realloc is a candidate |
recursion 1.63 / 3.04 | the overflow-checked add; rust-checked (1.51 / 3.34) and zig-safe (1.38 / 2.95) pay the same | 9d (value ranges cannot remove it here: fib's values are unbounded; recorded as the cost of the check) |
large-objects 0.98 / 3.02 | F6: two 64 KiB copies per box, which clang removes and gcc does not | 9e′ |
binary-trees 1.43 / 1.32, arena 1.55 / 2.27, linked-list 1.12 / 1.18 | F4: nodes holding Box<T>? or borrow T? are larger (binary-trees' peak heap 20 against 12 MiB, arena's chunks 29,601 against 19,601) | 9e′ (niche layout) |
stack-alloc 1.17 / 1.60, numeric 1.52 / 1.56, data-oriented 0.86 / 2.19 | index and overflow checks in tight loops; under gcc more than under clang, as in C6b's fbvalid | 9c, 9d |
small-objects 1.22 / 1.24, heap-alloc 1.20 / 1.82 | the allocation path (nova_alloc's fault-injection counters, nova_rt_core.c:232-248) and Box.create's Result (inferred) | a leaner allocation path |
hash-map 1.02 / 1.57 | probing loop checks; equal to C under clang | 9c |
Nova never failed a check here; its binaries are 32 KiB (C's 16), and it allocates nothing before main beyond what C does. Its compile time is 0.5–0.6 s per program, most of it the .NET start-up of novac (inferred: the C compile is 0.05 s).
Other languages, for context and not as Nova claims: Goose's pools make its rows with many small nodes 4–7× faster than C's allocator (linked-list, binary-trees, arena); Zig's ReleaseFast heap-alloc makes one allocation in all (LLVM removed the allocation, which C at -O2 did not); Rust's checked overflow costs it 1.0–1.6× its unchecked build.
9b — checked bulk operations (2026-10-25)
What was built:
d.copyFrom(s)andd.fill(v)on aSliceMut<T>(type-system §16.5; spec first).copyFromchecks that the lengths are equal (a new panic, "slice lengths differ",NV_PANIC_LENGTH), then makes one copy withmemmovesemantics.fillis amemsetfor one-byte elements, and otherwise a loop in the generated C. The safety argument is unchanged: the receiver's loan is live while the source is evaluated, so in safe code the source cannot overlap the destination (check-fail/slice-bulk-loans.nova). Ifunsafecode makes them overlap, the result is still defined (run-pass/slice-bulk.novacopies through overlappingfromRawslices in both directions, under ASan+UBSan too). In the IR, aLengthCheckstatement is followed by aSliceCopyorSliceFillbuilt-in (intermediate-representation.md). The C runtime hasnova_mem_copy/move/set(__builtin_mem*,<string.h>under MSVC) and guards the zero-length case, since a slice from raw parts may be null when empty.nova_vec_growcopies withnova_mem_copyinstead of a byte loop.- The LZ4 port's literal copy and table clear use the new operations (
integrations/cosmoos/c6/lz4.nova).c6.py --component lz4passes: the host test, 20,000 fuzz runs, and the differential test (20,000 cases, 0 mismatches). The aarch64 module object now hasmemmoveundefined, which CosmoOS exports, and still meets C4's checks. - Tests:
- run:
run-pass/slice-bulk.nova(every receiver form; u8, i8, bool, u32, u64, struct and enum elements; generic elements; empty and null slices; evaluation order),run-fail/slice-copy-length.nova; - check:
check-fail/slice-bulk.novaandslice-bulk-loans.nova; - emit: the C snapshot
emit-c/slice-bulk.c; - freestanding:
demo_blocksinfreestanding/demo.nova, on the host and both CosmoOS triples, with the symbol allowlist unchanged. - 1,995 tests pass.
- run:
A flaw in the 9a memory-copy workload. In the baseline the blocks were copied in address order, and clang turned C's 4,096 adjacent memcpy calls of 4 KiB into one 16 MiB memcpy (the disassembly passes 0x1000000 to memcpy). C's baseline time (252 ms) therefore measured one large copy, not the workload. Rust's 290 ms suggests the same of LLVM (inferred, not disassembled). The workload now copies block i × 2053 mod 4096 in step i: each block once, in an order no compiler can merge. The checksum is unchanged, and the change was made in all six languages (benchmarks/suite/README.md). The M9a memory-copy row above stays as it was measured, but it is not comparable with the rows below.
Results (Ryzen, as for 9a; load 0.6–2.5; run.py --runs 5; the tree at the 9b change). memory-copy with the permuted blocks, all languages:
| C compiler | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
| clang 18.1.8 | 686 (1.06) | 646 (1.00) | 686 (1.06) | 705 (1.09) | 638 (0.99) | 608 (0.94) | 661 (1.02) | 636 (0.99) |
| gcc 12.2.0 | 834 (1.67) | 499 (1.00) | 425 (0.85) | 631 (1.27) | 629 (1.26) | 632 (1.27) | 590 (1.18) | 2080 (4.17) |
Times are ms, with the ratio to C in brackets.
copyFrom's own gain was measured against the 9a element loop, with the same permuted blocks. That variant is the workload with the call replaced byfor (k in 0..BLOCK) { to[block + k] = from[block + k]; }, run throughmeasure.csix times; the medians below are of the last five runs. It took 2,552 ms (clang) and 3,220 ms (gcc), socopyFromis 3.7× and 3.9× faster.- Under clang Nova is now level with C, C++, Rust and Zig.
- Under gcc the remaining gap has a visible cause: gcc compiles C's constant-size
memcpy(…, 4096)inline asrep movsq, whereas Nova's length is a value at run time and goes to glibc'smemmove. That is for 9d (the gcc gap).
The rest of the suite, Nova and C only (run.py --langs nova,c), against the baseline:
- Vector growth under gcc went from 2,610 to 1,217 ms (4.41× → 2.11× C), from the
memcpyinnova_vec_grow. Under clang it is unchanged (1.87× → 1.86×), presumably because clang already made the byte loop amemcpy(inferred). The peak heap is still 1.5× C's: growth is still allocate, copy, free. - Every other Nova/C ratio moved by less than the 15% noise band, in both directions: string-building 2.37 → 2.57 under clang, binary-trees 1.43 → 1.23, numeric 1.56 → 1.77 under gcc. The C times moved by as much (C's binary-trees went 469 → 513 ms, with nothing changed), so these are noise, not effects of 9b.
LZ4 (benchmarks/lz4/run.py, same machine, MB/s, Nova/C; baseline in benchmarks/lz4/README.md):
| Compiler | Set | compress | decompress before → after |
|---|---|---|---|
| clang | text | 0.73 | 1.12 → 0.94 |
| random | 0.57 | 0.03 → 0.97 | |
| records | 0.57 | 0.18 → 0.22 | |
| gcc | text | 0.63 | 0.65 → 0.69 |
| random | 0.55 | 0.02 → 0.89 | |
| records | 0.48 | 0.31 → 0.39 |
Literal copies (random) now run at about C's speed, as the C6 scratch experiment predicted (0.94). Compression and short matches (records) are unchanged: they are byte loops with checked indexes in both codecs, which is 9c's work. text decompression, whose literals are short, measured 545, 586 and 514 MB/s in three clang runs (0.89–1.02 of C), against 657.7 in the single baseline run (1.12). Each literal is now a call to memmove where the loop was inline, so a small cost for short copies is possible. It is not established beyond the run-to-run variation, and is a 9c question: when the range checks are proven, a short copy could stay inline.
9c — bounds-check elimination (2026-10-25)
What was built (spec first: optimisations.md, normative; ADR 0010):
Analysis/BoundsCheckElimination.cs(renamedCheckElimination.csin 9d): a forward dataflow over each function's NIR. It removes aBoundsCheckwhen facts holding before it prove the index is below the length (§2.4 lists the facts and how they are derived). The C backend omits proved checks.--no-bcekeeps them;--explain-bounds-checkslists every check as removed or kept.- The owner chose to add a rule to the unsafe contract (ADR 0010,
unsafe.md§2). A pointer made from a reference may write its target only until the target is next used directly. That lets the analysis treat any local, including a vector's length, as fixed between two mutable borrows of it. The repository's unsafe code was audited against the rule (casts of references to pointers inlib/core,integrations/cosmoos/**,tests/corpus/**). Every such pointer targets a struct, a field or array elements, or (emit-c/volatile.nova) is written before the local is used again: no violation. - Equivalence checks:
- the interpreter evaluates every check and faults if a removed one would fail; every interpreter run in the suite checks this, and
BoundsCheckEliminationTestsshows the fault on a forced removal; - a
clang -O2 no-bceconfiguration in the run corpus and the fuzzer's oracle; tests/corpus/bounds-checks/proved.novaandkept.nova, with reviewed.bcesnapshots: 14 checks proved, one per loop or guard shape, and 8 kept. Seven of the kept are near misses: a vector popped in the loop, a borrowed index, another slice's length,<=,||, a stable vector and an unguarded offset. The eighth is a known miss, a vector behind a reference.
- the interpreter evaluates every check and faults if a removed one would fail; every interpreter run in the suite checks this, and
- Two emit-C snapshots lost a check (
arrays.c: index 2 of a 3-element array through a local;vectors.c: a secondv[0]after the first one passed). 2,000 tests pass.c6.py --component allpasses: both host tests, the fuzz targets and the differential tests, 0 mismatches.
What it removes. With --explain-bounds-checks:
- the LZ4 port: 14 of 21 checks, all of the decompressor's hot ones (literal and match copies, token and offset reads) and the compressor's match extension. The 7 kept are in
load32, which indexes through its parameter;table[h](a function's result);out[token]; andreadLength(through*ip). All of these need facts from another function; - the suite:
data-oriented20 of 26,stack-allocandheap-allocall;hash-map,numeric,string-buildingandmemory-copynone. Their indexes relate to a vector's length only through the number ofpushcalls made earlier (composite[j]withj ≤ LIMIT), or through a constant mask, which no rule of §2.4 covers.
Measurements (Ryzen, as before; load 1.2–2.5; run.py --runs 5, and the same with --nova-flag=--no-bce):
- Under clang, nothing changes. clang's binary for
data-orientedis byte-identical with and without the analysis, although the generated C differs by 20 checks: clang's own optimiser had already removed them, so 9c cannot gain there (verified withcmp). LZ4 under clang: equal within noise. - Under gcc, LZ4 gains: decompression 1.32× for
text(423 → 557 MB/s) and 1.33× forrecords(1310 → 1748 MB/s), compression 1.13× fortext. Nova/C forrecordsdecompression is now 0.53 (0.39 before 9c). - The suite shows no change beyond its noise in either compiler. An interleaved control (
hash-map, whose binaries are identical with and without the analysis) varied by up to 20% between runs on this shared machine, so suite differences under that are not evidence.data-orientedunder gcc, measured interleaved: equal. - Compile time: unchanged within noise (0.51–0.74 s either way).
So 9c's value is in what gcc does not prove itself, and in kernel code whose loops have their tests spelled out (LZ4). It does not close the suite's gaps; most of those are vector growth, overflow checks and allocation (9d, 9e′).
Fuzz campaign (2026-10-25, Ryzen, 10 jobs, 60 minutes, seeds 2000000–2011301): 11,302 programs, 0 findings (1,447 panicked, 8,065 other exit statuses). The oracle compared every C configuration, clang -O2 no-bce among them, and the interpreter, which faults if a removed check would fail.
9d — overflow checks and the gcc gap (2026-10-25)
What was built. The spec came first: optimisations.md §3 (normative), and c-backend.md §5 and §6.
- Overflow-check elimination.
Analysis/CheckElimination.cs(9c's analysis, renamed) also decides each checked+,-,*and negation.- It uses the same facts at the same program points. Each term gets bounds: its type's range, narrowed by what it is known to be below or above.
- An operation is proved when its result fits by interval arithmetic, when an unsigned subtraction has
b ≤ a, or when an unsigned sum is bounded through the facts (to + i < L). - Two new kinds of fact feed it: the exact result of arithmetic on constants, and the range of a widening cast of a value that is not a term (a field).
- Proved operations are emitted as plain C,
(T)(a + b). --no-ocekeeps every check;--explain-overflow-checkslists every decision.- The interpreter evaluates every operation exactly, and faults if a removed check would have overflowed.
- Location constants.
- The checked helpers in
rt.hnow takeconst nv_loc *, pointing to a per-site constantnvL_<n>that is read only on failure. - The emitter writes the program twice: a trial emission collects the locations, so that the table can come before the code.
nova_panicstill takes its location by value, so environments that define it are unchanged.- C4's checks pass with the table on both architectures: its relocations are
R_X86_64_64andR_AARCH64_ABS64, which CosmoOS's module loader applies.
- The checked helpers in
- Zero-initialisation only where it is needed.
- An aggregate local keeps
{0}only when part of it can be written without assigning it whole (a field, an element, a borrow, anUnpacktarget), or when one of its variant payloads is read. - Every other aggregate is assigned whole before each read.
- An aggregate local keeps
- Equivalence checks.
- The
clang -O2 no-bceconfiguration of the run corpus and the fuzzer becameclang -O2 no-elim, with both eliminations off. tests/corpus/overflow-checks/proved.novaandkept.nova, with reviewed.ocesnapshots: 13 proved and 1 kept inproved(the kept one isout.length - to, which nothing tests first); 7 kept and 1 proved inkept.CheckEliminationTestsshows the interpreter's fault on a forced removal.
- The
What it found:
- The cause of fbvalid's gcc gap (C6b, 1.76×). It was the zero-initialisation.
- Every aggregate local was declared
= {0}.refusalhas 32 such locals: optionalStringViewtemporaries, one per early return. - gcc 12 does not remove those stores. It reserves 424 bytes of stack and zeroes 144 of them on every call, before the first check.
- Removing the initialisers from a copy of the generated C took the port from 12.9 to 7.8 ns/call, against C's 7.3. Removing only the scalar initialisers changed nothing.
- Every aggregate local was declared
- Locations in the hot path. Passed by value, each check's location (a pointer and two 32-bit numbers) was loaded into registers on the path where the check passes. A copy of
data-oriented's C with constant locations went from 254 to 203 ms under gcc; clang was unchanged. - A gcc warning the fuzzer found. The first fuzz campaign found 4 programs, all one case, that gcc
-O0 -Werrorrejected after the zero-initialisation change.- An enum local that only ever holds
V2is matched againstV0, whose payload sits in bytesV2does not write. - The arm never runs, but
-Wmaybe-uninitializedcannot relate the payload to the tag. - Locals whose payload is read keep
{0}(c-backend.md§5), andrun-pass/payload-reads.novakeeps it fixed.
- An enum local that only ever holds
- Code placement moves LZ4 under gcc by up to 25%.
- Restoring every
{0}in LZ4's generated C made its decompressor faster at default alignment (records1816 → 2290 MB/s). - With
-falign-loops=32the order reversed: 2359–2383 without the initialisers, 2219–2255 with them. - The two decompressors execute the same instructions in a different register order. LZ4 differences under gcc of that size are placement, not cost.
- Restoring every
- The location change alone slows LZ4's decompressor under gcc by 6–15%, at either alignment.
- The assembly shows why:
iplives on the stack becausereadLengthborrows it, and with registers freed gcc now reloads it there at every sequence. - With overflow elimination as well, the decompressor is faster than before (below).
- The assembly shows why:
recursionunder gcc runs in 3 ms (C: 166).- Once
n - 1andn - 2are unchecked, gcc inlines several levels offiband, treating it as a pure function, merges the repeated calls. The exponential recursion collapses. - gcc does not do this for the C program at
-O2. The checksum is right, so it is a legitimate result, but it is gcc's transformation, not evidence about Nova. - From here on the workload is uninformative under gcc.
- Once
What it removes, with --explain-overflow-checks:
- LZ4: 35 of 50.
- fbvalid: all 4: two products of 32-bit fields widened to
u64, a sum of two widenedu8s, andU64_MAX - x. - The suite: 91 of 151 (
numeric16 of 25,hash-map10 of 17,parsing10 of 13,data-oriented8 of 23). - What stays is mostly arithmetic on values read from memory (
x[i] += vx[i]) and accumulators (t += …), which no fact bounds.
Measurements (Ryzen; three rounds alternating 9c, this tree with --no-oce, and this tree; medians of the rounds' medians; benchmarks/suite/run.py --runs 5, benchmarks/lz4/run.py, benchmarks/fbvalid/run.py):
| gcc 12 | 9c | 9d --no-oce | 9d | 9d / 9c |
|---|---|---|---|---|
| fbvalid (ns/call; C 7.2–7.5) | 12.95 | 7.67 | 7.86 | 0.61 |
LZ4 text decompress (MB/s; C 669–693) | 536 | 465 | 575 | 1.07× faster |
LZ4 records decompress (MB/s; C 3000–3437) | 1801 | 1641 | 2229 | 1.24× faster |
LZ4 text compress (MB/s; C 512–534) | 335 | 360 | 364 | 1.09× faster |
data-oriented (ms; C 117) | 266 | 182 | 202 | 0.76 |
parsing (ms; C 198) | 311 | 272 | 262 | 0.84 |
hash-map (ms; C 533) | 873 | 680 | 758 | 0.87 |
| clang 18 | 9c | 9d --no-oce | 9d | 9d / 9c |
|---|---|---|---|---|
| fbvalid (ns/call; C 6.4–6.6) | 6.62 | 6.76 | 6.90 | 1.04 (rounds 6.60–7.09) |
LZ4 records decompress (MB/s; C 7700–9340) | 2072 | 2093 | 7137 | 3.44× faster |
LZ4 text decompress (MB/s; C 566–605) | 588 | 572 | 600 | equal |
LZ4 text compress (MB/s; C 501–508) | 364 | 362 | 384 | 1.05× faster |
parsing (ms; C 136) | 243 | 232 | 202 | 0.83 |
numeric (ms; C 233) | 325 | 340 | 371 | 1.14 (not reproduced, below) |
- LZ4 under clang. With
op + iandfrom + iunchecked, clang vectorises the decompressor's match copy (16-bytemovups, two per iteration), which it did not do in 9c.records, whose matches are long, is now 0.77 of C. numericunder clang was 1.13× (the first run) and 1.14× (this run) slower with overflow elimination. Built and timed outside the suite runner, the two builds take the same time (334–339 ms each, five runs). The cause is not established; it is recorded for 9f's attribution.
Elsewhere in the suite the differences are within the rounds' spread (1–13%), except gcc's recursion (above). Compile time is unchanged (0.5–0.7 s; the second, trial emission is a few milliseconds).
Remaining gcc gaps:
memory-copy, 1.77× C: C's constant-sizememcpyis inlined asrep movsq, while Nova's length is a run-time value.large-objects(3.2×) andstring-building(3.5×): F6 and vector growth (9e′).- These are recorded, not fixed in 9d.
Fuzz campaign (2026-10-25, Ryzen, 10 jobs, 60 minutes, seeds 3000000–3011580): 11,581 programs, 0 findings (1,430 panicked, 8,312 other exit statuses). The first campaign on the same seeds, before the payload rule, found the gcc warning above in its first 400 programs.
9e — placement by default (2026-10-25)
What it found first: as M5 left it, placement changed nothing in the suite.
--explain-allocationsover the fifteen workloads placed no box at all. Two sites are returned (binary-trees), two are stored in aggregates (small-objects,linked-list), and one is over the 1 KiB limit (large-objects).- The fifth,
heap-alloc, was rejected only because it is in a loop: M5 refused every loop site, so that each site needs one slot. - Making placement the default as it was would have been a switch with nothing to measure.
What was built. The spec came first: memory-model.md §5.1 and optimisations.md §4, plus c-backend.md §2.2 and intermediate-representation.md §5.5a.
- Loop sites.
- A box created in a loop is placed when every local that may hold it ends, by its
StorageDead, on every path from the site back to itself. - The box of one iteration is then gone before the next, so one slot per site still suffices.
- A local declared outside the loop and assigned in it keeps the box on the heap ("may outlive its loop iteration").
- A box created in a loop is placed when every local that may hold it ends, by its
- Interpreter checks.
- Running a placed site again while its previous box is alive is a fault, since natively the slot would be overwritten.
- So is destroying a placed box twice. Before, the second destruction was silent; natively it runs the contents' drop glue twice. A mutation test shows each fault.
- The interpreter now keeps one slot allocation per site instead of one per execution.
- The default.
CEmitOptions.PlacementandInterpreterOptions.Placementdefault to on.novac --no-placementkeeps every box on the heap, and--placementis still accepted.- The run corpus's and the fuzzer's oracle configuration is now
clang -O2 no-placement. optimisations.md§1's rule that an optimisation keeps allocation counts now names placement as its one exception (§4.2). The allocation report and fault injection see fewerSystemHeapattempts, whichSystemHeap's placement-transparent contract allows.FailingAllocator,KernelHeapand user allocators are never placed.
- Tests.
placement/loops.nova(.allocsnapshot):- five loop sites placed: per-iteration use,
continueandbreak, nested loops, and a box whose field is moved out (ended byBoxFree); - one near miss kept on the heap.
- five loop sites placed: per-iteration use,
reasons.nova's loop site is now placed.emit-c/placed-boxes.novashows the slot reused in a loop.- Two interpreter mutation tests: a duplicated drop of a heap box, and of a placed one.
- 2,117 tests pass.
Measurements:
- Only
heap-alloc's generated C changes; the other fourteen workloads' C is byte-identical with and without placement. heap-alloc(Ryzen, five alternated rounds,run.py --workloads heap-alloc):
heap-alloc | without placement (ms) | with placement (ms) | gain | C | Rust |
|---|---|---|---|---|---|
| gcc 12 | 804–845 (median 821) | 699–743 (median 708) | 1.16× | 457 | 567 |
| clang 18 | 379–427 (median 381) | 287–309 (median 293) | 1.30× | 318 | 548 |
- Allocations drop from 10,000,001 to 1.
- Under clang Nova is now 0.92 of C. Under gcc it is still 1.55× C.
- The generated C zeroes a 256-byte temporary each iteration and copies it into the slot before the loop writes the real values; C writes the values directly. That is F6's shape, the likely cause of what remains under gcc (inferred; 9e′).
Not done: placement of boxes that escape into aggregates (small-objects, linked-list) or are returned (binary-trees). Those need ownership to move with the value, a different analysis.
Fuzz campaign (2026-10-25, Ryzen, 10 jobs, 60 minutes, seeds 4000000–4011613): 11,614 programs, 0 findings (1,453 panicked, 8,384 other exit statuses). Placement is on in every configuration but the clang -O2 no-placement oracle, and in the interpreter.
9e′ — the niche layout and Box.create in place (2026-10-25)
Both 9a findings showed in the baseline:
- F4 made nodes holding
Box<T>?orborrow T?24 bytes instead of 16. - F6 made
large-objects2.9× C under gcc.
So both were taken. Each spec came first.
- The niche layout (type-system §22.1,
c-backend.md§2).Box<T, A>?andborrow T?/borrow mut T?are their pointer alone, null fornull. Pool boxes, slices and string views keep the tag.- A box or reference is never null: allocation returns a pointer or fails,
nova_allocgives zero-sized objects one byte, a placed box points to its slot, and U2 requires a reference made from a raw pointer to be non-null. - Optionals do not cross the C boundary (§28.3), so no C code sees the layout.
TypeTable.EnumLayoutgives these optionals the pointer's size, no tag and the payload at offset 0.- In the emitter, three helpers carry every tag read, payload access and construction:
Tag,PayloadandOptionValue. They cover the drop glue,Unpack, the discriminant, enum aggregates, and the vector and slot-map helpers. - In the interpreter,
VariantIndexreads a niche optional's pointer, andEnumValuewrites it.
Box.createin place (c-backend.md§2). When the argument is a temporary built by the statement just before (array repeat, array, struct or enum aggregate) and used nowhere else, and its type has no drop and no C state:- the site calls
nvBA_<box>(), the allocation alone, and writes the construction into*p; - a placed box is built in its slot the same way;
- the construction's operands are already evaluated, so it can neither panic nor allocate, and the order is unobservable;
- a type with a drop goes through
nvB_as before, which destroys the value if allocation fails.
- the site calls
- Tests.
run-pass/niche-optionals.nova: a linked list throughBox<Node>?,borrow Node?walks,?and!on optional references, a consuming match out ofSome(box),Box<T>??fromVector.pop, and drops ofSome(box). It runs in every configuration and in the interpreter.NicheLayoutTestschecks the sizes: 8 bytes for the three niche kinds, and a tag for views, slices, nested optionals and integers. ANodeholding aBox<Node>?is now 16 bytes.emit-c/boxes-in-place.nova: a heap box built in place, and one that must not be, since its type has a drop.- The
slot-mapsandplaced-boxessnapshots were re-reviewed. - 2,130 tests pass.
c2.py --sshon the CosmoOS VM: 160 programs and the guest sweep (30 programs, 251 runs) PASS.c4.pyon both arches: PASS.
- One emitter detail: the structured printer re-indented every statement line from scratch, which flattened the in-place block. Lines deeper than one level now keep their extra indent.
Measurements:
- Only six workloads' generated C changes. LZ4, fbvalid and the other nine workloads are byte-identical.
- Ryzen, three alternated rounds of 9e and 9e′, medians in ms, with Nova/C in brackets:
| Workload | gcc 12: 9e | gcc 12: 9e′ | gain | clang 18: 9e | clang 18: 9e′ | gain | memory |
|---|---|---|---|---|---|---|---|
large-objects | 971 (2.92) | 278 (0.84) | 3.49× | 292 (1.03) | 286 (1.01) | 1.02× | — |
arena | 1681 (2.11) | 950 (1.20) | 1.77× | 1290 (1.53) | 930 (1.10) | 1.39× | peak 4.6 → 3.1 MiB (C 3.1); chunks 1850 → 1225 MiB |
heap-alloc | 721 (1.63) | 551 (1.25) | 1.31× | 289 (0.91) | 290 (0.91) | equal | — |
linked-list | 508 (1.30) | 415 (1.06) | 1.22× | 430 (1.06) | 417 (1.02) | 1.03× | 458 → 305 MiB allocated (C 305) |
binary-trees | 626 (1.28) | 587 (1.20) | 1.07× | 664 (1.32) | 568 (1.13) | 1.17× | peak 20 → 12 MiB (C 12); 915 → 457 MiB allocated |
small-objects | 388 (1.29) | 351 (1.17) | 1.11× | 366 (1.24) | 369 (1.25) | equal | — |
F6 is most of the gcc gains: clang had already removed the temporary and the copies, so its rows barely move.
F4 is the memory column, and
arena's andbinary-trees' gains under both compilers. Nodes are now C's size, and so are the peak heaps.MSVC: the first CI run failed. With in-place construction the temporary was no longer used, and MSVC's C4101 ("unreferenced local", an error with
/WX) rejected its declaration;NV_UNUSEDdoes not silence MSVC. Such temporaries are no longer declared.
Fuzz campaign (2026-10-25, Ryzen, 10 jobs, 60 minutes, seeds 5000000–5011602): 11,603 programs, 0 findings (1,468 panicked, 8,377 other exit statuses).
9f — acceptance (2026-10-25)
Run on the Ryzen (m9f.sh: benchmarks/suite/run.py and benchmarks/suite/attribute.py) and on the Windows VM (benchmarks/suite/run_msvc.py, docs/benchmarks.md §4.1):
- A. The whole suite in every language, as 9a's baseline was run: one
run.py --runs 5per compiler, with Zig 0.17.0 and Goose. - B. Nova by default, with each switch off (
--no-bce,--no-oce,--no-placement), and with all three off: three alternated rounds per compiler, on the suite, LZ4 and fbvalid. - MSVC. Nova and C, three rounds.
Where Nova stands. Nova/C, with Nova's time the median of B's three default rounds and C's from A:
| Compiler | Nova/C across the fifteen workloads | median | 9a's baseline |
|---|---|---|---|
| clang 18 | 0.93 (heap-alloc, data-oriented) to 2.44 (string-building) | 1.17 | 0.86 to 17.33 (memory-copy) |
| gcc 12 | 1.02 (large-objects) to 3.54 (string-building) | 1.40 | 1.18 to 12.96 (memory-copy) |
| MSVC 19.51, ARM64 | 0.99 (large-objects, recursion) to 2.27 (string-building) | 1.18 | — |
The gcc row leaves out recursion, which gcc rewrites algorithmically (9d): 0.02 of C. Every number keeps every check that could fail.
Against 9a's baseline (Nova/C in A's single run against 9a's; days apart, so read changes under about 15% as noise):
| Workload | clang 9a → 9f | gcc 9a → 9f | what changed it |
|---|---|---|---|
memory-copy | 17.33 → 0.96 | 12.96 → 1.56 | 9b copyFrom (and 9a's C was understated, 9b) |
vector-growth | 1.87 → 2.03 | 4.41 → 2.12 | 9b, memcpy in vector growth |
large-objects | 0.98 → 1.00 | 3.02 → 1.02 | 9e′ Box.create in place |
arena | 1.55 → 1.02 | 2.27 → 1.22 | 9e′ one-pointer optionals |
heap-alloc | 1.20 → 0.91 | 1.82 → 1.28 | 9e placement of loop boxes, 9e′ in place |
binary-trees | 1.43 → 1.28 | 1.32 → 1.22 | 9e′ one-pointer optionals |
parsing | 1.56 → 1.44 | 1.96 → 1.61 | 9c/9d, 9d locations |
data-oriented | 0.86 → 0.93 | 2.19 → 1.93 (B: 1.72) | 9d locations |
hash-map | 1.02 → 1.23 | 1.57 → 1.27 | (clang: within this machine's noise; B's median: 0.96) |
string-building | 2.37 → 2.51 | 3.94 → 3.55 | — (growth and appends, not yet addressed) |
Each switch's share. The time with the optimisation off, over the time with it on (B's medians). numeric and linked-list have no box placement could place, so their C is identical with and without --no-placement. Yet they read 1.13 and 1.15 under clang: differences under about 15% between variants are this machine's noise, not evidence.
| Program | --no-bce | --no-oce | --no-placement | all off |
|---|---|---|---|---|
heap-alloc, gcc / clang | 1.00 / 1.03 | 0.99 / 0.97 | 1.16 / 1.29 | 1.21 / 1.29 |
parsing, clang | 1.00 | 1.17 | 0.99 | 1.15 |
LZ4 records decompress, clang (MB/s 7149 on) | 3.30 | 3.44 | 1.00 | 3.43 |
LZ4 records decompress, gcc (MB/s 2398 on) | 1.13 | 1.46 | 0.98 | 1.73 |
LZ4 text decompress, gcc (MB/s 575 on) | 1.02 | 1.22 | 0.99 | 1.24 |
LZ4 text compress, gcc / clang | 1.02 / 1.05 | 1.00 / 1.06 | 1.02 / 0.99 | 1.08 / 1.09 |
recursion, gcc | 1.00 | 186 | 1.53 | 197 (gcc's call merging, 9d) |
| every other suite workload | within noise | within noise, except data-oriented gcc 0.91 and hash-map gcc 0.83 (11% spread) | within noise | — |
| fbvalid, gcc / clang | 1.01 / 1.01 | 0.99 / 0.99 | 1.00 / 1.00 | 0.99 / 1.00 |
- LZ4's decompressor under clang needs both eliminations: with either off, the match copy is not vectorised. Under gcc, overflow elimination gains the most and both together 1.73×.
- fbvalid's gain (12.9 → 7.8 ns under gcc, 9d) came from the zero-initialisation rule, which has no switch.
- So did 9b's bulk copies and 9e′'s layout and in-place construction, which the table against 9a shows.
data-orientedunder gcc is 9% faster with overflow elimination off, as in 9d. The code is placement-sensitive under gcc (9d), and the cause is not established. It stays recorded.
MSVC (Windows VM, ARM64, three rounds; Nova/C per round, then the median): small-objects 1.12, large-objects 0.99, stack-alloc 1.73, heap-alloc 1.01, arena 1.07, vector-growth 1.33, string-building 2.27, recursion 0.99, linked-list 1.08, binary-trees 1.18, hash-map 1.31, parsing 1.79, memory-copy 1.07, numeric 1.38, data-oriented 1.43. Under MSVC, rt.h uses the portable overflow checks, not compiler builtins (c-backend.md §1.1).
Acceptance (roadmap.md, M9):
- Optimisations with preconditions:
optimisations.md§2–§4 (normative), with a switch each. - A benchmark suite against C/C++/Rust/Zig/Goose: fifteen workloads in eight languages and modes (A, below).
- Each optimisation: equivalence tests and a measured gain:
- the interpreter checks every removed check and every placed box;
- the corpus and the fuzzer build without each optimisation;
- a 60-minute campaign after each increment: 9c–9e′, 0 findings at the end of each, with 9d's first campaign finding a gcc
-Werrorissue that was fixed; - gains measured in 9b–9e′ and attributed above.
- Published methodology:
benchmarks.md. - No claims without data: each figure here names its run.
M9 is complete.
What remains slow, for later work:
string-building(2.4–3.5× C) andvector-growth(about 2×): growth is allocate, copy, free (norealloc), and everypush/appendgoes through theResultpath;parsingandnumeric(about 1.45×);stack-allocunder gcc (1.43×) and MSVC (1.73×);recursionunder clang (1.68×), the overflow-checked addition that Rust and Zig pay too (their checked modes: 1.66 and 1.57× here).
A: the whole suite in every language (run.py --runs 5, one run per compiler; Rust 1.99.1, Zig 0.17.0, Goose 94cc64d; Goose has no heap, so its heap rows are empty):
Wall time, ms, and relative to C (x86_64 Linux, clang version 18.1.8, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 388 (1.33) | 292 (1.00) | 384 (1.32) | 367 (1.26) | 389 (1.34) | 346 (1.19) | 295 (1.01) | — |
large-objects | 285 (1.00) | 284 (1.00) | 278 (0.98) | 285 (1.00) | 277 (0.97) | 278 (0.98) | 283 (1.00) | — |
stack-alloc | 583 (1.19) | 490 (1.00) | 515 (1.05) | 521 (1.06) | 654 (1.33) | 425 (0.87) | 537 (1.10) | 496 (1.01) |
heap-alloc | 291 (0.91) | 319 (1.00) | 331 (1.04) | 549 (1.72) | 581 (1.82) | 192 (0.60) | 553 (1.73) | — |
arena | 944 (1.02) | 923 (1.00) | 834 (0.90) | 525 (0.57) | 529 (0.57) | 776 (0.84) | 799 (0.87) | 245 (0.27) |
vector-growth | 1112 (2.03) | 549 (1.00) | 758 (1.38) | 551 (1.00) | 646 (1.18) | 594 (1.08) | 611 (1.11) | 231 (0.42) |
string-building | 869 (2.51) | 346 (1.00) | 552 (1.59) | 446 (1.29) | 478 (1.38) | 357 (1.03) | 458 (1.32) | 440 (1.27) |
recursion | 467 (1.63) | 287 (1.00) | 290 (1.01) | 323 (1.12) | 477 (1.66) | 292 (1.02) | 451 (1.57) | 300 (1.04) |
linked-list | 473 (1.23) | 383 (1.00) | 590 (1.54) | 477 (1.24) | 466 (1.22) | 382 (0.99) | 381 (0.99) | 65 (0.17) |
binary-trees | 599 (1.28) | 468 (1.00) | 628 (1.34) | 561 (1.20) | 573 (1.22) | 492 (1.05) | 532 (1.14) | 86 (0.18) |
hash-map | 563 (1.23) | 456 (1.00) | 408 (0.89) | 432 (0.95) | 477 (1.05) | 386 (0.85) | 389 (0.85) | 492 (1.08) |
parsing | 201 (1.44) | 140 (1.00) | 143 (1.02) | 187 (1.34) | 206 (1.47) | 143 (1.02) | 193 (1.38) | 179 (1.28) |
memory-copy | 692 (0.96) | 717 (1.00) | 632 (0.88) | 626 (0.87) | 729 (1.02) | 603 (0.84) | 623 (0.87) | 617 (0.86) |
numeric | 337 (1.45) | 232 (1.00) | 261 (1.12) | 228 (0.98) | 233 (1.00) | 233 (1.00) | 246 (1.06) | 243 (1.05) |
data-oriented | 195 (0.93) | 210 (1.00) | 176 (0.84) | 195 (0.93) | 152 (0.72) | 225 (1.07) | 141 (0.67) | 141 (0.67) |
Peak resident memory, MiB (x86_64 Linux, clang version 18.1.8, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 1.4 | 1.5 | 2.9 | 2.0 | 2.0 | 1.9 | 2.0 | — |
large-objects | 1.7 | 1.6 | 3.0 | 2.3 | 2.4 | 1.9 | 2.1 | — |
stack-alloc | 1.3 | 1.4 | 2.8 | 2.0 | 1.9 | 1.8 | 1.9 | 1.8 |
heap-alloc | 1.2 | 1.4 | 2.9 | 1.9 | 1.9 | 1.7 | 1.9 | — |
arena | 4.1 | 4.0 | 5.4 | 6.4 | 6.4 | 4.4 | 4.6 | 5.8 |
vector-growth | 259.0 | 155.3 | 260.4 | 155.7 | 156.1 | 155.4 | 155.7 | 156.3 |
string-building | 16.8 | 10.0 | 17.1 | 10.4 | 11.9 | 11.7 | 12.4 | 10.1 |
recursion | 1.3 | 1.4 | 2.8 | 1.9 | 1.9 | 1.7 | 1.9 | 1.9 |
linked-list | 62.4 | 62.5 | 63.9 | 62.9 | 62.9 | 62.9 | 63.0 | 26.6 |
binary-trees | 17.3 | 17.5 | 18.9 | 17.9 | 18.0 | 17.8 | 17.9 | 8.8 |
hash-map | 65.0 | 65.0 | 66.3 | 65.8 | 65.8 | 65.4 | 65.5 | 68.5 |
parsing | 34.7 | 30.1 | 35.6 | 31.0 | 30.4 | 30.8 | 30.6 | 31.1 |
memory-copy | 32.9 | 32.8 | 34.3 | 33.8 | 33.8 | 33.2 | 33.3 | 36.7 |
numeric | 40.6 | 39.0 | 42.2 | 41.4 | 41.3 | 40.9 | 41.0 | 43.2 |
data-oriented | 16.2 | 16.3 | 17.5 | 17.0 | 17.0 | 16.6 | 16.7 | 22.4 |
Allocations; peak live heap, MiB (x86_64 Linux, clang version 18.1.8, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 32,000,002; 0.0 | 32,000,001; 0.0 | 32,000,002; 0.1 | 32,000,011; 0.0 | 32,000,011; 0.0 | 32,000,001; 0.0 | 32,000,001; 0.0 | — |
large-objects | 400,002; 0.6 | 400,001; 0.6 | 400,002; 0.6 | 400,011; 0.6 | 400,011; 0.6 | 400,001; 0.6 | 400,001; 0.6 | — |
stack-alloc | 1; 0.0 | 1; 0.0 | 2; 0.1 | 10; 0.0 | 10; 0.0 | 1; 0.0 | 1; 0.0 | 10; 0.1 |
heap-alloc | 1; 0.0 | 10,000,001; 0.0 | 10,000,002; 0.1 | 10,000,010; 0.0 | 10,000,010; 0.0 | 1; 0.0 | 10,000,001; 0.0 | — |
arena | 19,601; 3.1 | 19,601; 3.1 | 19,602; 3.1 | 21,610; 4.6 | 21,610; 4.6 | 19,601; 3.1 | 19,601; 3.1 | 10; 0.1 |
vector-growth | 241; 384.0 | 241; 256.0 | 262; 384.1 | 250; 256.0 | 250; 256.0 | 331; 170.0 | 331; 170.0 | 10; 0.1 |
string-building | 661; 12.0 | 631; 8.0 | 572; 11.3 | 640; 8.0 | 640; 8.0 | 781; 9.3 | 781; 9.3 | 10; 0.1 |
recursion | 1; 0.0 | 1; 0.0 | 2; 0.1 | 10; 0.0 | 10; 0.0 | 1; 0.0 | 1; 0.0 | 10; 0.1 |
linked-list | 20,000,001; 45.8 | 20,000,001; 45.8 | 20,000,002; 45.8 | 20,000,010; 45.8 | 20,000,010; 45.8 | 20,000,001; 45.8 | 20,000,001; 45.8 | 10; 0.1 |
binary-trees | 29,971,807; 12.0 | 29,971,807; 12.0 | 29,971,808; 12.1 | 29,971,816; 12.0 | 29,971,816; 12.0 | 29,971,807; 12.0 | 29,971,807; 12.0 | 10; 0.1 |
hash-map | 3; 64.0 | 3; 64.0 | 4; 64.1 | 12; 64.0 | 12; 64.0 | 3; 64.0 | 3; 64.0 | 10; 0.1 |
parsing | 25; 48.0 | 21; 32.0 | 28; 48.1 | 33; 32.0 | 33; 32.0 | 30; 31.5 | 30; 31.5 | 10; 0.1 |
memory-copy | 3; 32.0 | 3; 32.0 | 4; 32.1 | 12; 32.0 | 12; 32.0 | 3; 32.0 | 3; 32.0 | 10; 0.1 |
numeric | 4; 39.7 | 2; 38.1 | 5; 39.8 | 13; 39.7 | 13; 39.7 | 4; 39.7 | 4; 39.7 | 10; 0.1 |
data-oriented | 5; 15.3 | 5; 15.3 | 6; 15.3 | 14; 15.3 | 14; 15.3 | 5; 15.3 | 5; 15.3 | 10; 0.1 |
Compile time, s; binary size, KiB (x86_64 Linux, clang version 18.1.8, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 0.60; 32 | 0.05; 16 | 0.49; 16 | 0.10; 4432 | 0.09; 4432 | 11.04; 3512 | 12.37; 3371 | — |
large-objects | 0.61; 32 | 0.06; 16 | 0.47; 16 | 0.11; 4432 | 0.11; 4433 | 11.26; 3514 | 12.01; 3373 | — |
stack-alloc | 0.55; 32 | 0.06; 16 | 0.14; 16 | 0.08; 4431 | 0.08; 4432 | 0.31; 134 | 12.25; 3371 | 0.10; 68 |
heap-alloc | 0.56; 32 | 0.05; 16 | 0.50; 16 | 0.09; 4431 | 0.08; 4432 | 6.32; 260 | 11.94; 3374 | — |
arena | 0.59; 32 | 0.05; 16 | 0.11; 16 | 0.11; 4433 | 0.11; 4433 | 11.15; 3513 | 12.02; 3372 | 0.12; 68 |
vector-growth | 0.62; 32 | 0.06; 16 | 0.25; 16 | 0.10; 4432 | 0.09; 4432 | 10.94; 3515 | 12.26; 3376 | 0.09; 68 |
string-building | 0.63; 32 | 0.06; 16 | 0.32; 16 | 0.10; 4432 | 0.09; 4433 | 11.25; 3518 | 12.09; 3380 | 0.12; 68 |
recursion | 0.53; 28 | 0.05; 16 | 0.07; 16 | 0.07; 4431 | 0.07; 4431 | 0.13; 131 | 11.90; 3366 | 0.09; 68 |
linked-list | 0.57; 32 | 0.05; 16 | 0.47; 16 | 0.09; 4432 | 0.09; 4432 | 11.21; 3520 | 12.39; 3371 | 0.11; 68 |
binary-trees | 0.58; 32 | 0.06; 16 | 0.48; 16 | 0.09; 4433 | 0.08; 4433 | 11.20; 3515 | 12.17; 3375 | 0.15; 72 |
hash-map | 0.71; 32 | 0.05; 16 | 0.26; 16 | 0.10; 4431 | 0.09; 4432 | 11.14; 3513 | 12.17; 3373 | 0.11; 68 |
parsing | 0.59; 32 | 0.07; 16 | 0.24; 16 | 0.13; 4432 | 0.09; 4433 | 11.66; 3526 | 12.13; 3384 | 0.13; 68 |
memory-copy | 0.58; 32 | 0.07; 16 | 0.31; 16 | 0.10; 4432 | 0.10; 4433 | 11.31; 3512 | 12.78; 3371 | 0.12; 72 |
numeric | 0.64; 32 | 0.05; 16 | 0.24; 16 | 0.10; 4432 | 0.09; 4433 | 11.21; 3514 | 12.31; 3374 | 0.12; 68 |
data-oriented | 0.69; 32 | 0.06; 16 | 0.23; 16 | 0.10; 4432 | 0.10; 4433 | 11.16; 3514 | 11.96; 3374 | 0.12; 68 |
Wall time, ms, and relative to C (x86_64 Linux, gcc (Debian 12.2.0-14+deb12u1) 12.2.0, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 350 (1.15) | 304 (1.00) | 359 (1.18) | 370 (1.22) | 367 (1.21) | 291 (0.96) | 330 (1.09) | — |
large-objects | 280 (1.02) | 274 (1.00) | 281 (1.02) | 281 (1.02) | 287 (1.04) | 286 (1.04) | 294 (1.07) | — |
stack-alloc | 1135 (1.49) | 761 (1.00) | 718 (0.94) | 516 (0.68) | 695 (0.91) | 381 (0.50) | 577 (0.76) | 1250 (1.64) |
heap-alloc | 563 (1.28) | 441 (1.00) | 587 (1.33) | 571 (1.29) | 584 (1.33) | 192 (0.43) | 496 (1.13) | — |
arena | 963 (1.22) | 788 (1.00) | 785 (1.00) | 538 (0.68) | 554 (0.70) | 779 (0.99) | 803 (1.02) | 207 (0.26) |
vector-growth | 1218 (2.12) | 575 (1.00) | 968 (1.68) | 627 (1.09) | 663 (1.15) | 604 (1.05) | 608 (1.06) | 318 (0.55) |
string-building | 1094 (3.55) | 308 (1.00) | 584 (1.89) | 431 (1.40) | 483 (1.56) | 362 (1.17) | 458 (1.49) | 361 (1.17) |
recursion | 3 (0.02) | 147 (1.00) | 147 (1.00) | 324 (2.20) | 464 (3.16) | 317 (2.16) | 422 (2.87) | 175 (1.19) |
linked-list | 490 (1.28) | 382 (1.00) | 492 (1.29) | 469 (1.23) | 465 (1.22) | 382 (1.00) | 416 (1.09) | 58 (0.15) |
binary-trees | 549 (1.22) | 451 (1.00) | 584 (1.29) | 594 (1.32) | 569 (1.26) | 495 (1.10) | 523 (1.16) | 72 (0.16) |
hash-map | 745 (1.27) | 587 (1.00) | 446 (0.76) | 469 (0.80) | 452 (0.77) | 418 (0.71) | 389 (0.66) | 400 (0.68) |
parsing | 292 (1.61) | 182 (1.00) | 158 (0.87) | 188 (1.03) | 237 (1.30) | 141 (0.78) | 211 (1.16) | 138 (0.76) |
memory-copy | 758 (1.56) | 486 (1.00) | 541 (1.11) | 620 (1.28) | 623 (1.28) | 602 (1.24) | 594 (1.22) | 2122 (4.37) |
numeric | 428 (1.77) | 241 (1.00) | 214 (0.89) | 271 (1.12) | 233 (0.97) | 231 (0.96) | 246 (1.02) | 230 (0.95) |
data-oriented | 224 (1.93) | 116 (1.00) | 137 (1.18) | 161 (1.39) | 138 (1.19) | 210 (1.81) | 137 (1.18) | 117 (1.01) |
Peak resident memory, MiB (x86_64 Linux, gcc (Debian 12.2.0-14+deb12u1) 12.2.0, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 1.3 | 1.4 | 2.9 | 1.9 | 2.0 | 1.8 | 2.0 | — |
large-objects | 1.7 | 1.6 | 3.0 | 2.3 | 2.4 | 1.9 | 2.1 | — |
stack-alloc | 1.3 | 1.5 | 1.5 | 2.0 | 2.0 | 1.7 | 2.0 | 1.5 |
heap-alloc | 1.2 | 1.4 | 2.9 | 1.9 | 2.0 | 1.7 | 1.9 | — |
arena | 4.1 | 4.1 | 4.0 | 6.4 | 6.4 | 4.5 | 4.6 | 5.6 |
vector-growth | 259.0 | 155.2 | 260.4 | 156.4 | 155.7 | 155.7 | 155.6 | 155.8 |
string-building | 16.8 | 10.0 | 17.1 | 10.5 | 10.5 | 11.4 | 12.1 | 10.0 |
recursion | 1.3 | 1.4 | 1.4 | 1.9 | 2.0 | 1.7 | 1.9 | 1.6 |
linked-list | 62.3 | 62.4 | 63.9 | 63.0 | 63.0 | 62.8 | 62.9 | 26.3 |
binary-trees | 17.3 | 17.5 | 18.9 | 17.9 | 17.9 | 17.8 | 17.9 | 8.4 |
hash-map | 65.0 | 65.0 | 66.3 | 65.8 | 65.8 | 65.4 | 65.5 | 68.4 |
parsing | 34.6 | 29.9 | 35.6 | 31.0 | 30.7 | 30.6 | 29.8 | 30.3 |
memory-copy | 32.9 | 32.8 | 34.3 | 33.8 | 33.8 | 33.2 | 33.3 | 36.5 |
numeric | 40.8 | 39.0 | 42.2 | 41.3 | 41.5 | 41.1 | 41.1 | 42.9 |
data-oriented | 16.3 | 16.3 | 17.5 | 17.0 | 17.0 | 16.6 | 16.8 | 21.0 |
Allocations; peak live heap, MiB (x86_64 Linux, gcc (Debian 12.2.0-14+deb12u1) 12.2.0, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 32,000,002; 0.0 | 32,000,001; 0.0 | 32,000,002; 0.1 | 32,000,011; 0.0 | 32,000,011; 0.0 | 32,000,001; 0.0 | 32,000,001; 0.0 | — |
large-objects | 400,002; 0.6 | 400,001; 0.6 | 400,002; 0.6 | 400,011; 0.6 | 400,011; 0.6 | 400,001; 0.6 | 400,001; 0.6 | — |
stack-alloc | 1; 0.0 | 1; 0.0 | 1; 0.0 | 10; 0.0 | 10; 0.0 | 1; 0.0 | 1; 0.0 | 10; 0.1 |
heap-alloc | 1; 0.0 | 10,000,001; 0.0 | 10,000,002; 0.1 | 10,000,010; 0.0 | 10,000,010; 0.0 | 1; 0.0 | 10,000,001; 0.0 | — |
arena | 19,601; 3.1 | 19,601; 3.1 | 19,601; 3.1 | 21,610; 4.6 | 21,610; 4.6 | 19,601; 3.1 | 19,601; 3.1 | 10; 0.1 |
vector-growth | 241; 384.0 | 241; 256.0 | 262; 384.1 | 250; 256.0 | 250; 256.0 | 331; 170.0 | 331; 170.0 | 10; 0.1 |
string-building | 661; 12.0 | 631; 8.0 | 572; 11.3 | 640; 8.0 | 640; 8.0 | 781; 9.3 | 781; 9.3 | 10; 0.1 |
recursion | 1; 0.0 | 1; 0.0 | 1; 0.0 | 10; 0.0 | 10; 0.0 | 1; 0.0 | 1; 0.0 | 10; 0.1 |
linked-list | 20,000,001; 45.8 | 20,000,001; 45.8 | 20,000,002; 45.8 | 20,000,010; 45.8 | 20,000,010; 45.8 | 20,000,001; 45.8 | 20,000,001; 45.8 | 10; 0.1 |
binary-trees | 29,971,807; 12.0 | 29,971,807; 12.0 | 29,971,808; 12.1 | 29,971,816; 12.0 | 29,971,816; 12.0 | 29,971,807; 12.0 | 29,971,807; 12.0 | 10; 0.1 |
hash-map | 3; 64.0 | 3; 64.0 | 4; 64.1 | 12; 64.0 | 12; 64.0 | 3; 64.0 | 3; 64.0 | 10; 0.1 |
parsing | 25; 48.0 | 21; 32.0 | 28; 48.1 | 33; 32.0 | 33; 32.0 | 30; 31.5 | 30; 31.5 | 10; 0.1 |
memory-copy | 3; 32.0 | 3; 32.0 | 4; 32.1 | 12; 32.0 | 12; 32.0 | 3; 32.0 | 3; 32.0 | 10; 0.1 |
numeric | 4; 39.7 | 2; 38.1 | 5; 39.8 | 13; 39.7 | 13; 39.7 | 4; 39.7 | 4; 39.7 | 10; 0.1 |
data-oriented | 5; 15.3 | 5; 15.3 | 6; 15.3 | 14; 15.3 | 14; 15.3 | 5; 15.3 | 5; 15.3 | 10; 0.1 |
Compile time, s; binary size, KiB (x86_64 Linux, gcc (Debian 12.2.0-14+deb12u1) 12.2.0, median of 5 runs)
| Workload | nova | c | cpp | rust | rust-checked | zig | zig-safe | goose |
|---|---|---|---|---|---|---|---|---|
small-objects | 0.61; 28 | 0.04; 16 | 0.41; 16 | 0.13; 4432 | 0.09; 4432 | 11.51; 3512 | 11.76; 3371 | — |
large-objects | 0.65; 28 | 0.04; 16 | 0.48; 16 | 0.10; 4432 | 0.10; 4433 | 11.03; 3514 | 12.28; 3373 | — |
stack-alloc | 0.58; 28 | 0.04; 16 | 0.11; 16 | 0.08; 4431 | 0.12; 4432 | 0.31; 134 | 11.97; 3371 | 0.06; 65 |
heap-alloc | 0.67; 28 | 0.04; 16 | 0.48; 16 | 0.09; 4431 | 0.08; 4432 | 6.46; 260 | 12.69; 3374 | — |
arena | 0.60; 28 | 0.05; 16 | 0.07; 16 | 0.10; 4433 | 0.11; 4433 | 11.35; 3513 | 12.09; 3372 | 0.06; 65 |
vector-growth | 0.57; 28 | 0.04; 16 | 0.21; 16 | 0.08; 4432 | 0.08; 4432 | 11.09; 3515 | 12.11; 3376 | 0.06; 65 |
string-building | 0.61; 28 | 0.05; 16 | 0.28; 16 | 0.09; 4432 | 0.09; 4433 | 11.69; 3518 | 12.47; 3380 | 0.07; 65 |
recursion | 0.59; 32 | 0.05; 16 | 0.07; 16 | 0.10; 4431 | 0.07; 4431 | 0.14; 131 | 12.51; 3366 | 0.10; 65 |
linked-list | 0.60; 28 | 0.04; 16 | 0.41; 16 | 0.08; 4432 | 0.08; 4432 | 11.25; 3520 | 12.29; 3371 | 0.07; 65 |
binary-trees | 0.61; 33 | 0.11; 20 | 0.72; 36 | 0.08; 4433 | 0.10; 4433 | 11.52; 3515 | 11.98; 3375 | 0.10; 65 |
hash-map | 0.64; 33 | 0.05; 16 | 0.20; 16 | 0.10; 4431 | 0.09; 4432 | 11.41; 3513 | 11.81; 3373 | 0.08; 65 |
parsing | 0.64; 29 | 0.05; 16 | 0.22; 16 | 0.09; 4432 | 0.09; 4433 | 11.08; 3526 | 12.16; 3384 | 0.09; 65 |
memory-copy | 0.63; 33 | 0.04; 16 | 0.29; 16 | 0.10; 4432 | 0.13; 4433 | 11.28; 3512 | 11.94; 3371 | 0.08; 65 |
numeric | 0.63; 33 | 0.05; 16 | 0.22; 16 | 0.09; 4432 | 0.09; 4433 | 11.17; 3514 | 12.50; 3374 | 0.08; 65 |
data-oriented | 0.63; 33 | 0.06; 16 | 0.22; 16 | 0.09; 4432 | 0.09; 4433 | 11.42; 3514 | 12.34; 3374 | 0.08; 65 |