runtime: allocation accounting — deterministic signal for complexity gating #131
Reference in New Issue
Block a user
Delete Branch "feat/alloc-accounting"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the three primitives the test-framework design (§6.5) needs to gate on growth curves:
el_alloc_count,el_alloc_bytes,el_peak_rss. Registered inbuiltin_arity, wrapped inel_seed.c.Why counts, not wall-clock. A growth-curve gate must be a hard build failure, so the signal can't flake. Wall-clock needs warmup, statistics, and a quiet machine — unusable as a CI gate. Allocation counts are perfectly deterministic: same input, same number, every machine, every run.
Why bytes as well as count — this is the whole gate, not redundancy. Measured with two El programs, one allocating once per item, one rebuilding its accumulator each iteration:
The quadratic program's allocation count is exactly linear — identical to the healthy one. Counting allocations alone would have missed it entirely. Bytes catch it: each doubling of n quadruples bytes (3.94 → 3.97 → 3.99, converging on 4.0 = O(n²)) while the linear case converges on 2.0.
That shape — count linear, per-allocation size growing — is the classic accidental quadratic, and it's exactly
elc's defect: quadratic allocation volume, paid as 27 GB RSS by the old binary and as 42s of malloc/free churn by the rebuilt one. Volume was the invariant; RSS and wall-clock were just how it surfaced.el_peak_rssis exported for context and explicitly not a gating signal — perturbed by allocator internals, page cache, and OS.Counters are unsynchronised by design: this is measurement, and a lock would change what's being measured. Exact on the single-threaded compile path.
Implements the three primitives the test-framework design (DESIGN.md §6.5) requires for gating on growth curves: el_alloc_count, el_alloc_bytes, el_peak_rss. Registered in codegen's builtin_arity and wrapped in el_seed.c per the project's C-builtin recipe. WHY COUNTS AND NOT WALL-CLOCK: a growth-curve gate has to be a hard build failure, which means the signal cannot flake. Wall-clock needs warmup, statistics, and a quiet machine; on shared CI it is unusable as a gate. Allocation counts are perfectly deterministic — same input, same number, every machine, every run. Fit them against n and a complexity regression becomes a build failure with zero noise. All four runtime string allocators (el_strdup, el_strbuf, and their _persist variants) funnel every allocation the language performs, so instrumenting there counts everything. WHY BYTES AS WELL AS COUNT — this is not redundancy, it is the whole gate. Measured with two El programs, one allocating once per item, one rebuilding its accumulator each iteration: n linear allocs / bytes quadratic allocs / bytes 100 100 / 290 100 / 5,150 200 200 / 690 200 / 20,300 400 400 / 1,490 400 / 80,600 800 800 / 3,090 800 / 321,200 The quadratic program's allocation COUNT is exactly linear — identical to the healthy one. Counting allocations alone would have missed it completely. Bytes catch it: each doubling of n quadruples bytes (ratios 3.94, 3.97, 3.99 -> converging on 4.0, i.e. O(n^2)), while the linear case converges on 2.0. That shape — count linear, per-allocation size growing — is the classic accidental quadratic, and it is exactly elc's defect: quadratic allocation VOLUME, which the old shipped compiler paid in RSS (27 GB, OOM) and the rebuilt one pays in malloc/free churn (42s on 1.4 MB). Volume was the invariant across both; RSS and wall-clock were just the two ways it surfaced. el_peak_rss is exported for context and is explicitly NOT a gating signal — it is perturbed by allocator internals, the page cache, and the OS. Gate on the deterministic numbers; report the physical one. Counters are unsynchronised by design: this is measurement, and a lock would change the thing being measured. Exact on the single-threaded compile path, approximate under threads.