~bigbes/tarantool

tarantool-protobuf

c85386b1692ea4af5ad52aa383a49576f7f50d2e — Eugene Blikh 3 months ago 9ee21c0
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
1 files changed, 75 insertions(+), 0 deletions(-)

M bench/PERF_LOG.md
M bench/PERF_LOG.md => bench/PERF_LOG.md +75 -0
@@ 84,6 84,81 @@ macOS arm64.

<!-- Append new entries below this line. Newest at the top. -->

## 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)