Graven

Generated code (zidlc) against the hand-written code

Historical document. Written on 2026-11-13, when the project was called zidl (later Graven, with the compiler gravenc).

Date: 2026-11-13 · Plan 2 (the compiler), Task 8: zidl-schemas drops the hand-written code and includes the code generated by zidlc via build.rs. Machine: Intel Core i9-14900HX, Linux 7.3, rustc 1.94.0, --release ([profile.bench] with debug = true). Machine load: 1-minute average between 1.7 and 4.6 during the runs (a browser open in the background).

The plan's criterion: within ±5% of the pre-plan state, measured with interleaved A/B runs.

Methodology

Commands (equivalent): cargo bench -p zidl-bench --bench codec -- <group> --warm-up-time 1 --measurement-time 3 for write, read_all, read_one, read_all_verified and envelope.

First measurement: two real code differences

In the first A/B round (the generated code as Task 7 left it), two groups of rows were consistently outside ±5%: the 3 measurements of B were above the 3 of A.

row pre-plan generated Δ
read_all_verified/zidl/bank/10 111.2 123.3 +10.9%
read_all_verified/zidl/bank/100 876.7 978.2 +11.6%
envelope/read_verified_one/bank/10 86.2 99.8 +15.8%
envelope/read_verified_all/bank/10 110.6 124.9 +13.0%
read_all_verified/zidl/chunk/64 4.4 5.2 +17.0%

A 3-sided A/B (A, C and B, interleaved, median of 3) showed that these differences came from Task 8, that is, from generated against hand-written code over the same runtime. Examples: envelope/read_verified_one/bank/10 measured C 87.3 and B 101.6 (+16%); read_all_verified/zidl/chunk/64 measured C 4.7 and B 5.3 (+14%).

Bisection (binaries with a single generated file swapped into t8-base, interleaved, median of 3, ns):

row C (by hand) only stream generated only bank generated bank generated with the hand-written order
envelope/read_verified_one/bank/10 94.8 — 104.2 94.8
read_all_verified/zidl/bank/10 120.5 — 128.1 119.4

Causes and fixes, both in the codegen (crates/zidl-compiler/src/codegen/read.rs):

  1. The verify order of bank.Transaction. The generated code verified the fields in ID order (entries @5 and only then note @6). The hand-written code verified note first and then the list. Just swapping the order in a generated bank.rs brings back the hand-written time (table above). Fix: the generated FromTable::verify now visits the leaf data first (string, bytes, lists without tables) and then the fields that descend (a nested table, a union, [Table]), in ID order within each group. The helper function is descends(). This changes no result: the precedence between errors is not normative (spec §7.5) and the budget's sum does not depend on the order (§7.3). The gates still pass: conformance vectors, budget_totals_unchanged and the mutation gate.
  2. The shape of the charge for a required string/bytes (stream.Chunk.data). The generated code emitted if let Some(b) = self.0.bytes(..)? { ctx.charge(4 + b.len())?; }. The hand-written code emitted ctx.charge(4 + data.len())? unconditionally. LLVM orders the blocks differently in the two cases. Fix: for a required field, the generated code now emits ctx.charge(4 + self.0.bytes(..)?.unwrap_or(&[]).len())? (and the equivalent with str). The unwrap_or arm is unreachable, because from_ref already rejected the absent pointer with Missing. With that, the machine code of read_all_chunk_verified is identical to the hand-written one.

The snapshots in crates/zidl-compiler/tests/snapshots were re-recorded. The diff contains only these two changes, plus the sentence in the verify doc.

Proof that the code is the same

I disassembled (objdump -d) the 116 functions with zidl in their name from the bench binary (the zidl_bench::zidl_impl::* ones with everything inlined into them, plus the runtime functions that were not inlined). I compared the final binary (B) with the hand-written t8-base (C), normalizing addresses and the numbers of anonymous symbols.

The rest is code layout

With identical machine code, the remaining differences come from layout (the address and alignment of the functions and hot loops in the binary). Evidence:

So the remaining differences against the pre-plan mix two things: (a) the runtime changes of Tasks 1–7 (A → C, different machine code, outside the scope of this measurement) and (b) layout. In Task 8 (C → B) the machine code is the same, except for telemetry.

Final table (pre-plan × generated, after both fixes)

Final binary, interleaved A/B, median of 3 rounds, ns. ⚠ = outside ±5%. See the previous sections: in the bank, frame and chunk rows, the generated machine code is identical to the hand-written one (t8-base).

