Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
Native record-literal templates: experiment report
Status: implemented and validated on the isolated performance branch. This is an AOT-native optimization only. It does not add an mcode operation, alter Mach, introduce shapes, or change the runtime record representation.
Composed integration baseline: 530a9fcf, which includes unified native
suspension, raw numeric array results, streamlined native frame activation,
checked native array-load folding, and checked Mach array-store fusion. The
record patch replayed without a textual conflict, but the lowering interaction
was audited and retested explicitly; see the final section.
Why this experiment
An ordinary literal allocates an exact-capacity hash record and then performs one general record insertion per field. The key hash, linear probe, stone check, possible growth check, length update, and generic helper boundary are redundant for a fresh literal whose ordered keys are already known to the compiler.
The earlier estimate in perf-next.md was that record templates could move
record_new from about 18.7 ms toward 8-12 ms. The direction was right, but the
endpoint was optimistic: on the current baseline the controlled native median
moved from 21.55 ms to 14.04 ms. Template construction removes 7.51 ms (34.8%),
but allocation/collection and the benchmark’s later field reads remain.
Design
For each profitable, proven-safe literal, QBE emission interns its ordered key set as a small writable descriptor:
u32 count
u32 mask
atomic u32 initialization_state
u32 reserved
u64 key_or_c_string[mask] // ordinary record slots 1..mask
The compiler calculates the ordinary FASH64 hash and collision chain exactly. The first execution converts the descriptor’s C-string pointers to actor-independent immediate-ASCII PitValues. It then recomputes every hash and probe with the runtime implementation. A mismatch is a loud disruption, never a silently malformed record. The immutable descriptor is shared by every actor using that native module.
Allocation remains one normal PitRecord allocation, with the same mask and
object size as Pit_NewRecordCap. Its mandatory slot-initialization loop seeds
the keys and null values directly; there is no second template copy. Generated
construction stores then write the already-known value offsets. Pointer-capable
values retain the generational write barrier. If evaluation of an earlier field
could collect, the generated store follows the ordinary forwarding chain before
writing. A no-allocation sequence such as record_new omits that chase.
The result is an ordinary mutable hash record. General reads, writes, deletion, reinsertion, growth, enumeration, stone, GC, and C APIs are unchanged. There is no template pointer or shape ID in each object.
Eligibility and semantic boundaries
The recognizer runs over final streamlined instructions, so its facts never
survive an instruction-mutating pass. It accepts only the single deterministic
normal path from record through all construction stores. It rejects:
- disruption handlers, unknown branches, loops inside construction, captured construction temporaries, or observation/escape before the final store;
- dynamic, non-ASCII, longer-than-seven-byte, or duplicate keys;
- null or nullable values, because assigning null means deleting the key;
- current
num/floatfacts, whose canonicalization can still produce null; - dynamic/apply calls or a call whose result is not proven non-null.
Exact int, text, logical, array, record, function, and blob values qualify.
A resolved call qualifies only when every reachable explicit return of its
callee has an exact non-null representation type. Unknown facts lose the
optimization rather than weakening semantics.
The original evaluation order and every call/suspension point remain. A fresh partly initialized record is held in its ordinary rooted frame slot but is not observable by a callee. Functions with local disruption handlers are rejected; actor preemption may freeze the frame and heap, then resumes the same stores.
The generated fresh-record is_stone guard and its untaken branch are omitted.
Every later mutation keeps the original guard. No semantic mcode or Mach
instruction is changed.
ROM and memory policy
The target-ROM gate requires a literal to be enclosed by a backedge or live in a directly recursive function. This keeps one-shot setup records on the exact old path. Descriptors are deduplicated by ordered key set within a native module:
- mask 3 (one or two fields): 40 bytes;
- mask 7 (three through five fields): 72 bytes;
- in general: 16 + 8 * mask bytes on the current 64-bit ABI.
record_new adds one 72-byte descriptor. binarytrees adds one 40-byte
descriptor for its non-leaf two-field nodes; the nullable leaf literal remains
generic. The adversarial regression adds 112 bytes across two descriptors.
There is no per-record memory increase and no extra allocation. Controlled
native allocation traffic was unchanged for record_new (42,205 KiB in both
arms). binarytrees and the adversarial regression differed by 6 KiB and 3 KiB
respectively over tens of MiB, in the favorable direction and within observer
noise. The ordinary three-field record_new object remains 144 bytes.
The shared host runtime cost, measured by rebuilding exact commit 7e3bbc90
with the same release toolchain, is:
- dylib file: 1,454,488 -> 1,454,632 bytes (+144 bytes after link layout);
__text: 1,025,852 -> 1,027,052 bytes (+1,200);__cstring: 56,537 -> 56,609 bytes (+72);- total populated
__TEXTsections: +1,280 bytes.
This is a favorable trade on 4 MiB-class targets: about 1.25 KiB of shared runtime text plus 40-72 bytes per hot key set buys material execution wins, without multiplying memory by actor or allocation count. A static console link may dead-strip the helper when no selected literal references it; that has not yet been measured with a console linker.
Controlled runtime and code-size results
Both arms used the same source and runtime. A single compiler constant disabled only descriptor selection. Each runtime number is the median of seven runs.
| benchmark | native off | native on | change | QBE IL off -> on | assembly off -> on |
|---|---|---|---|---|---|
record_new | 21.55 ms | 14.04 ms | -34.8% | 63,707 -> 59,465 B | 47,906 -> 43,361 B |
binarytrees | 52.62 ms | 44.95 ms | -14.6% | 152,905 -> 149,046 B | 105,055 -> 98,282 B |
d_field | 11.15 ms | 11.10 ms | noise | 213,994 -> 213,994 B | 138,911 -> 138,911 B |
record_template | 29.05 ms | 26.81 ms | -7.7% | 223,775 -> 216,915 B | 147,186 -> 139,509 B |
Mach medians and instruction counts were unchanged apart from timing noise:
record_new 30.67/30.21 ms and 10,505,170 instructions; binarytrees
83.13/82.77 ms and 22,241,522 instructions. d_field’s complete emitted QBE
hash was byte-identical with templates on and off
(35b94713bb023d2170b6f91906c41881bf288468302bedae0aa2e5199749b4c2).
The reduction in generated code is itself useful on small systems: each fixed construction store replaces generic probing and helper/error control flow with one fixed offset write, plus a chase/barrier only when required.
Composed integration result
After replay onto 530a9fcf, one four-row run measured the medians below. The
record fixture shown in that run was the raw-context suspension variant; the
full enumeration fixture was then restored and checked separately so its
original semantic coverage was not weakened.
| benchmark | Mach | native | QBE IL | assembly | descriptors / calls |
|---|---|---|---|---|---|
record_new | 30.75 ms | 14.48 ms | 61,638 B | 44,474 B | 1 / 1 |
binarytrees | 86.42 ms | 46.77 ms | 159,582 B | 104,190 B | 1 / 1 |
d_field | 23.73 ms | 11.45 ms | 220,518 B | 142,656 B | 0 / 0 |
record_template_suspend | 20.32 ms | 14.61 ms | 189,902 B | 121,505 B | 2 / 2 |
All four results matched. A later focused exact run of the restored
record_template fixture measured 38.56 ms Mach and 26.24 ms native, with both
lanes returning its independent golden result 853729; the suspension variant
measured 19.86/14.82 ms and returned 813729 in both lanes. These absolute
numbers are not an off/on comparison and include the composed branch’s native
suspension and raw-array work.
The composed emitted-IL hashes were 590547eb4c28bb35... (record_new),
174cf303f4a0cee6... (binarytrees), 5c2ff99f8eea2caf... (d_field),
1ccdd3fdb2f7b31e... (record_template), and ed5e11c978e5b840...
(record_template_suspend). Descriptor/helper-call counts confirm narrow
selection: 1/1, 1/1, 0/0, 2/2, and 2/2 respectively. In particular,
d_field neither pays for nor enters the template path.
Correctness coverage
- Compiler tests require a hot exact literal to select the helper, reject a nullable value, reject cold literals, and accept a recursive resolved non-null construction.
record_capacity_testverifies seeded key positions and length, ordinary existing-key writes, deletion tombstones, reinsertion, and unchanged capacity.aot_bench/regress/record_template.ceforces GC while a partially constructed record crosses a recursive call and array allocation. It also covers a nullable generic literal, deletion, reinsertion, key growth, retained records, reads, and stone. Mach, native, and the independent golden result are 853729.aot_bench/regress/record_template_suspend.ceis the raw-context counterpart used by the standalone native suspension harness. It deliberately omits only the context builtinarray(record), which a raw context cannot resolve; the original fixture retains all enumeration/length checks. An identical-runner high-budget control returned 813729 with zero suspensions. With budget 64 andPIT_SUSP_TEST_GC=1, it returned the same 813729 after 1,562 suspensions, maximum native depth 3, and 1,563 copying collections.- Runtime descriptor priming verifies compiler/runtime hash and collision agreement before the first optimized allocation.
Audit notes and remaining limits
The runtime functions added here perform either no Pit allocation or exactly one record allocation. No PitValue local is held across a second allocation; the C GC-rooting checklist therefore requires no additional frame in the new runtime helper. The native capacity test roots its record throughout mutation.
This experiment deliberately does not optimize literals containing nullable fields or arbitrary heap-text keys. Supporting the latter would require a rooted actor/runtime key table or per-actor priming and is not justified by the current evidence. A future structured value IR could replace the localized return/type proof, but it is not required for this win.
The implementation is larger in the compiler (exact hash arithmetic plus a strict path recognizer), and compile-time allocation/speed will regress. That is accepted for this campaign: generated-program speed and target memory are the gates. Before production merge, the code should still receive a dedicated proof audit for every mcode operation classified as non-allocating.
Composed suspension/raw-array audit
The composed emitter retains both independent lowering systems:
- raw array result candidates still carry their alternate/join/no-reload maps, emit the nan64 raw-load helper only when selected, and participate in the existing frame-write reload exclusion;
- native resume segments and exact promoted-slot liveness are still computed
from canonical mcode before emission, including
storeoperands; - a record construction temporary is a pointer and therefore never enters the numeric/bool promotion set. It remains in its normal GC-scanned frame slot across every resumable field call;
- when a field call suspends, the abandoned C stack is irrelevant to the
template. Resume reconstructs
%fp; the fixed store reads the record again from that frame and follows a forwarding header when the preceding evaluation could have collected; - pointer-valued fields, including a nested array literal or resolved record call, still execute the generational barrier after the fixed write; and
- the template allocator uses the same allocation tail as the ordinary record helper. It introduces no suspension point or continuation state of its own.
Thus the composition does not make a native activation unfreezable and does not turn a raw numeric array value into a tagged record field by accident. The adversarial record fixture covers a recursive call, nested array allocation, copying GC pressure, and later ordinary record operations in one construction. The separate suspension fixture proves the same templated partial construction survives repeated native frame freezing, resume reconstruction, and object forwarding under a collection after every resume.
Source: plans/archive/perf-2026-07/perf-native-record-templates.md