perf: P10 - do not emit a witness member nothing calls #57
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Context
F12: a whole-program dead-member scan, a code-size fix. Waits until there are two back ends to answer to.
Source
Acceptance criteria
Dependencies
None known.
Moved from the planning lists of the docs; the design record stays the specification.
[loop:torbscript] intake: in scope (author orion, label loop), source PERFORMANCE.md round P10, not a duplicate. Same precondition as P9 (two back ends), checked in refine; code-size result measured in forge CI. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z
[loop:torbscript] claim refine by session:loop-torbscript-20261009 until 2026-10-10T00:30Z
[loop:torbscript] refine: ready.
Goal. A witness table slot that no
CallWitnessof the whole program can reach is left empty: the C back end writesNULLand emits neither the thunk nor (unless a directCallstill names it) the member's instance, and the bytecode back end gets the same-1it already writes for a missing chunk. Output of every program stays byte-for-byte the same at run time; only code size and build time move.Implements. PERFORMANCE.md finding F12 and round P10 (section 5, first ledger only). The second ledger of F12 (P6 copies that gained nothing) is split off, see below.
Precondition "two back ends": met. ROADMAP milestone 7: the bytecode VM interprets the same IR as the C back end, and the conformance suite holds both to one behaviour (
torb run/torb testrun in the VM by default,--nativeandtorb buildin C). So the pass answers to both: the bytecode emitter (backend/bytecode/emit.trb,emitTables) already maps a member without a chunk to-1, andvm/embed.trb/vm/interpret.trbcopy the member list as numbers. This also corrects the round table's "only the C back end pays": the VM is covered by the same IR convention.Seed. Does not touch the seed: no syntax change, no new native, no new driver flag, no std name looked up by string. One PR, a plain compiler change, so the two-commit rule does not apply.
Size. One PR, two Conventional Commits:
perf(ir): leave a witness member nothing calls out of its tabledocs(performance): record round P10 and its measurementDesign (decided by the loop, no owner question).
(table.applied trait, member index). Tables of one trait share their slot layout, andCallWitness.memberis a static index, so a scan over allCallWitnessinstructions of all functions, resolving each witness source to the tables it can name, is complete. ReusereachableTables/membersAtfromir/kept.trb(move to a shared place if needed) rather than a second resolver. A source that cannot be named (reachableTablesreturnsNone) makes every slot of its trait live: sound by construction, nothing proved is nothing removed.nested, whose tables are tables of the supertrait with their own slots, so aCallWitnesson the narrowed value marks the supertrait's slot; the scan followsnestedsteps the same wayreachableTablesdoes.WitnessTable.memberskeeps its length (indices are positions); a dead slot holdsFunctionId(-1). Chosen over a separate set because the three consumers that tolerate "no member" already testhasFunction/bounds (ir/kept.trbisKeptByAny/membersAt,cli/partial.trb,backend/c/body.trbkeep set viakeepReached), and bytecode emit already writes-1.ir/lower/lower.trb,finishLowering): afterdropListBoundsChecks(the last step that can add or removeCallWitness;lowerBoxedUpdatesadds some) and beforetrackCallerSites(which reads table members to make caller-site copies, so no copy is made for a dead slot). Runs forlowerLibrarytoo, since both go throughfinishLowering.CallWitnessof another slot (a dead member's body is not emitted, so its calls no longer count). Iterate to the fixed point over the keep set; stop when no slot changes. The result is a pure function of the program (no ordering dependence), so--emit-ctwice stays byte identical.ir/verify.trb): assert that every-1slot is unreachable, not merely unreached: noCallWitnessin a function the program keeps resolves to a table whose slot at its member is-1; andprintWitnessTableprints a dead slot as-instead of callingfunctionAt. This is the "IR verifier asserts every NULL entry is unreachable" the design record asks for.Acceptance criteria (testable).
add,length), the C contains one thunk for each(trait, index, target)that someCallWitnesscan reach and no thunk for any other; the count is pinned in the test (design record: 24 thunks, 25 102 bytes before).compiler/tests/lower-generics.test.trb/ a newcompiler/tests/witness-dead.test.trb: (a) a slot called only through a supertrait-narrowed value stays live; (b) a slot reached only by a member that is itself dead dies in the same run (fixpoint); (c) a witness source that cannot be named keeps all slots of the trait; (d) a table of a trait whose value is only ever devirtualized has all slots dead and the table still emits as a valid (empty-array,NULL) table.CallWitnessonto a-1slot is rejected byir/verify.trb.sh tools/gates.sh abgreen (tier B: the conformance suite in both back ends with the leak gate on, same output/exit codes/panics/leak counts),--emit-ctwice byte identical, bootstrap fixpoint holds.program.cis smaller than before by a recorded number of bytes and thunks; the number goes into the PR description and into PERFORMANCE.md (round P10 row marked done, F12 "what was measured").Exact files.
compiler/src/ir/witness.trb(the scan; the round table names it) and/orcompiler/src/ir/instances.trbonly if the instance set needs the same notion,compiler/src/ir/kept.trb(sharereachableTables/membersAt),compiler/src/ir/lower/lower.trb(one call infinishLowering),compiler/src/ir/ir.trb(doc ofWitnessTable.members:-1is a dead slot),compiler/src/ir/verify.trb,compiler/src/ir/print.trb,compiler/src/backend/c/emit.trb(emitWitnessTables,emitWitnessTable,thunkNamesOf,emitThunkloops skip-1, writeNULL),compiler/src/backend/c/body.trbandcompiler/src/backend/c/crossing.trb(the loops overtable.membersat the keep set and the split-unit crossing check),compiler/src/backend/bytecode/emit.trb(confirm-1path),compiler/src/cli/partial.trb(confirm). Tests: newcompiler/tests/witness-dead.test.trb, a pinned size incompiler/tests/emit-c.test.trb. Docs:docs/PERFORMANCE.md(P10 row, F12 measured),docs/BACKEND.mdif it states the table layout. Nostd/, noruntime/change (the runtime never reads a table slot by index; checked by grep).Measurement plan (forge CI, decided here).
wc -coftorb build --emit-cfor the three-line list program; (ii) the count of witness thunk definitions in it; (iii)wc -cand the thunk count of the compiler's ownprogram.cfrom the bootstrap; (iv) wall time of the compiler's C compile step (87 s today) and ofsh tools/bootstrap.sh; (v) the VM: bytecode size of the same program (torb build --embed-vmor the disassembler) if the bytecode emitter's function set shrinks.benchmarks.ymltable (benchmarks/*,game.sh) of the first nightly after merge is compared with the previous one; the pass changes no executed instruction, so the expected delta is noise. A slowdown over noise reopens the issue.program.cshrinks by less than the 13% the thunk and instance shares suggest, record the number and why in PERFORMANCE.md; that is a result, not a blocker.Out of scope (split). F12's second ledger, dropping the P6 copies that gained nothing (334 of 1 021 in the compiler's program, 0.5 MB of C), is a different mechanism (a remap of the contiguous tail of
FunctionIds inir/devirtualize.trb) and gets its own issue (#281), so this PR stays one reviewable change.Implementer. opus, effort high (compiler/src IR pass with a soundness argument and a verifier invariant; a wrong dead slot is a null function pointer at run time).
next: implement
[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-10T04:40Z
[loop:torbscript] implement: PR #284 open, CI green (
617f8b16).Token usage: roughly 1.5M tokens over two contexts; the CI measurement waits took most of it.
next: waiting (merge gate review of PR #284)
[loop:torbscript] PR #284 merged by argus (
fced7f58). Issue closed by the merge; no release or deploy step in the PR. Open: the compiler-size rows of the PR table were measured on the pre-rebase base; main CI forfced7f58is still running. If its artifacts differ, correct the table in docs/PERFORMANCE.md via a follow-up PR.next: none (done)
forover a concrete list is a counted loop #58