Source on Codeberg (opens in a new tab)

The compiler got about three times faster, and the collections about four

On main since rc2

Release notes

Compile throughput was profiled end to end and three root causes fell out, each fixed without a rewrite.

The strlen tax

Kern strings are NUL-terminated, so every length query was O(n), and a compiler slices and bounds-checks the same string constantly. The GC object header already carried a spare uint32 that nothing read or wrote; it is now a memoized strlen, so the first query on a string computes and caches and every later one is O(1). Self-bootstrap user-CPU went from 133.8 s to 37 to 46 s, about 3x, with no layout change and no producer changes.

The GC hash

The collector's address-to-liveness table folded address bits with XOR-shifts, which collapses to addr>>4 for the near-constant strides malloc returns, textbook primary clustering. Measured over 44 million inserts, average probe was 14.97 and a single insert probed 123,898 slots. Fibonacci hashing diffuses every input bit into the high word: average probe 0.45, worst case 57. Wall time is unchanged, so this is a tail-latency fix, and it matters most for a long-running Kern server allocating under KERN_AUTO_GC.

The prelude

Every build force-injected all 146 replaceable-stdlib modules and emitted every function whether the program called it or not, so hello.kern shipped 255 function definitions and 52,002 IR lines for clang to optimise and the linker to throw away. Emission now runs a reference fixpoint over the resolved @kern_ symbols, so unreferenced prelude functions are never emitted at all: 25 definitions, 6,411 IR lines, build user-CPU 1.97 s to 0.40 s.

The compiler's own bootstrap deliberately keeps the shake off, so the reproducibility chain is untouched, and the bootstrap fixpoint is still byte-identical generation over generation.

The latest round went after collections

list_append is the allocation hot path under strings, parsers and every collection-heavy stdlib function, and profiling a tight append loop showed the cost was not the store but the machinery around it: a mutex taken on every single element even when no second thread exists, a GC header probe per append, and a root slot pushed for a result the caller discards. A monotonic flag now records whether the process has ever spawned a thread, set on the parent side before every thread-creating primitive so no window exists where two threads both read “single-threaded”, and the lock is skipped until that flag flips, after which it re-arms forever.

The GC roots moved inside the grow branch, because a no-grow append allocates nothing and therefore cannot trigger a collection. The compiler emits an inline fast path at the call site. Sorting got its own fix: a Kern quicksort was making a runtime call per comparison and per swap, where the values already sit inline in the list's own slots, so a native sort over those slots replaced it. Together, appending is about 4x quicker and sorting a million integers went from 731 ms to 118, with the sort step itself down to roughly 16 ms, which upstream calls Go-class. Verified under GC stress with a collection on every allocation, so nothing is left unrooted.