Spike work for tarantool-protobuf-04c (benchmark which Lua↔C
boundary wins for protobuf codec work). Not part of the shipping
codec; lives under bench/ because its only purpose is measurement.
The acceleration design question is where the Lua↔C boundary should sit. Four candidate boundaries:
mode=full generated code,
no C involved. Measured via the existing bench/bench.lua.wire.lua's encode_varint,
decode_string, etc. become ffi.C.<fn> calls. The dispatch
loop stays in Lua; only the inner bit-twiddling is in C.All four strategies are wired into spike_bench.lua:
prim.c + prim_ffi.lua — Strategy 2 (FFI primitives)generic_codec.c — Strategy 3 (one generic C call, descriptor-walking)person_codec.c — Strategy 4 (hand-written for hello.Person)Both person_codec.c and generic_codec.c implement only the
Person fields exercised by bench/bench.lua's payload builder:
name, age, emails, address (with street/city/zip),
lucky_numbers. The spike measures perf, not coverage.
generic_codec.c walks a hand-built message_desc_t /
field_desc_t. A real ra6 would build these descriptors from
the Lua descriptor at pb.finalize_message time and pass them
through a registered userdata.
The generic decode caches per-field stack indices for repeated /
packed arrays for the duration of decode_message, so each
lua_setfield of the array root into the result table happens
once, not per element. That matches person_codec.c's pattern;
the naive "lazy lookup per element" version (initial commit) was
~2× slower than hand-written at 100 KB.
make -C bench/c_accel # builds pb_c_person.dylib
tarantool bench/c_accel/spike_bench.lua
Override the Tarantool include dir if auto-detection fails:
make -C bench/c_accel TT_INC=/path/to/include/tarantool
Apple M-series, Tarantool 3.8.0-entrypoint-49 / LuaJIT 2.1.0-beta3. Throughput msg/s; bandwidth MB/s. ×L columns are speedup vs the pure-Lua baseline.
| size | bytes | pure-Lua msg/s (MB/s) | S2 FFI msg/s (MB/s) | ×L | S3 gen msg/s (MB/s) | ×L | S4 hand msg/s (MB/s) | ×L |
|---|---|---|---|---|---|---|---|---|
| 10B | 10 | 3,159,308 (31.6) | 3,117,936 (31.2) | 0.99 | 10,382,060 (103.8) | 3.29 | 11,981,070 (119.8) | 3.79 |
| 100B | 94 | 3,210,840 (301.8) | 2,984,273 (280.5) | 0.93 | 10,017,531 (941.6) | 3.12 | 11,095,085 (1042.9) | 3.46 |
| 1KB | 930 | 369,992 (344.1) | 303,955 (282.7) | 0.82 | 1,970,288 (1832.4) | 5.33 | 1,826,351 (1698.5) | 4.94 |
| 10KB | 9,634 | 85,025 (819.1) | 50,697 (488.4) | 0.60 | 300,336 (2893.4) | 3.53 | 243,132 (2342.3) | 2.86 |
| 100KB | 96,674 | 8,726 (843.6) | 5,458 (527.7) | 0.63 | 30,428 (2941.6) | 3.49 | 24,826 (2400.0) | 2.85 |
| size | bytes | pure-Lua msg/s (MB/s) | S2 FFI msg/s (MB/s) | ×L | S3 gen msg/s (MB/s) | ×L | S4 hand msg/s (MB/s) | ×L |
|---|---|---|---|---|---|---|---|---|
| 10B | 10 | 3,499,685 (35.0) | 1,003,014 (10.0) | 0.29 | 9,078,941 (90.8) | 2.59 | 8,896,006 (89.0) | 2.54 |
| 100B | 94 | 2,964,500 (278.7) | 981,865 (92.3) | 0.33 | 8,728,669 (820.5) | 2.94 | 8,734,387 (821.0) | 2.95 |
| 1KB | 930 | 165,113 (153.6) | 64,890 (60.3) | 0.39 | 1,191,611 (1108.2) | 7.22 | 1,258,812 (1170.7) | 7.62 |
| 10KB | 9,634 | 22,807 (219.7) | 8,683 (83.7) | 0.38 | 219,809 (2117.6) | 9.64 | 233,209 (2246.7) | 10.23 |
| 100KB | 96,674 | 2,344 (226.6) | 870 (84.1) | 0.37 | 25,487 (2463.9) | 10.87 | 26,562 (2567.8) | 11.33 |
ffi.load'd library's per-call dispatch is several
hundred ns.ra6 must encode this.pf6)ra6 (generic C runtime, one C call per message). It's
the message-level boundary and S3 lands within noise of the
hand-written ceiling. 3–11× over pure Lua at every size.c0i (codegen-emitted per-message C). ≤15% headroom
over ra6, going the wrong way at scale. The codegen complexity
isn't justified.ra6 — the floor is the Lua side
of the boundary, not the wire layer.