~bigbes/tarantool

tarantool-protobuf

b624a6bf75cfdf7ff59c14073e6b1ecc21f9f143 — Eugene Blikh 2 months ago 48ef418
beads: close bgu (ffi.string decode disproven) + memory note on buffer-reuse trap

Two-round microbench shows ffi.string(scratch + off, len) loses 2-2.7x
to buf:sub even with a pre-allocated stable scratch cdata and ffi.copy
amortized over 26 strings. The per-call pointer-arith cdata crosses
the ffi.string frame and can't be sunk — same root cause as a6n.
2 files changed, 3 insertions(+), 2 deletions(-)

M .beads/interactions.jsonl
M .beads/issues.jsonl
M .beads/interactions.jsonl => .beads/interactions.jsonl +1 -0
@@ 35,3 35,4 @@
{"id":"int-2a7d9cba","kind":"field_change","created_at":"2026-05-23T20:04:23.62044Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-43t","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Parity gate landed. Added test-c / test-all / bench-c Justfile recipes; bench.lua relabels runtime→c-runtime under PB_ENABLE_C=1 and refuses --baseline/--compare in that mode. Reuses existing 1043-test luatest matrix, Google conformance suite, and interop fixtures via the transitivity-through-reference strategy. c-generated column dropped from scope (c0i deferred per docs/c-accel.md); starwing column lives in bench/starwing_bench.lua separately."}}
{"id":"int-a631a4f9","kind":"field_change","created_at":"2026-05-23T20:13:43.682922Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-74c","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Phase-2 EmmyLua annotations landed. Added ---@param/---@return on public surface of codec.lua (encode_message/decode_message/encode_field/compile_writers/compile_readers/merge_message), grpc.lua (loopback/multiplex/new_stream_pair/wrap_call/wrap_server_stream/wrap_server_view), json.lua (M.encode/M.decode with new pb.JsonEncodeOpts/pb.JsonDecodeOpts shapes), and wkt.lua (register/lookup/any_pack/any_unpack). Also corrected two pre-existing _types.lua signature lies: pb.register is (desc), not (full_name, desc); pb.any.pack is (desc, t, prefix?), not (t, type_url). just test 752/752, just test-c 1043/1043."}}
{"id":"int-37f11812","kind":"field_change","created_at":"2026-05-23T20:48:32.931008Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-a6n","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Inlined 1-byte LEN fast path for string/bytes scalar + repeated fields in generated full-mode _decode; skips wire.decode_string/_bytes function call frame and inlines utf8_len. Dropped the U8CP cast-at-top idea — 24B/decode cdata allocation that LuaJIT couldn't sink, regressed small messages. Person_decode full: 10B -6.9%, 100B -7.2%, 1KB -10.7%, 10KB -12.2%, 100KB -14.0%. Zero alloc impact. Tests: 1043 pass, 37 JIT trace checks pass."}}
{"id":"int-1ae608c3","kind":"field_change","created_at":"2026-05-24T04:20:10.628185Z","actor":"Eugene Blikh","issue_id":"tarantool-protobuf-bgu","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Spike (bench/c_accel/bgu_probe.lua, removed) measured ffi.string vs buf:sub for the 1-byte LEN string fast path on Tarantool 3.8.0/LuaJIT 2.1.0-beta3 / Apple M-series. Results (ns/call):\n\n  size  buf:sub  per-call-cast  amortized/26  amortized/4\n  10B   89.7     280.5 (3.1x)   224.9 (2.5x)  246.2 (2.7x)\n  32B   107.6    299.4 (2.8x)   243.8 (2.3x)  260.9 (2.4x)\n  80B   154.3    362.4 (2.3x)   300.1 (1.9x)  314.9 (2.0x)\n\nV1 (per-call ffi.cast(U8CP, buf) inside decode_string): 2.3-3.1x slower.\nV2 (cast hoisted to caller, threaded as ptr arg, amortized over 26 emails — the Person 1KB peak shape): still 1.9-2.5x slower. Cast-once-amortize-many fails because 'ptr + np - 1' mints a fresh cdata wrapper per call that the JIT cannot sink across the decode_string frame — same root cause as a6n's failed top-of-_decode cast. The amortization helps a little (V2 < V1) but does not close the gap.\n\nConclusion: bgu's premise (ffi.string lets the JIT fuse the string read with surrounding code) does not hold on the current LuaJIT fork — the cdata allocation cost dominates any reduction in dispatch. Closing as superseded by a6n-decode-inline-len-fast-path-2026-05-23. Revival criteria: a measurable cdata-sink win on a future LuaJIT (likely needs upstream allocation-sink improvements for pointer-arith cdata), OR an API redesign where the decoder works on a cdata buffer end-to-end (not a Lua string)."}}

