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:
+129
@@ -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.5–2.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.5–2.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.1–4 (clone +
|
||||
linearity + uniqueness + per-type drop), 18d.1–4 (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.
|
||||
|
||||
Reference in New Issue
Block a user