group row pre-plan (by hand) generated Δ n
write zidl/bank/1 33.1 33.0 -0.2% 3/3
write zidl_known/bank/1 24.9 24.7 -1.0% 3/3
write zidl/bank/10 98.9 94.4 -4.5% 3/3
write zidl_known/bank/10 59.6 60.4 +1.4% 3/3
write zidl/bank/100 743.5 708.2 -4.7% 3/3
write zidl_known/bank/100 406.4 409.1 +0.7% 3/3
write zidl/frame/10 28.1 28.0 -0.5% 3/3
write zidl/frame/500 843.6 841.4 -0.3% 3/3
write zidl_owned/chunk/64 28.9 28.6 -1.2% 3/3
write zidl_batched/chunk/64 12.2 12.0 -0.9% 3/3
write zidl_owned/telemetry 24.6 23.7 -3.7% 3/3
write zidl_batched/telemetry 8.4 7.8 -7.2% ⚠ 3/3
write zidl_batched/frame/500 1024.1 1031.1 +0.7% 3/3
write zidl_owned/frame/500 989.8 980.6 -0.9% 3/3
write zidl/telemetry 7.3 6.5 -11.0% ⚠ 3/3
write zidl/chunk/64 10.6 10.6 +0.8% 3/3
write zidl/chunk/65536 1042.9 1088.0 +4.3% 3/3
read_all zidl/bank/1 34.6 36.3 +4.9% 3/3
read_all zidl_trusted/bank/1 7.8 7.9 +1.0% 3/3
read_all zidl/bank/10 89.4 92.4 +3.4% 3/3
read_all zidl_trusted/bank/10 25.5 25.7 +0.8% 3/3
read_all zidl/bank/100 732.2 742.4 +1.4% 3/3
read_all zidl_trusted/bank/100 210.7 207.6 -1.5% 3/3
read_all zidl/frame/10 64.0 70.4 +10.0% ⚠ 3/3
read_all zidl/frame/500 2400.1 3318.8 +38.3% ⚠ 3/3
read_all zidl/telemetry 1.6 1.7 +10.2% ⚠ 3/3
read_all zidl/chunk/64 2.6 2.9 +11.2% ⚠ 3/3
read_all zidl/chunk/65536 2.5 2.8 +11.1% ⚠ 3/3
read_one zidl/bank/1 1.5 1.8 +23.2% ⚠ 3/3
read_one zidl/bank/10 1.5 1.8 +23.0% ⚠ 3/3
read_one zidl/bank/100 1.5 1.8 +23.2% ⚠ 3/3
read_one zidl/frame/10 3.5 3.4 -3.5% 3/3
read_one zidl/frame/500 3.5 3.3 -5.4% ⚠ 3/3
read_one zidl/telemetry 1.5 1.5 -2.4% 3/3
read_one zidl/chunk/64 1.5 1.7 +10.7% ⚠ 3/3
read_one zidl/chunk/65536 1.5 1.7 +12.3% ⚠ 3/3
read_all_verified zidl/bank/1 44.6 47.5 +6.6% ⚠ 3/3
read_all_verified zidl/bank/10 118.3 133.0 +12.4% ⚠ 3/3
read_all_verified zidl/bank/100 931.8 1037.3 +11.3% ⚠ 3/3
read_all_verified zidl/frame/10 72.9 81.3 +11.6% ⚠ 3/3
read_all_verified zidl/frame/500 2778.3 3759.3 +35.3% ⚠ 3/3
read_all_verified zidl/telemetry 3.7 4.2 +14.1% ⚠ 3/3
read_all_verified zidl/chunk/64 4.8 5.9 +21.5% ⚠ 3/3
read_all_verified zidl/chunk/65536 4.9 5.8 +17.8% ⚠ 3/3
envelope write_owned/telemetry 23.9 23.9 +0.2% 3/3
envelope write_enveloped/telemetry 35.2 35.5 +1.0% 3/3
envelope read_verified_one/telemetry 2.1 2.4 +16.5% ⚠ 3/3
envelope read_from_bytes_one/telemetry 13.3 12.0 -10.4% ⚠ 3/3
envelope write_owned/chunk/64 29.1 28.7 -1.3% 3/3
envelope write_enveloped/chunk/64 43.5 43.7 +0.4% 3/3
envelope read_verified_one/chunk/64 2.9 3.1 +5.1% ⚠ 3/3
envelope read_from_bytes_one/chunk/64 13.6 12.3 -9.3% ⚠ 3/3
envelope write_owned/chunk/65536 1650.1 1639.1 -0.7% 3/3
envelope write_enveloped/chunk/65536 2775.0 2766.6 -0.3% 3/3
envelope read_verified_one/chunk/65536 3.0 3.1 +4.7% 3/3
envelope read_from_bytes_one/chunk/65536 1794.6 1802.6 +0.4% 3/3
envelope write_owned/bank/10 78.2 79.6 +1.9% 3/3
envelope write_enveloped/bank/10 118.6 117.6 -0.9% 3/3
envelope read_verified_one/bank/10 92.5 106.1 +14.7% ⚠ 3/3
envelope read_from_bytes_one/bank/10 119.8 126.4 +5.5% ⚠ 3/3
envelope read_verified_all/bank/10 120.3 133.7 +11.1% ⚠ 3/3
envelope read_from_bytes_all/bank/10 148.3 154.4 +4.1% 3/3

Verdict

item verdict
Writing (write, envelope/write_*) ✅ within ±5% in every row, except write/zidl(_batched)/telemetry, which got faster (−7% to −11%, ~0.7 ns)
Checked reading of bank (read_all, zidl_trusted) ✅ within ±5%
Real code differences introduced by the generated code ✅ fixed in the codegen (the verify order; the charge for a required string/bytes). After that, 111/116 bench functions have machine code identical to the hand-written one
Remaining ⚠ rows (frame, chunk, verified bank, read_one/bank) layout/alignment noise, accepted. The machine code is identical to the hand-written one. The same identical function swings from −3% to +12% between binaries. With aligned loops, frame/500 drops from +38% to +5–7%
telemetry (5 functions with +2 instructions) accepted: the REQ order is ID order (a Task 3 decision, same as the interpreter). An effect of ~0.1–0.2 ns with a varying sign. Not changed

The next plan that wants stable numbers against snapshots should pin the layout. Options: build both sides with -C llvm-args=-align-loops=64 (or -align-all-functions), or compare instruction counts (perf stat -e instructions) instead of time. This kind of measurement can be off by ±10–40% because of layout in this binary.

Note: the final 5-sided round (A, C, B and A/B with aligned loops, every group) was cut short by the machine-time limit. The final table comes from the last complete 2-sided round. The control numbers (C, K, aligned loops) come from the focused rounds described above.