121. Inactive Work-Control Checks in the Generated Go Runtime
- Status: Implemented
- Date: 2026-10-25
Problem and Mechanism
A generated parser that is used without a work control still runs the runtime helpers that can
charge one. Each such helper tests the control, and the compiler can inline it only when its body
is small. The helpers are therefore split: the test, the call of a ...Controlled function that
holds the charging body, and the ordinary body stay in the helper; the controlled body is a
separate unexported function. This covers appendWork, copyWork, clearWork, growWork, the
typed memoGet and memoPut, valueField, setValueField, the typed and node action contexts
(And, Or, Unary, Fold, ListItems, asNode), stringBinary, compareText,
equalExpected, sortExpected and evalErrorf.
FirstCall runs the ordinary body itself after controlledFirstCallPrepare, which does the
controlled charges and growth, so a controlled parse executes the same steps. The operator
apply of both runtimes replays recovery errors only when there are some (an empty append
changes nothing), through an out-of-line function, so apply inlines.
In the runtime build only appendWork, copyWork, clearWork and the two apply functions
inline; the other helpers lose a call layer or a large frame but stay calls (the typed memoPut
costs 152 and Parser.memoPut 81 against a budget of 80 even without the check). Charges,
cancellation points, limits and results of controlled parses are as in the unsplit helpers.
Results
Reference control: the same source with every work-control check deleted at build time (the ceiling,
not a supported build). Generated parsers of parsers/json, duckdb, cel and golang (Node,
recognizer and typed, generated without work controls, embedded runtime), inputs of the benchmark
suite. Ten interleaved rounds over the build before, the build after, the control and a copy of the
build before, -benchtime=500ms; medians as the change against the build before (the copy of the
build before stayed within ±1.2%):
| Benchmark | Before | After | Control |
|---|---|---|---|
| json node | 5.08 ms | -1.8% | -2.6% |
| json recognize | 1.80 ms | -2.4% | +1.4% |
| json typed | 3.44 ms | -2.9% | -5.6% |
| duckdb node | 70.9 ms | -2.2% | -6.5% |
| duckdb recognize | 42.7 ms | -1.8% | -6.0% |
| duckdb typed | 58.9 ms | -3.4% | -9.8% |
| cel node | 0.40 ms | -1.5% | -2.2% |
| cel recognize | 0.14 ms | -0.2% | -0.1% |
| cel typed | 0.35 ms | -1.7% | -4.2% |
| golang node | 34.5 ms | -0.8% | -2.8% |
| golang recognize | 34.1 ms | -1.3% | -3.8% |
| golang typed | 26.0 ms | -1.9% | -6.8% |
Bytes and allocations per operation are identical. The generated parser.go files are unchanged
(the change is in the runtime). A test binary linking all eight parsers grows from 28,715,058 to
28,775,698 bytes (+0.2%).
Limits
Of the gap to the control, between 28% (golang typed) and 69% (json node) is recovered; on most
rows 65-72% remains. The json recognize control is slower than the build before, and cel
recognize has no gap. What remains is the checks in Expect, Text, memoEnv, bind and the
other helpers that do more than one thing, the helpers that stay calls (memoGet, memoPut,
valueField), and code layout. Splitting those further needs a second call that costs as much
as the check.