M .beads/issues.jsonl => .beads/issues.jsonl +2 -2
@@ 26,7 26,7 @@
{"_type":"issue","id":"tarantool-protobuf-qwt","title":"Codegen: emit per-extension writers for proto2 extensions (skip pb.codec.encode_field dispatch)","description":"proto2 extensions are the slowest line in the bench: proto2_basic.BenchPayload 'min' fixture encodes at 11.0 MB/s — vs 281 MB/s for the 100B Person. Cause: generated _extensions walk in mode=full falls through to pb.codec.encode_field, the slow runtime dispatch that mode=full otherwise avoids.\n\nFix: at codegen time, when 'extend Foo { ... ext_count = 100; ... }' is seen, emit a dedicated writer per extension. The Foo_encode body's extensions block becomes:\n\n  local _ev = _exts['proto2_basic.ext_count']\n  if _ev ~= nil then\n      -- direct inline write for the int32 ext, just like a regular int32 field\n      n = n + 1; out[n] = '\u003cprecomputed tag bytes\u003e'\n      n = n + 1; out[n] = wire.encode_int32(_ev)\n  end\n  -- repeat per extension\n\nDecoder side: extensions_by_id[id] dispatch in the else branch currently calls pb.codec.decode_extension; can similarly be replaced by inline-emitted per-extension decoder blocks alongside the regular field branches.\n\nConcretely closes the proto2 min throughput gap (3.4x vs Go apiv2) which is bench's worst data point. Has zero impact on payloads without extensions.","notes":"Generated code grows by one if-block per registered extension. For a message with 50 extensions this could be significant — but proto2 extensions are typically used sparingly. Worth gating on extension count if size becomes a problem.\n\nExisting extensions_by_id / extensions_list / extensions_by_full_name registries can stay (they're used by text/json/runtime dispatch); the fast path just bypasses them in mode=full.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T17:14:38Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T17:14:38Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-ch2","title":"Encoder: JIT-friendly map\u003cK,V\u003e field iteration (avoid pairs() ISNEXT NYI)","description":"map\u003cK,V\u003e field encoding walks the user-supplied table with pairs() — runtime/pb/codec.lua:162 and every codegen-emitted map block in mode=full. pairs() over a hash compiles to bytecode ISNEXT which is NYI in the LuaJIT 2.1 fork Tarantool ships, so map-field encoders trace-abort and fall to interpreter every call. bench/jit_trace.lua already pins this as the only intentional NYI in the hot path.\n\nUnlike oneofs/extensions (which are flattened to *_list arrays at finalize-time in init.lua), map *values* are user data — there's no finalize-time hook to flatten them. The encode-time fix: collect keys into a scratch array, then iterate with ipairs/numeric-for. Pattern:\n\n  local _mkeys, _mn = {}, 0\n  for k in pairs(value) do _mn = _mn + 1; _mkeys[_mn] = k end\n  for _i = 1, _mn do\n    local k = _mkeys[_i]\n    local v = value[k]\n    -- emit entry as today\n  end\n\nCosts one extra alloc (scratch keys table) per map field per call but lets the inner emit-loop stay on a JIT trace. For maps with \u003e3 entries the trace-stable inner loop should net out positive. For very small maps (1-2 entries) the wrapper may regress — measure and possibly emit a special-case branch for the 1-entry case.\n\nPairs naturally with x9f (deterministic map option) — when on, the scratch keys array can be sorted, giving deterministic output for free vs the current pairs()-order-undefined behavior. Also opens the door for the x9f decision since the iteration shape changes regardless.","notes":"Generated code lives in protoc-gen-tarantool. Both runtime mode (codec.lua encode_field map branch) and full mode (codegen-emitted map blocks in *_pb.lua) need updating. Bench fixture: hello.Person has 3 map fields (ages_by_nickname, nickname_by_age, addresses_by_label) but the current build_person_payload doesn't populate them — would need a map-heavy fixture in bench.lua to measure the win.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T17:13:53Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T17:13:53Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-cch","title":"Decoder: use local counter instead of #list at repeated-field append","description":"jit.p profile (Person 1KB): line 1021 'list[#list + 1] = val' is 6.2% of total decode time. Each #list invocation re-traverses to find the array length. Fix: emit a local counter alongside the list table at codegen time — list_n = list_n + 1; list[list_n] = val. Already done on encode side ('out, n' pattern). Tiny 1-line codegen change. Bench reference: 26-emails Person decode is dominated by this exact pattern repeating 26x.","notes":"At decode start, when initializing a repeated-field list, also init the counter: 'local emails, emails_n = result.emails, #(result.emails or {}) ' — for the first-encounter case, counter starts at 0. Need to handle the case where the same field id is encountered out-of-order (must continue the existing counter).","status":"closed","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T16:52:38Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T19:01:17Z","started_at":"2026-05-18T18:56:39Z","closed_at":"2026-05-18T19:01:17Z","close_reason":"Local counter per repeated non-map field; median-of-3 decode wins +5.0% (1KB), +5.0% (10KB), +7.1% (100KB). Profile target was 6.2%; landed wins of 5-7%. Encode flat, runtime flat. 745/745 tests, 37/37 JIT. See bench/PERF_LOG.md.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-a6n","title":"Decoder: ffi-cast buffer ptr once per decode; replace buf:byte/buf:sub at fast paths","description":"Decode hot path uses buf:byte(pos) and buf:sub(start, end-1) — both are method calls dispatched via the string metatable per invocation. Wire.lua already has U8CP = ffi.typeof('const uint8_t*'). The fix: at the top of each generated Type_decode, cast once (local ptr = ffi.cast(U8CP, buf)) and use ptr[pos-1] for byte reads. For string returns, keep buf:sub (or use ffi.string per bgu issue) since the result must be a Lua string. Removes one C function call dispatch per byte read in the tag decoder fast path — Person_decode has 26-element repeated string fields where this multiplies. Compatible with bgu (the ffi.string change for decode_string) — both rely on the same cast.","notes":"Caveat: ffi.cast holds the string pinned. Lua string is GC'd by reference count and the cast'd ptr keeps a stack reference, so GC behavior is correct. Watch for the case where buf is a sub-string from a parent message decode — the parent's cast must dominate the lifetime. Easy fix: each Type_decode re-casts the buf it owns.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T16:51:24Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T20:34:10Z","started_at":"2026-05-23T20:34:10Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-a6n","title":"Decoder: ffi-cast buffer ptr once per decode; replace buf:byte/buf:sub at fast paths","description":"Decode hot path uses buf:byte(pos) and buf:sub(start, end-1) — both are method calls dispatched via the string metatable per invocation. Wire.lua already has U8CP = ffi.typeof('const uint8_t*'). The fix: at the top of each generated Type_decode, cast once (local ptr = ffi.cast(U8CP, buf)) and use ptr[pos-1] for byte reads. For string returns, keep buf:sub (or use ffi.string per bgu issue) since the result must be a Lua string. Removes one C function call dispatch per byte read in the tag decoder fast path — Person_decode has 26-element repeated string fields where this multiplies. Compatible with bgu (the ffi.string change for decode_string) — both rely on the same cast.","notes":"Caveat: ffi.cast holds the string pinned. Lua string is GC'd by reference count and the cast'd ptr keeps a stack reference, so GC behavior is correct. Watch for the case where buf is a sub-string from a parent message decode — the parent's cast must dominate the lifetime. Easy fix: each Type_decode re-casts the buf it owns.","status":"closed","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-18T16:51:24Z","created_by":"Eugene Blikh","updated_at":"2026-05-23T20:48:33Z","started_at":"2026-05-23T20:34:10Z","closed_at":"2026-05-23T20:48:33Z","close_reason":"Inlined 1-byte LEN fast path for string/bytes scalar + repeated fields in generated full-mode _decode; skips wire.decode_string/_bytes function call frame and inlines utf8_len. Dropped the U8CP cast-at-top idea — 24B/decode cdata allocation that LuaJIT couldn't sink, regressed small messages. Person_decode full: 10B -6.9%, 100B -7.2%, 1KB -10.7%, 10KB -12.2%, 100KB -14.0%. Zero alloc impact. Tests: 1043 pass, 37 JIT trace checks pass.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-6bb","title":"Decoder: opt-in skip_utf8_validation flag for trusted sources","description":"decode_string runs utf8_len (ICU) on every decoded string. Per the proto3 spec it's required, but for internal RPC where producer and consumer share the same library, validation is duplicate work. Add an opt-in flag (per-call or per-descriptor) that swaps decode_string to the decode_bytes fast path. Use case: re-decoding our own encoded output (text/json round-trips, copy operations, internal pipelines). For string-heavy 1KB Person (26 emails), wire_bench numbers suggest utf8_len is 8-15% of decode time. Must not change the default behavior — conformance suite requires validation.","notes":"Plumbing options: pb.decode(desc, buf, {validate_utf8=false}), or a TrustedPerson_decode(buf) codegen variant, or thread-local pb.set_validate_utf8(false). Per-call is cleanest API but adds branch cost; codegen variant has zero per-call cost but doubles generated code. Per-descriptor (desc.skip_validation) is a middle ground. Decide during implementation.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T16:51:23Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T16:58:51Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-kot","title":"Codegen: localize wire.* upvalues at top of each generated message function","description":"examples/expected/full/hello/hello_pb.lua has 106 wire.* accesses — each is a hash-table lookup on the upvalue. LuaJIT hoists this when traces stay hot, but every nested-message boundary breaks the trace (see gcy notes), forcing re-lookup in the side trace / interpreter. Fix: codegen-emit at the top of each Type_encode and Type_decode function the locals it actually uses — only for the wire.* entries referenced in that function body — so the in-body calls are direct LJ_FUNCC dispatches with no table lookup. One-line codegen change; broad impact whenever traces stitch poorly. Pairs naturally with h8v and 4kj (both move more work into the same generated function bodies, magnifying the per-call lookup cost). Expected: 3-8% across the board; larger when traces break.","notes":"Codegen-side change in the Go plugin (protoc-gen-tarantool). Pre-compute the set of wire.* symbols used in each function (e.g., {encode_varint, decode_string, decode_tag}) and emit 'local encode_varint = wire.encode_varint' style preambles. Keep the existing 'local wire = pb.wire' so non-emitted entries still work. Verify with bench/jit_trace.lua that we don't increase trace size past LuaJIT inline budget on large messages (test_messages_proto3 is 4212 lines).","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T16:51:23Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T16:51:23Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-lkz","title":"Encoder: single-pass two-phase (size + emit) into single buffer","description":"Eliminate the per-call out table and the per-field intermediate string allocations by emitting once into a pre-sized buffer. Two viable shapes (per drm notes):\n\n1. Two-pass: walk fields once to compute exact size, allocate one buffer, walk again to emit. Mirrors vtproto's Size()+MarshalTo() pattern — vtproto's 1-alloc encode is what gives it 13-20x throughput over us at 1KB-10KB Person.\n2. Single-pass with backpatched length varints: write tag + 1-byte placeholder for the length, recurse, fill in (memmove if final length \u003e= 128). Skips the size pass (~3 us at 1KB).\n\nImplementation can live in codegen (mode=full) — emit a _encode_to_buf per message that takes (data, ibuf, offset) and returns new_offset, then a public Person_encode that wraps it with the buffer allocation. Constraint: per-field closure count must stay \u003c= current pb.encode writers count, else the per-field dispatch cost re-emerges (drm: two prior ibuf prototypes both regressed by ~2x because of dispatch).\n\nRelates to h8v (codegen FFI direct writes) which is the per-field building block this work composes.","notes":"drm notes have the deeper analysis including why M6 ibuf path naive byte-write fails (20x interpreter dispatch overhead). The codegen approach sidesteps that by inlining all writes at codegen time so there's no runtime closure dispatch.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-18T16:49:16Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T16:49:16Z","dependencies":[{"issue_id":"tarantool-protobuf-lkz","depends_on_id":"tarantool-protobuf-h8v","type":"blocks","created_at":"2026-05-18T19:49:21Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}


@@ 41,7 41,7 @@
{"_type":"issue","id":"tarantool-protobuf-7lf","title":"CI: wire up conformance suite with cached Docker image","description":"The Docker image build (docker/conformance.Dockerfile) is the long pole at ~10-15 min on a clean cache. A registry push from a scheduled job would let CI runs reuse a warm image. Today the conformance suite runs locally via 'just conformance' but isn't gated on pushes. Goal: every push to master runs the binary+JSON and text-format suites; PRs run the same. Pre-requisite for M8 sourcecraft setup (this defines what the CI pipeline runs).","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:15Z","created_by":"Eugene Blikh","updated_at":"2026-05-17T15:47:15Z","labels":["ci","conformance"],"dependency_count":0,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-0an","title":"Profile the 1 KB decode cliff in hello.Person","description":"bench/bench.lua shows hello.Person decode dropping 260 MB/s @ 100 B to 128 MB/s @ 1 KB — per-MB throughput halves at the payload-shape transition (scalars-only -\u003e emails[] + nested address + packed lucky_numbers). starwing's C decoder scales smoothly across the same transition. Smoking gun: a trace abort or side-trace stitching failure at the array/nested branch. Capture jit.dump=tbim over the transition, identify the abort, fix it. Likely small change once located. Prereq for several decoder fixes — pin down what we're actually hitting first.","notes":"2026-05-17 inspection vs starwing/lua-protobuf 0.5.3 on Tarantool 3.8.0: reproduced starwing hello.Person encode at 75/688/888/1391/1431 MB/s and decode at 11.5/101.6/378/698.7/702.8 MB/s for 10B/100B/1KB/10KB/100KB. Current full mode is encode 30/257/287/541/643 MB/s and decode 30.7/247.5/128.8/186.4/184.3 MB/s. So we beat starwing on tiny decode, but lose 2.9-3.8x once Person switches to repeated strings + nested Address + packed ints. jit_trace passes including multi-byte varint paths; no fatal abort or bridge smoking gun. Focused probes on 930B Person: generated full decode ~6.9-7.5 us/op; scan tags+skip_field ~3.6 us/op; scan tags/lengths only ~2.1 us/op; hand order-specialized decoder with same string extraction/Address decode/lucky parse ~4.3 us/op. Patching decode_string to skip UTF-8 improves only ~16% (7.1 -\u003e 6.1 us/op), so UTF-8 is material but not the main cliff. Likely cause: per-field Lua dispatch/function-call/table growth overhead repeated across ~30 LEN/VARINT fields; C starwing parses from pointer/buffer and lands near our tag-only scan cost. Existing follow-ups bgu (ffi.string string decode), gcy (inline nested decode), and 0u1 (typed decoder byte fast paths) are relevant, but the largest remaining gap likely needs generated ordered/specialized decode loops or ptr+offset readers that avoid decode_tag/decode_len helper dispatch and substring slicing.","status":"closed","priority":2,"issue_type":"bug","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:12Z","created_by":"Eugene Blikh","updated_at":"2026-05-17T16:02:54Z","started_at":"2026-05-17T15:56:33Z","closed_at":"2026-05-17T16:02:54Z","close_reason":"Profiled against starwing and local focused probes. No fatal JIT abort found; cliff attributed mainly to repeated Lua tag/field dispatch plus LEN/string slicing/validation. Follow-up fast-path work captured in tarantool-protobuf-4kj and existing decoder tasks.","labels":["decoder","investigation","perf"],"dependency_count":0,"dependent_count":3,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-b12","title":"Encoder: parallel-array iteration for proto2 extensions","description":"Mirror the desc.oneofs_list finalize-time pattern: build desc.extensions_list once in pb.finalize_message, iterate with 'for i=1,#' in encode instead of walking the descriptor's hash table on every call. The 4-7x proto2 gap (vs 2-4x proto3 in bench/starwing_bench.lua) is partly explained by hash-walked extensions on the hot path. Expected: brings proto2 BenchPayload encode closer to proto3 Person encode at the same shape.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:12Z","created_by":"Eugene Blikh","updated_at":"2026-05-17T15:47:12Z","labels":["encoder","perf","proto2"],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-bgu","title":"Decoder: ffi.string(ptr+off, len) for string/bytes decode","description":"Replace buf:sub(npos, npos+len-1) in decode_string/decode_bytes with ffi.string against a cached ffi.cast('const uint8_t*', buf). Removes method-call dispatch and lets the JIT fuse the read with surrounding code. Hits hardest on repeated-string fields (26 emails strings in 1KB Person -\u003e 26 sub calls today). Watch for LuaJIT string-intern collision behavior to stay identical to sub().","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:10Z","created_by":"Eugene Blikh","updated_at":"2026-05-17T15:47:10Z","labels":["decoder","perf","wire"],"dependencies":[{"issue_id":"tarantool-protobuf-bgu","depends_on_id":"tarantool-protobuf-0an","type":"blocks","created_at":"2026-05-17T18:47:21Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-bgu","title":"Decoder: ffi.string(ptr+off, len) for string/bytes decode","description":"Replace buf:sub(npos, npos+len-1) in decode_string/decode_bytes with ffi.string against a cached ffi.cast('const uint8_t*', buf). Removes method-call dispatch and lets the JIT fuse the read with surrounding code. Hits hardest on repeated-string fields (26 emails strings in 1KB Person -\u003e 26 sub calls today). Watch for LuaJIT string-intern collision behavior to stay identical to sub().","status":"closed","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:10Z","created_by":"Eugene Blikh","updated_at":"2026-05-24T04:20:11Z","closed_at":"2026-05-24T04:20:11Z","close_reason":"Spike (bench/c_accel/bgu_probe.lua, removed) measured ffi.string vs buf:sub for the 1-byte LEN string fast path on Tarantool 3.8.0/LuaJIT 2.1.0-beta3 / Apple M-series. Results (ns/call):\n\n  size  buf:sub  per-call-cast  amortized/26  amortized/4\n  10B   89.7     280.5 (3.1x)   224.9 (2.5x)  246.2 (2.7x)\n  32B   107.6    299.4 (2.8x)   243.8 (2.3x)  260.9 (2.4x)\n  80B   154.3    362.4 (2.3x)   300.1 (1.9x)  314.9 (2.0x)\n\nV1 (per-call ffi.cast(U8CP, buf) inside decode_string): 2.3-3.1x slower.\nV2 (cast hoisted to caller, threaded as ptr arg, amortized over 26 emails — the Person 1KB peak shape): still 1.9-2.5x slower. Cast-once-amortize-many fails because 'ptr + np - 1' mints a fresh cdata wrapper per call that the JIT cannot sink across the decode_string frame — same root cause as a6n's failed top-of-_decode cast. The amortization helps a little (V2 \u003c V1) but does not close the gap.\n\nConclusion: bgu's premise (ffi.string lets the JIT fuse the string read with surrounding code) does not hold on the current LuaJIT fork — the cdata allocation cost dominates any reduction in dispatch. Closing as superseded by a6n-decode-inline-len-fast-path-2026-05-23. Revival criteria: a measurable cdata-sink win on a future LuaJIT (likely needs upstream allocation-sink improvements for pointer-arith cdata), OR an API redesign where the decoder works on a cdata buffer end-to-end (not a Lua string).","labels":["decoder","perf","wire"],"dependencies":[{"issue_id":"tarantool-protobuf-bgu","depends_on_id":"tarantool-protobuf-0an","type":"blocks","created_at":"2026-05-17T18:47:21Z","created_by":"Eugene Blikh","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-u39","title":"Decoder: emit table.new(0, N) for result tables","description":"Codegen knows the field count of every message at emission time. Emit table.new(0, N) instead of {} so the result table is sized correctly from the start. Cheap, broad win — especially closes the 1KB cliff where hello.Person rehashes as nested address + repeated fields populate. LuaJIT-only (table.new is in require('table.new')).","status":"closed","priority":2,"issue_type":"task","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:08Z","created_by":"Eugene Blikh","updated_at":"2026-05-18T19:06:00Z","started_at":"2026-05-18T19:01:45Z","closed_at":"2026-05-18T19:06:00Z","close_reason":"Attempted and reverted. table_new(0, N) for decode result tables passed tests + JIT but regressed small-payload decode (10B -15.4%, 100B -17.7%) because the call cost exceeds rehash savings at small scale, and the small case doesn't reach the first rehash. Large-payload decode flat. Confirms drm bug: alloc is not the throughput bottleneck. See bench/PERF_LOG.md.","labels":["codegen","decoder","perf"],"dependency_count":0,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-aah","title":"Encoder: codegen-emitted packed-scalar tight loops","description":"Replace per-element wire.encode_int32 calls with one inlined varint-emitting loop per packed field. Applies to packed int32, int64, sint32, sint64, bool, enum. The 1KB Person has 5 packed lucky_numbers + 26 emails — current per-element function-call boundary costs add up. Expected: 30%+ on packed-heavy payloads.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:07Z","created_by":"Eugene Blikh","updated_at":"2026-05-17T15:47:07Z","labels":["codegen","encoder","perf"],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"tarantool-protobuf-4ql","title":"Encoder: optional caller-owned ffi.cdata ibuf API","description":"Separate API surface, not the default. ibuf-based encoder that writes into a caller-owned ffi.cdata buffer instead of returning a fresh Lua string. Targets hot RPC paths where the caller already owns a reusable buffer (e.g. net.box send path). Independent of the codegen rewrite — different API contract. Earlier attempt parked in stash@{0}; revisit after the codegen-time emission lands so we can compare apples-to-apples.","status":"open","priority":2,"issue_type":"feature","owner":"bigbes@gmail.com","created_at":"2026-05-17T15:47:06Z","created_by":"Eugene Blikh","updated_at":"2026-05-17T15:47:06Z","labels":["api","encoder","perf"],"dependency_count":0,"dependent_count":0,"comment_count":0}