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

> On main since rc2: The compiler got about three times faster, and the collections about four.

Source: https://kern-lang.eu/releases/faster-compiler-and-collections

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.
