~bigbes/tarantool

tarantool-protobuf

ref: 55b5386f999fbd6e3abe6d5a0297dd8ac753b988 tarantool-protobuf/examples/expected/runtime/c_repeated d---------
117c6ef9 — Eugene Blikh 2 months ago
codegen: type-elision for sint64/fixed64/sfixed64 encode (a7l)

For mode=full the field type is statically known, so the codegen can
skip wire.to_int64 / wire.to_uint64's runtime type dispatch by pre-
casting at the call site:

  -- before
  out[n] = wire.encode_sint64(v)        -- calls to_int64(v) inside
  out[n] = wire.encode_fixed64(v)       -- calls to_uint64(v) inside

  -- after
  out[n] = wire.encode_sint64_i(INT64(v))   -- direct cdata path
  out[n] = wire.encode_fixed64_u(UINT64(v))
  out[n] = wire.encode_sfixed64_u(UINT64(v))

Wire-level: added encode_sint64_i / encode_fixed64_u / encode_sfixed64_u
alongside the existing encoders; encode_fixed64 is now a thin wrapper
that calls to_uint64 + encode_fixed64_u so the generic API surface stays
intact. INT64/UINT64 added as file-header upvalues in generated modules.

int32/int64/uint32/uint64 unchanged: those alias to encode_varint, which
only dispatches through to_uint64 on the slow-slow cdata path (large
negatives) — not worth the codegen surface.

Applied at every encode emit site that took a scalar: singular,
required, repeated non-packed, extension singular, extension repeated,
packed slow path, proto2 extension repeated. Map-value path also picks
it up via the shared helper.

c_int64.Wide encode (full mode, 4-run median):
  lua-num inputs   2769 -> 2520 ns/op   (+9.9% throughput)
  cdata inputs    12993 -> 12935 ns/op  (unchanged; to_uint64 already
                                         takes the cdata fast path)

Just below the issue's optimistic 10-15% but exactly the case that
matters — Lua-number inputs are the common case for user code on
sint64/fixed64 fields. Suites: test 766/766, test-c 1057/1057.
e3f06741 — Eugene Blikh 2 months ago
runtime: pb.decode_unsafe + M.<Name>_decode_unsafe in runtime mode (58u)

Completes the unsafe-decode story 6bb started in full mode. Runtime mode
now exposes the same API via parallel `f._reader_unsafe` closures
compiled in pb.finalize_message against a swapped scalar table where
`string` maps to the bytes handler (no utf8_len). build_reader /
build_repeated_reader / decode_one are now parameterized on
(scalar_tbl, decode_msg_fn, decode_group_fn) so the same builders emit
both reader shapes. decode_message_unsafe, decode_group_unsafe, and
decode_extension_unsafe are literal clones of their safe twins with
three substitutions (documented in codec.lua): the _reader field, the
scalar table in slow paths, and the sub-message / group / extension
dispatchers. Tests in test/decode_unsafe_test.lua are now parameterized
over both modes (14 cases, including a map<string, int32> invalid-key
case that exercises the decode_one map-fallback path).

Runtime-mode microbench shows ~8% throughput vs validating decode on
the string-heavy 1KB Person; smaller than full mode's ~20% because the
descriptor dispatch + closure indirection swamp utf8_len, but still a
net win and the perf-cost-of-validating story is now consistent across
modes. Conformance 3240/3240 + JIT trace 37/37 still pass.

Closes 58u, also closes b12 (already fixed in 2656c97; never closed).
kyt still tracks unifying _decode_unsafe with C accel.
035b2adb — Eugene Blikh 2 months ago
codegen: CHARS[_len] lookup replaces string.char(_len) at length-prefix sites (2ri)

Profile attributed 39% of Person_encode's trace share (~28% of total) to a
single line emitting `string.char(_len)` at every inlined length-prefix
site. The `_len` argument is rarely a compile-time constant (lengths come
from user data), so the JIT can't fold the C-function call, and the cost
compounds — the 1KB Person fixture fires ~30 length-prefix sites per
encode.

Replace with a 256-entry lookup table `wire.CHARS` (built once at module
load, byte i -> string.char(i)). Codegen header now emits
`local CHARS = wire.CHARS` alongside the other hot-path localizers; the
single emit site in `emitInlineLenPrefix` swaps `string.char(_len)` for
`CHARS[_len]`. Parity is by construction — both return the same interned
1-byte string.

Tests: 752/752 pass. Bench (work.lab.local, median of 3, hello.Person
encode): 10B +4.7%, 1KB +16.8%, 10KB +21.1%, 100KB +34.2%, proto2 mid
+11.1%. Decode unchanged. See bench/PERF_LOG.md 2026-05-24 2ri entry.

