~bigbes/tarantool

tarantool-protobuf

ref: 1eb062c1de71ba3ca4d3bc55efdcd1a1c98e063f tarantool-protobuf/bench/c_accel/README.md -rw-r--r-- 7.1 KiB
5c541416 — Eugene Blikh 2 months ago
bench(c_accel): drop defensive lua_type checks; refresh numbers

person_codec.c had a per-element lua_type(L,-1)==LUA_TSTRING check
inside the emails loop, plus field-level lua_type checks that
generic_codec.c skipped. At 100KB that's ~2700 extra C calls per
message in the hot path, which made S4 (hand-written) look slower
than S3 (generic) at 1KB+ and led to a wrong "branch prediction on
divergent paths" hypothesis in the original README.

Replaced the per-element check with no check and the field-level
lua_type checks with lua_isnil to match generic_codec.c's semantics.
S3 and S4 are now within +/-5% at every size, which strengthens the
ra6 architecture call (ship the generic one-call codec; codegen-
emitted per-message C buys nothing).

Also added bench/c_accel/compile_flags.txt so clangd resolves
<module.h> and friends -- mirrors runtime/pb/c/compile_flags.txt.
d4dbd2f3 — Eugene Blikh 3 months ago
bench: C-acceleration spike — measure four Lua↔C boundaries

Adds bench/c_accel/ with hello.Person codecs at four boundary
strategies and a harness comparing them across 10B–100KB:

  S1 pure Lua (full mode) — existing baseline
  S2 prim.c + prim_ffi.lua — per-primitive FFI calls
  S3 generic_codec.c — one C call per message, descriptor-walking
  S4 person_codec.c — hand-written, no dispatch, upper bound

Plus ffi_probe.lua, a microbenchmark decomposing FFI call cost.

Headline numbers (×L = speedup vs pure-Lua full mode):

  ENCODE       S2     S3     S4
  10B        0.99   3.29   3.79
  100B       0.93   3.12   3.46
  1KB        0.82   5.33   4.94
  10KB       0.60   3.53   2.86
  100KB      0.63   3.49   2.85

  DECODE       S2     S3     S4
  10B        0.29   2.59   2.54
  100B       0.33   2.94   2.95
  1KB        0.39   7.22   7.62
  10KB       0.38   9.64  10.23
  100KB      0.37  10.87  11.33

S3 lands within ±15% of S4 at every size and beats it on encode
at 1KB+: the descriptor-walking loop is uniformly branch-predictable;
hand-written has more divergent per-field paths.

S2 loses to pure Lua at every size ≥1KB on encode and at every size
on decode. FFI cost decomposition (ffi_probe.lua):

  bare FFI call into ffi.load lib        33 ns
  pointer-returning FFI                  73 ns
  + v_out[0] read + tonumber            118 ns
  ffi.C.memcmp (for comparison)          60 ns
  ffi.cast(const uint8_t*, str)         156 ns
  ffi.string(p, 32)                      23 ns
  pure-Lua varint decode                 75 ns
  read_varint in tight traced loop       59 ns (floor)

Per-call FFI dispatch into a ffi.load'd lib is ~60–75 ns — the
same order of magnitude as pure-Lua varint decode. The "win" from
going to C only materializes when you cross the boundary ONCE per
message, not per primitive.

C encode plateaus at ~2.5–2.9 GB/s from 1KB upward; bottleneck
moves to Lua table reads and output string allocation. C decode
hits 2.5 GB/s at 100KB, while pure-Lua decode hits a per-byte
cliff (158 k msg/s @ 1KB → 2.3 k @ 100KB).

Architecture implication for pf6: the generic C runtime (ra6) gets
the full perf envelope; codegen-emitted per-message C (c0i) earns
≤15% headroom and goes the wrong way at scale. Per-primitive FFI
is a non-starter.

Required impl pattern in any future ra6: cache per-field stack
indices for repeated/packed arrays for the duration of
decode_message — the naive lazy-getfield variant was 2× slower
than hand-written at 100KB (see bench/c_accel/README.md).

beads-tarantool-protobuf-04c