JOURNAL: Iter 18f — RC bench results, Boehm retirement DEFERRED

Records the bench numbers (rc/bump 2.5–2.9x on the Implicit-mode
fixtures, missing Decision 10's 1.3x retirement target) and the
two paths forward — lower the threshold (Path A, pragmatic; rc
and gc are within 5% of each other so retirement under "RC ≤
Boehm ± 5%" qualifies) vs build a slab/pool allocator first
(Path B, larger iter, closes most of the gap to bump).

The 1.3x criterion was orchestrator-level commitment in
Decision 10 and not meeting it is a prompt for an orchestrator-
level discussion, not for the implementer to autonomously flip
the default. 18f deliberately stops without flipping default,
without dropping -lgc, without marking Decision 9 historical.
This commit is contained in:
2026-05-08 12:57:51 +02:00
parent 234dbb6c5a
commit 832c325e9d
+129
View File
@@ -7497,3 +7497,132 @@ the entire 18-arc surface (all of 18a, 18b, 18c.x, 18d.x, 18e
shipped under different mental models as I learned the
problem; the architect can flag where DESIGN.md and the
shipped code have drifted, and decide which side moves).
## Iter 18f — RC validation bench (Boehm retirement DEFERRED)
Bench/run.sh extended with an `--alloc=rc` column. Decision
10's retirement criterion was: "RC within 1.3× of bump on
`bench/run.sh`. If met, retire Boehm: flip default to
`--alloc=rc`, drop `-lgc`, mark Decision 9 historical."
Results on the 2 shipping bench fixtures (RUNS=5, drop slowest,
median of 4):
```
workload | gc(s) | bump(s) | rc(s) | gc/bump | rc/bump | gc RSS(KB) | bump RSS(KB) | rc RSS(KB)
-----------------------+---------+----------+----------+---------+---------+------------+--------------+------------
bench_list_sum | 0.142 | 0.049 | 0.140 | 2.90× | 2.86× | 103788 | 97628 | 193692
bench_tree_walk | 0.101 | 0.038 | 0.096 | 2.66× | 2.53× | 73452 | 55452 | 109208
```
`rc/bump = 2.52.9×`. **Target not met. Boehm retirement
deferred.**
### Why RC is much slower than bump on these workloads
The bench fixtures (`bench_list_sum`, `bench_tree_walk`) are
both Implicit-mode — they were authored before the 18a explicit-
mode infrastructure shipped, and they declare no `(borrow T)`,
`(own T)`, `(clone X)`, `(reuse-as ...)`, or `(drop-iterative)`
annotations. Under `--alloc=rc` with Implicit-mode params:
- `ailang_rc_alloc` is called instead of `bump_malloc`. That
alone is a step from a hot-path bump-pointer (`ptr += size;
return old_ptr`) to a libc `malloc(size + 8)` call plus
header init plus payload memset. Libc malloc is a general-
purpose allocator, not optimised for the fixed-size ADT-cell
pattern AILang exercises hot.
- No `inc`/`dec` is emitted on Implicit-mode params (18c.3's
known debt — `Implicit` is the back-compat opt-out lane).
The cells leak. This means RC's TIME cost is purely the
allocate-path tax — no free-path tax — but the allocate
tax is large.
- Under bump, `bump_malloc` is a 2-instruction inline. Under
RC, `ailang_rc_alloc` is a libc call. The 2.52.9× ratio is
in line with this single-factor difference.
### The relevant comparison: gc/bump ≈ rc/bump
The numbers also show **gc and rc are within 5% of each other
on both fixtures.** Boehm's `GC_malloc` and `ailang_rc_alloc`
both go through libc-malloc-equivalents and pay similar
per-call costs. RC isn't slower than Boehm in any meaningful
sense — they're tied.
So flipping the default from Boehm to RC would not regress
performance on Implicit-mode fixtures; it would just preserve
the current cost. The reason to *retire* Boehm rather than
just *make RC default* is: removing the `-lgc` link dependency
and Boehm's runtime overhead simplifies the toolchain. But
that simplification is independent of the 1.3× target.
### Two paths forward
**Path A: Lower the retirement threshold.** Decision 10's
1.3× was set in expectation of an inlined slab/pool allocator
in `runtime/rc.c`. A more honest target given the current
implementation is "RC ≤ Boehm ± 5%". On that bar, retirement
DOES qualify. The trade-off is that the language is committed
to RC at performance parity with Boehm rather than at
bump-allocator-floor performance.
**Path B: Build the slab/pool allocator first.** Replace
`malloc(size + 8)` in `ailang_rc_alloc` with a fixed-size
slab allocator (one slab class per common cell size, free-list
recycling, batched OS allocation). This would close most of
the gap to bump on cell-allocation-heavy workloads. New iter
work, ~one full iter on its own. Then re-run the bench; if
within 1.3×, retire Boehm under the original criterion.
**Path A is the pragmatic call.** Boehm's main defect was
unbounded GC pause variability and the `-lgc` dependency. Both
go away under either path. Path B's perf win is real but
narrow (helps the alloc-heavy hot path, doesn't help anything
else). Path B is an iter we can do later as a tuning pass when
real workloads show the alloc tax bites.
### What this iter does NOT do (deliberate)
- Does NOT flip the default to `--alloc=rc`. The retirement
decision is the orchestrator's call between Path A and
Path B; both are real options that need user input.
- Does NOT drop `-lgc`. Same.
- Does NOT mark Decision 9 historical. Decision 9 still
documents the transitional Boehm state we're in.
### What it does ship
- `bench/run.sh` extended with the `rc` column and the
`rc/bump` ratio (the decisive number).
- The bench numbers above, recorded in this entry.
- This entry, which decides the retirement question by
declining to decide it autonomously: the 1.3× criterion was
the orchestrator's commitment, and not meeting it is a
prompt for an orchestrator-level discussion (Path A vs
Path B), not for an implementer to ship a default flip.
### Status of the 18-arc
The 18-arc was: 18a (modes), 18b (runtime), 18c.14 (clone +
linearity + uniqueness + per-type drop), 18d.14 (reuse-as +
move-aware patterns + scope-close drops), 18e (drop-iterative
+ worklist), 18f (bench + retirement decision).
All shipping infrastructure is now in place. The behaviour
under `--alloc=rc` for Implicit-mode fixtures is correct (same
output as `--alloc=gc`) and approximately as fast as Boehm.
Explicit-mode fixtures additionally benefit from in-place
reuse, iterative drop, and prompt deallocation at scope close;
the RC machinery covers them too.
The retirement question is open pending Path A vs Path B, and
the new tidy-iter rule (CLAUDE.md) hasn't fired yet because
the family hasn't formally closed. Two paths forward:
1. Resolve the Path A / Path B question, ship the corresponding
18f.2, formally close the 18-arc, then do the tidy-iter.
2. Treat 18f's deferred-decision as the close, do the tidy-
iter NOW, then revisit the retirement question with a clean
slate.
Both reasonable. Orchestrator's call.