From c85386b1692ea4af5ad52aa383a49576f7f50d2e Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Mon, 18 May 2026 21:44:39 +0300 Subject: [PATCH] bench: record gcy (inline nested-message decode) post-mortem Implemented and benchmarked the inline-nested-decode plan from tarantool-protobuf-gcy. Test suite and JIT gate both pass, but median-of-3 bench shows 3-8% regressions on Person 1KB/10KB/100KB encode AND decode. Profile's "100% interpreter bail at Address_decode call" turned out to be a vl trace-attribution artifact; LuaJIT was already handling the call well. Lesson recorded in bench/PERF_LOG.md so the next person who reads the profile entry knows the obvious-looking inline transformation does not deliver here. Code change reverted; only the writeup lands. beads-tarantool-protobuf-gcy --- bench/PERF_LOG.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/bench/PERF_LOG.md b/bench/PERF_LOG.md index 17daceaddfa0eacfca1a7e31633f80bcf2774b87..c4315be73aa492daaf68f7e47772abd2e241fa00 100644 --- a/bench/PERF_LOG.md +++ b/bench/PERF_LOG.md @@ -84,6 +84,81 @@ macOS arm64. +## 2026-05-18 — gcy: inline nested-message decode at the call site (REVERTED) + +**Task:** [tarantool-protobuf-gcy] Decoder: inline nested-message decode at +the call site. Profile flagged `result.address = M.Address_decode(payload)` +at hello_pb.lua:1031 as a 100% interpreter bail in the `vl` trace; thesis +was that replacing the call with the inlined Address decode body would +eliminate the bail and lift Person decode by 20–40%. + +**Change attempted:** In `cmd/protoc-gen-tarantool/internal/gen/inline.go`, +added `inlineCandidateForDecode` (singular, non-group, non-WKT message +fields whose target has no further message subfields) and +`emitInlineNestedDecode`, which emits Address's decode loop inline in +Person_decode using a `do/end` scope to shadow the outer `result`. The +length prefix was read in-place (no `wire.decode_len` substring alloc). + +**Tests:** 745/745 pass. JIT trace gate: 37/37 (after a flaky first-run +0/37 caused by the documented macOS arm64 mcode alloc issue). + +**Bench (hello.Person — full, msgs/s, median of 3 runs):** + +| size | dir | h8v baseline | after gcy | Δ | +|-------|-----|--------------|-----------|----------| +| 1KB | enc | 302,117 | 292,680 | **-3.0%** | +| 1KB | dec | 110,115 | 104,182 | **-5.3%** | +| 10KB | enc | 64,052 | 59,543 | **-6.4%** | +| 10KB | dec | 15,086 | 14,113 | **-6.6%** | +| 100KB | enc | 6,487 | 6,661 | +2.7% | +| 100KB | dec | 1,519 | 1,396 | **-8.1%** | + +**Outcome: reverted.** Tests passed but the change is a net regression at +every size that exercises the inlined nested decode (Person 1KB/10KB +have Address embedded; 100KB grows the Address body but is dominated by +emails). Encode also regressed because Person_encode's trace had to +account for a larger Person_decode (shared mcode arena / instruction +cache pressure on macOS arm64, or LuaJIT abandoning some inlining of +Person_encode under the new pressure). + +**Why the profile claim didn't translate:** + +1. `vl` (and `jit.p` count) reported `Address_decode` as 100% Interpreted + for the *parent line*, but in the live benchmark LuaJIT was already + inlining the small Address_decode body into Person_decode's trace + when entering. The "bail" was a tooling artifact of how + `jit.attach('trace')` attributes side traces, not a sustained + interpreter fallback. The benchmark numbers refute the trace + interpretation. +2. Person_decode trace went from N stops to **22 stops** after the + inline. Larger root traces compile more slowly, are more sensitive + to side-trace stitching limits, and produce more mcode — exactly + the failure mode CLAUDE.md's "Keep hot wire helpers small" warning + describes, applied at the call-site instead of the helper. +3. The inlined-body's `do/end` scope with shadowed `result` may + introduce extra upvalue references that LuaJIT 2.1 doesn't optimize + as well as a plain function call into a trace it has already + inlined. + +**What would actually help here:** the *real* decode bottleneck on +hello.Person (per profile recap) is still `decode_string` (utf8 +validation = ~16% of decode time), `decode_tag` (21%), and +`list[#list+1] = val` at the email append loop (6%). Those are the next +targets — see entries on **6bb** (skip_utf8_validation), **4kj** +(generated tag/length fast path), **cch** (local counter for repeated +append). + +**Beads:** issue closed with `--reason` referencing this entry; not +reopened. Inlining nested decode bodies can come back if a workload +shows that the nested call site is *the* hot trace boundary, but the +candidate restriction + bench coupling needs to be rethought first +(measure each call-site in isolation, not just at the trace level). + +**Commit:** none — change reverted before commit. PERF_LOG entry is the +only artifact. + +--- + ## 2026-05-18 — h8v: inline 1-byte varint length prefix at codegen sites **Task:** [tarantool-protobuf-h8v] Encoder: codegen-time inline FFI writes (mode=full)