~bigbes/tarantool-protobuf: ac9b9c13

codegen+wire: split length-prefix emission + 2-byte varint fast path

Two complementary encode optimizations: codegen + runtime writers no
longer pay a per-field string concat for length-prefixed fields, and
encode_varint_slow now handles 2/3/4-byte values without dropping into
the uint64 cdata path.

Composite bench/bench.lua, full mode:
  1KB encode:    229 K -> 332 K msgs/s  (1.45x)
  10KB encode:   38 K  -> 56.5 K        (1.49x)
  100KB encode:  4.1 K -> 6.7 K         (1.64x)

Runtime mode now matches full mode at 10KB+:
  1KB encode:    203 K -> 276 K  (1.36x)
  10KB encode:   35 K  -> 55 K   (1.56x)

bench/shapes_bench.lua biggest swings (both modes):
  packed-int32x1000 enc:  1.7 K  -> 13 K  (7.0-7.6x)
  packed-int32x100  enc:  21 K   -> 113 K (5.3x)
  scalar-heavy      enc:  582 K  -> 1.0 M (1.7x)

Per-helper, bench/wire_bench.lua:
  encode_varint(150)  [2-byte]:   529 ns -> 68 ns  (7.8x)
  encode_varint(20K)  [3-byte]:   841 ns -> 88 ns  (9.6x)
  encode_tag(16, LEN) [2-byte]:   531 ns -> 78 ns  (6.8x)
  encode_string(200B) [2-byte L]: 560 ns -> 109 ns (5.1x)
  encode_varint(127)  [1-byte]:    29 ns -> 28 ns  (no regression)

1. Split length-prefix emission. Length-delimited fields (string/bytes
   scalars, nested messages, packed scalars/enums) used to emit
   `tag → encode_len(body)` where `encode_len(body)` returns
   `encode_varint(#body) .. body`. The string concat allocated a copy
   of the body per field. Now the codegen and runtime writers emit
   three separate `out` slots — `tag`, `varint(#body)`, `body` — and
   let `table.concat(out)` join them in one pass at the end of encode.

   Applied to inline.go (full-mode codegen) for: singular and repeated
   nested messages, singular and repeated string/bytes scalars, packed
   scalars, packed enums. Applied to codec.lua build_writer / build_
   repeated_writer for the same set. Maps still go through encode_len
   pending a separate pass.

2. encode_varint_slow Lua-number fast paths. The outer encode_varint
   stays at the tiny `1-byte check + tail call` shape that LuaJIT
   inlines into hot traces. The slow function (not inlined into hot
   traces, so its body size is unconstrained) now handles non-negative
   Lua numbers up to 2^28 directly via bit.rshift / string.char with no
   cdata allocation. Values in [2^28, 2^53) emit one byte and recurse
   on the smaller residue. Only cdata inputs, negative Lua numbers
   (sign-extended to 10-byte varint), and the rare > 2^53 case still
   take the uint64 cdata loop. Net effect: every multi-byte varint
   encode that fits in a Lua number — including the length prefix for
   any string >= 128 bytes, every tag for field IDs >= 16, and every
   negative-zigzag sint — drops from ~500 ns to ~70 ns.

497/497 luatest pass.  23/23 jit-trace gates pass (the two new
multi-byte varint gates added in the bench infra commit confirm
encode_varint_slow JIT-compiles cleanly).  Conformance suite (binary
+ text) shows 1478 expected passes, 0 unexpected failures.

Eugene Blikh <bigbes@gmail.com> — 2026-05-16 14:44:23 UTC

Commit ac9b9c1302c14cce6718a4f6e8d7ce84c5b7f1dfview raw patch

Parent(s): 6d0ec7bf

6 changed file(s)

FileStatus+
cmd/protoc-gen-tarantool/internal/gen/inline.go M +40 -6
examples/expected/full/conformance/conformance_pb.lua M +40 -19
examples/expected/full/hello/hello_pb.lua M +66 -25
examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua M +234 -80
runtime/pb/codec.lua M +44 -7
runtime/pb/wire.lua M +59 -7