Closes lkz and 86g (ffi.new buffer rewrite paths) — separate hand-spike
of that shape regressed 0.31x-0.77x across all sizes; the perceived
buffering inefficiency wasn't there, and 2ri captured the single hottest
line. Remaining encode-perf headroom is c0i (C-runtime backend).
ed383c92 — Eugene Blikh 2 months ago
codegen: inline proto2 extension writers/readers (qwt) + table.new(N,0) for packed lists (2sn)

qwt closes the proto2_basic.BenchPayload `min` bench's worst data point:
encode 1.89M → 3.30M msgs/s (+75%), decode 1.02M → 1.26M msgs/s (+23%).
Mechanism: collectExtsByExtendee groups all `extend Foo { ... }`
declarations by extendee FullName across input files; emitInlineEncode
emits one dedicated writer per extension instead of dispatching through
pb.codec.encode_field, and emitInlineDecode adds an elseif arm per
extension id straight into result._extensions[full_name]. Dynamic
extensions_list walk preserved for forward compat, gated on
#_elist > N so it pays one int compare when no runtime extension was
registered.

2sn pre-sizes packed-scalar repeated lists via table.new(N, 0) where
the count is recoverable from the LEN payload — exact (lim >> 2 or
lim >> 3) for fixed-width, upper bound (lim) for varint-packed. The
allocation is deferred into the `if wt == 2 then` branch so the
per-element fallback and non-packable types keep the bare-`{}`
alloc. Avoids u39's regression mode because the call cost is paid
once per repeated-field-first-occurrence, not per message decode.

Bench summary for hello.Person full mode: encode +3-5% across sizes,
decode +1-3% across sizes. Tests: 752/752 pure-Lua, 1043/1043 with
PB_ENABLE_C=1, JIT 37/37. PERF_LOG entry covers the rationale and
caveats.
b09c7213 — Eugene Blikh 2 months ago
codegen: inline 1-byte LEN fast path for string/bytes decode

Singular and repeated string/bytes fields in generated full-mode _decode
now read the length byte and dispatch in-line instead of calling
wire.decode_string / wire.decode_bytes. For length < 128 (the common
short-string RPC case), the path stays inside the parent JIT trace:
no child-trace stitch, no per-element function frame, utf8_len lookup
hoisted to a generated-file upvalue.

The original a6n design — one ffi.cast(U8CP, buf) at the top of each
_decode — was abandoned: the cdata wrapper is 24 bytes per call and
LuaJIT can't sink the allocation because the pointer local lives
across wire.decode_* call frames. Net regression at small sizes
(+11-16%) overwhelmed the single-byte-read savings.

Bench (hello.Person full-mode decode):

      size    ns/op before   ns/op after   delta
      10B          419            390     -6.9%
      100B         515            478     -7.2%
      1KB         9004           8042    -10.7%
      10KB       64644          56745    -12.2%
      100KB     625638         537750    -14.0%

Zero allocation impact across all sizes. Tests: 1043 pass, 37 JIT
trace gates pass. (a6n)
e875519e — Eugene Blikh 2 months ago
c_runtime: repeated + packed scalar encode/decode (ra6 3e)

Add repeated dispatch to the C-runtime encode/decode loop. Encode
side: encode_repeated_field walks Lua arrays via lua_objlen + per-
index rawgeti, dispatches on element kind. Packed numerics build
their payload in a stack-backed sub-buffer then emit `tag(LEN) +
varint(len) + body`; unpacked emit `tag + value` per element via
encode_one_field with force_emit=1 to bypass zero-suppression;
strings/bytes flow through the same path (never packable); repeated
messages reuse encode_submessage_field per element.

Decode side: per-field stack-slot cache (list_stack_idx[]) tied to
list_count[] avoids the per-element lua_getfield(result, name) round
trip the c-accel spike measured at 2x slower at 100KB. On first hit
for a repeated field we lua_createtable + write result[name] AND dup-
push the list onto the stack; subsequent hits lua_rawseti through the
cached absolute stack index. Lists stay valid across recursive sub-
message decodes because each child decode_body cleans up its own
scratch back to the caller's frame.

Packed/unpacked symmetry on read: a wt==LEN payload for any packable
scalar is decoded as a packed blob regardless of the schema's packed
flag, and a per-element-tagged stream is decoded element-by-element
even on a schema that defaults to packed — per proto3 reader rules.

New test/proto/c_repeated.proto fixture carries packed + explicit-
unpacked + repeated string/bytes + repeated message branches. The
encode and decode tests round-trip at 10/100/1000 elements per
branch. The two existing "skip repeated and map" marker tests
collapse to "skip map" — only map fields remain out of scope for
3e (bd-asz / 3h lands them next). 854 → 896 passing tests.

bd-jc9