diff --git a/bench/c_accel/.gitignore b/bench/c_accel/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f9edc52733ac8557f01df83b6dd9f3f16aab3810 --- /dev/null +++ b/bench/c_accel/.gitignore @@ -0,0 +1,3 @@ +*.dylib +*.so +*.o diff --git a/bench/c_accel/Makefile b/bench/c_accel/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..c0df6198526a3923c17a8efab606714419887fe8 --- /dev/null +++ b/bench/c_accel/Makefile @@ -0,0 +1,50 @@ +# Makefile for the C-acceleration spike (tarantool-protobuf-04c). +# +# Builds Lua C modules loadable from the bench harness with no install step +# -- the harness sets package.cpath to point here. + +# Locate . Order: env override, brew prefix, common system dirs. +TT_INC ?= $(shell \ + if [ -n "$$TARANTOOL_INCLUDE" ] && [ -f "$$TARANTOOL_INCLUDE/module.h" ]; then \ + echo "$$TARANTOOL_INCLUDE"; exit 0; \ + fi; \ + for d in \ + $$(brew --prefix tarantool 2>/dev/null)/include/tarantool \ + /opt/homebrew/include/tarantool \ + /usr/local/include/tarantool \ + /usr/include/tarantool; do \ + if [ -f "$$d/module.h" ]; then echo "$$d"; exit 0; fi; \ + done) + +ifeq ($(TT_INC),) +$(error Cannot find ; set TARANTOOL_INCLUDE=/path/to/include/tarantool) +endif + +UNAME_S := $(shell uname -s) + +CFLAGS := -O2 -fPIC -Wall -Wextra -std=c99 -I$(TT_INC) +ifeq ($(UNAME_S),Darwin) + LIB_EXT := dylib + LDFLAGS := -bundle -undefined dynamic_lookup +else + LIB_EXT := so + LDFLAGS := -shared +endif + +MODULES := pb_c_person.$(LIB_EXT) pb_c_generic.$(LIB_EXT) libpb_prim.$(LIB_EXT) + +all: $(MODULES) + +pb_c_person.$(LIB_EXT): person_codec.c + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< + +pb_c_generic.$(LIB_EXT): generic_codec.c + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< + +libpb_prim.$(LIB_EXT): prim.c + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< + +clean: + rm -f *.dylib *.so + +.PHONY: all clean diff --git a/bench/c_accel/README.md b/bench/c_accel/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a8715c20288b4a3bed86756137d1576403d76730 --- /dev/null +++ b/bench/c_accel/README.md @@ -0,0 +1,123 @@ +# bench/c_accel — C acceleration spike + +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. + +## Strategies (per the parent ticket) + +The acceleration design question is *where* the Lua↔C boundary +should sit. Four candidate boundaries: + +1. **Pure Lua** (baseline) — current `mode=full` generated code, + no C involved. Measured via the existing `bench/bench.lua`. +2. **Per-primitive FFI** — `wire.lua`'s `encode_varint`, + `decode_string`, etc. become `ffi.C.` calls. The dispatch + loop stays in Lua; only the inner bit-twiddling is in C. +3. **One generic C call per message** — a C module gets the + descriptor and the input table once and owns the inner loop. +4. **Hand-written C codec for hello.Person** — upper bound. No + dispatch, no descriptor walk. Tells us the ceiling. + +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) + +## Scope + +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. + +## Build and run + +```bash +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: + +```bash +make -C bench/c_accel TT_INC=/path/to/include/tarantool +``` + +## Results — 2026-05-18 + +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. + +### Encode + +| 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 | + +### Decode + +| 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 | + +### What the numbers say + +- **The C boundary is cheap; per-primitive FFI is not.** Crossing + the C boundary *once* per message wins 3–11×. Crossing it tens + of times per message (S2) *loses* — pure-Lua decode is 3× faster + than FFI-primitive decode because LuaJIT inlines its own wire + helpers but a `ffi.load`'d library's per-call dispatch is several + hundred ns. +- **S3 ≈ S4** within ±15% at every size, and S3 *beats* S4 on + encode at 1 KB+ (the descriptor-walk loop is uniformly branch- + predictable; the hand-written codec has more divergent per-field + paths). +- **C encode plateaus at ~2.5–2.9 GB/s** from 1 KB upward. The + bottleneck moves to Lua table reads and output string allocation, + not wire formatting. +- **C decode degrades much more gracefully than Lua decode.** + Pure-Lua decode is per-byte cliff-y (158 k msg/s @ 1KB → + 2.3 k @ 100KB); C decode degrades roughly linearly with size, + hitting 2.5 GB/s at 100KB. +- The cache-the-repeated-array-stack-idx pattern is required: + the naive lazy-getfield version was ~2× slower than hand-written + at 100 KB. `ra6` must encode this. + +### What this means for the architecture (`pf6`) + +- **Ship `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. +- **Drop `c0i` (codegen-emitted per-message C).** ≤15% headroom + over `ra6`, going the wrong way at scale. The codegen complexity + isn't justified. +- **Drop per-primitive FFI as an architecture.** S2 loses to pure + Lua at every size ≥1 KB on encode and at every size on decode. + The boundary is too chatty. +- The result-table allocation in C still goes through the Lua + runtime, so very-small-message C wins are capped (~3× at 10B + encode). Worth knowing for `ra6` — the floor is the Lua side + of the boundary, not the wire layer. diff --git a/bench/c_accel/ffi_probe.lua b/bench/c_accel/ffi_probe.lua new file mode 100644 index 0000000000000000000000000000000000000000..38a1d4a2a931cbeeb5a98660099280bbc3bd4201 --- /dev/null +++ b/bench/c_accel/ffi_probe.lua @@ -0,0 +1,226 @@ +#!/usr/bin/env tarantool +-- ffi_probe.lua -- diagnose why S2 (per-primitive FFI) is slow. +-- +-- Microbenchmarks the individual FFI calls used in prim_ffi.lua to +-- decompose where decode/encode time actually goes. Also dumps JIT +-- traces for the hottest call shapes. + +local SCRIPT_DIR = (debug.getinfo(1, 'S').source:match('@?(.*/)') or './') +package.cpath = SCRIPT_DIR .. '?.dylib;' .. SCRIPT_DIR .. '?.so;' .. package.cpath + +local ffi = require('ffi') +local bit = require('bit') +local clock = require('clock') + +ffi.cdef[[ + typedef struct ibuf_s { + uint8_t *data; + size_t len; + size_t cap; + } ibuf_t; + void pb_ibuf_init(ibuf_t *b); + void pb_ibuf_reset(ibuf_t *b); + void pb_write_varint(ibuf_t *b, uint64_t v); + void pb_write_bytes(ibuf_t *b, const uint8_t *src, size_t n); + void pb_write_string_field(ibuf_t *b, uint32_t tag, + const uint8_t *src, size_t n); + const uint8_t *pb_read_varint(const uint8_t *p, + const uint8_t *end, uint64_t *out); +]] + +local UNAME = io.popen('uname -s'):read('*l') +local ext = (UNAME == 'Darwin') and '.dylib' or '.so' +local C = ffi.load(SCRIPT_DIR .. 'libpb_prim' .. ext) + +local outbuf = ffi.new('ibuf_t') +C.pb_ibuf_init(outbuf) + +local v_out = ffi.new('uint64_t[1]') + +local function time_loop(fn, n) + -- Warm: let the JIT trace + for _ = 1, 5000 do fn() end + collectgarbage('collect') + local t0 = clock.monotonic64() + for _ = 1, n do fn() end + local t1 = clock.monotonic64() + return tonumber(t1 - t0) / n -- ns/op +end + +io.write('FFI primitive probe — Tarantool ', _TARANTOOL, '\n\n') + +-- ---------------------------------------------------------------- +-- Bench 1: bare FFI call into ffi.load'd lib, simplest signature. +-- pb_ibuf_reset is void(ibuf_t*). No marshalling beyond pointer pass. +-- ---------------------------------------------------------------- +local N = 2000000 + +io.write('=== Bare FFI calls (no return marshalling) ===\n') +local t = time_loop(function() C.pb_ibuf_reset(outbuf) end, N) +io.write(string.format(' pb_ibuf_reset(buf): %6.1f ns/call\n', t)) + +-- One write_varint with constant value (simplest). +local t = time_loop(function() + C.pb_ibuf_reset(outbuf) + C.pb_write_varint(outbuf, 42) +end, N) +io.write(string.format(' reset + write_varint(42): %6.1f ns/call\n', t)) + +-- Just write_varint, no reset. +local t = time_loop(function() C.pb_write_varint(outbuf, 42) end, N) +io.write(string.format(' write_varint(42) only: %6.1f ns/call (note: drifts buf)\n', t)) + +-- ---------------------------------------------------------------- +-- Bench 2: FFI call with pointer return + output parameter. +-- This is the suspected smoking gun. pb_read_varint returns +-- const uint8_t*. Trace formation around return-by-pointer is the +-- common LuaJIT FFI pitfall. +-- ---------------------------------------------------------------- +io.write('\n=== Pointer-return FFI (suspect: pb_read_varint) ===\n') + +local buf_str = string.char(0x96, 0x01) -- varint encoding of 150 +local p0 = ffi.cast('const uint8_t*', buf_str) +local end0 = p0 + #buf_str + +local t = time_loop(function() + local p = C.pb_read_varint(p0, end0, v_out) + -- discard p +end, N) +io.write(string.format(' pb_read_varint(p, end, out) [1B]: %6.1f ns/call\n', t)) + +-- Variant: assign return + read v_out[0] (the realistic decode pattern). +local t = time_loop(function() + local p = C.pb_read_varint(p0, end0, v_out) + local v = v_out[0] +end, N) +io.write(string.format(' read_varint + v_out[0] read: %6.1f ns/call\n', t)) + +-- Variant: ditto + tonumber. +local t = time_loop(function() + local p = C.pb_read_varint(p0, end0, v_out) + local v = tonumber(v_out[0]) +end, N) +io.write(string.format(' read_varint + v_out[0] + tonumber: %6.1f ns/call\n', t)) + +-- ---------------------------------------------------------------- +-- Bench 3: per-call overhead components. +-- ffi.cast / ffi.string allocate. Quantify. +-- ---------------------------------------------------------------- +io.write('\n=== Allocation-heavy FFI ops ===\n') + +local s32 = string.rep('a', 32) + +local t = time_loop(function() + local p = ffi.cast('const uint8_t*', s32) +end, N) +io.write(string.format(' ffi.cast(const uint8_t*, str): %6.1f ns/call\n', t)) + +local p_cdata = ffi.cast('const uint8_t*', s32) +local t = time_loop(function() + local s = ffi.string(p_cdata, 32) +end, N) +io.write(string.format(' ffi.string(ptr, 32): %6.1f ns/call\n', t)) + +-- ---------------------------------------------------------------- +-- Bench 4: same op, but call ffi.C symbol vs ffi.load'd lib. +-- LuaJIT can sometimes inline ffi.C calls (libc symbols loaded via +-- the process namespace) more aggressively than ffi.load'd libs. +-- ---------------------------------------------------------------- +io.write('\n=== ffi.C (libc) vs ffi.load (libpb_prim) calls ===\n') + +ffi.cdef[[ + int memcmp(const void *s1, const void *s2, size_t n); +]] + +local b1 = ffi.cast('const uint8_t*', 'aaaaaaaa') +local b2 = ffi.cast('const uint8_t*', 'aaaaaaaa') + +local t = time_loop(function() ffi.C.memcmp(b1, b2, 8) end, N) +io.write(string.format(' ffi.C.memcmp(p, p, 8): %6.1f ns/call\n', t)) + +-- ---------------------------------------------------------------- +-- Bench 5: pure-Lua equivalent — a Lua varint decoder. +-- The baseline that beats us. If pure-Lua varint is ~10 ns, +-- FFI is paying a real boundary cost per call. +-- ---------------------------------------------------------------- +io.write('\n=== Pure Lua varint decode (the thing we are losing to) ===\n') + +local function lua_decode_varint(s, pos) + local v = 0 + local shift = 0 + while true do + local b = s:byte(pos) + pos = pos + 1 + v = v + (b - (b >= 128 and 128 or 0)) * (2 ^ shift) + if b < 128 then break end + shift = shift + 7 + end + return v, pos +end + +local s150 = string.char(0x96, 0x01) +local t = time_loop(function() + local v, p = lua_decode_varint(s150, 1) +end, N) +io.write(string.format(' lua_decode_varint(s150) — Lua only: %6.1f ns/call\n', t)) + +-- ---------------------------------------------------------------- +-- Bench 6: jit.dump on the suspect path to see whether a trace +-- actually compiles for the FFI read_varint loop. +-- ---------------------------------------------------------------- +io.write('\n=== JIT trace status (read_varint loop) ===\n') +local jutil = require('jit.util') +local jv = require('jit.v') + +io.write(' Run with TARANTOOL_VERBOSE_JIT=1 to dump traces; below is a 100k-iter loop:\n') + +-- Compile-trigger loop +local function read_varint_loop() + for _ = 1, 100 do + local p = C.pb_read_varint(p0, end0, v_out) + local v = tonumber(v_out[0]) + end +end + +-- Try to expose trace formation. +local jv_enabled = false +local ok = pcall(function() + jv.start('-') + jv_enabled = true +end) +read_varint_loop() +read_varint_loop() +if jv_enabled then + pcall(function() jv.stop() end) +end + +-- ---------------------------------------------------------------- +-- Bench 7: same FFI primitives, but loop the calls so the trace +-- has the loop body to compile. If per-call cost falls dramatically +-- inside a loop, then the issue is trace formation per inner-most +-- call. If it stays the same, the FFI call itself is slow. +-- ---------------------------------------------------------------- +io.write('\n=== Tight loop of read_varint (amortize boundary, look for trace) ===\n') + +local function run_loop_n(reps) + -- pre-warm + for _ = 1, 100 do + for _ = 1, reps do + local p = C.pb_read_varint(p0, end0, v_out) + end + end + collectgarbage('collect') + local t0 = clock.monotonic64() + for _ = 1, 1000 do + for _ = 1, reps do + local p = C.pb_read_varint(p0, end0, v_out) + end + end + local t1 = clock.monotonic64() + return tonumber(t1 - t0) / (1000 * reps) +end + +for _, reps in ipairs({1, 4, 16, 64, 256}) do + io.write(string.format(' read_varint x %3d in loop: %6.1f ns/call\n', + reps, run_loop_n(reps))) +end diff --git a/bench/c_accel/generic_codec.c b/bench/c_accel/generic_codec.c new file mode 100644 index 0000000000000000000000000000000000000000..4b1a94c647c5e79011613aeb473205915bb7d683 --- /dev/null +++ b/bench/c_accel/generic_codec.c @@ -0,0 +1,424 @@ +/* + * generic_codec.c -- one-C-call generic codec, descriptor-walking. + * + * Strategy 3 of tarantool-protobuf-04c. + * + * Same boundary as person_codec.c (one C call per top-level + * encode/decode), but the inner loop walks a `message_desc_t` and + * dispatches per-field on `kind_t`. This is what `ra6` would ship + * in production. The gap to person_codec.c (Strategy 4) is the + * dispatch overhead of being generic. + * + * Scope mirrors person_codec.c: only Person fields exercised by + * bench/bench.lua. The descriptor tables for Person and Address + * are hand-built; a real implementation would build them at + * `pb.finalize_message` time from the Lua descriptor. + */ + +#include +#include + +#include +#include +#include + +/* ---------------------------------------------------------------- * + * buf_t (identical to person_codec.c -- duplicated to keep the * + * spike modules independent). * + * ---------------------------------------------------------------- */ + +typedef struct { + uint8_t *data; + size_t len; + size_t cap; + uint8_t stack[4096]; +} buf_t; + +static inline void +buf_init(buf_t *b) +{ + b->data = b->stack; + b->len = 0; + b->cap = sizeof(b->stack); +} + +static inline void +buf_free(buf_t *b) +{ + if (b->data != b->stack) + free(b->data); +} + +static void +buf_grow(buf_t *b, size_t need) +{ + size_t nc = b->cap ? b->cap * 2 : 64; + while (nc < b->len + need) + nc *= 2; + uint8_t *nd = (uint8_t *)malloc(nc); + memcpy(nd, b->data, b->len); + if (b->data != b->stack) + free(b->data); + b->data = nd; + b->cap = nc; +} + +static inline void +buf_reserve(buf_t *b, size_t need) +{ + if (b->len + need > b->cap) + buf_grow(b, need); +} + +static inline void +write_varint(buf_t *b, uint64_t v) +{ + buf_reserve(b, 10); + while (v >= 0x80) { + b->data[b->len++] = (uint8_t)(v | 0x80); + v >>= 7; + } + b->data[b->len++] = (uint8_t)v; +} + +static inline void +write_bytes(buf_t *b, const void *src, size_t n) +{ + buf_reserve(b, n); + memcpy(b->data + b->len, src, n); + b->len += n; +} + +static const uint8_t * +read_varint(const uint8_t *p, const uint8_t *end, uint64_t *out) +{ + uint64_t v = 0; + int shift = 0; + while (p < end) { + uint8_t c = *p++; + v |= (uint64_t)(c & 0x7f) << shift; + if (!(c & 0x80)) { + *out = v; + return p; + } + shift += 7; + if (shift >= 64) + return NULL; + } + return NULL; +} + +/* ---------------------------------------------------------------- * + * Descriptor model. * + * ---------------------------------------------------------------- */ + +typedef enum { + K_INT32 = 0, + K_STRING, + K_MESSAGE, + K_REPEATED_STRING, + K_PACKED_INT32, +} kind_t; + +struct message_desc; + +typedef struct field_desc { + uint32_t tag; /* (field_num << 3) | wire_type */ + int field_num; + const char *name; + kind_t kind; + const struct message_desc *submsg; +} field_desc_t; + +typedef struct message_desc { + const char *name; + int n_fields; + const field_desc_t *fields; +} message_desc_t; + +/* Address (sub-message used by Person.address) */ +static const field_desc_t address_fields[] = { + {(1 << 3) | 2, 1, "street", K_STRING, NULL}, + {(2 << 3) | 2, 2, "city", K_STRING, NULL}, + {(3 << 3) | 0, 3, "zip", K_INT32, NULL}, +}; +static const message_desc_t Address_desc = { + "Address", 3, address_fields, +}; + +/* Person (subset exercised by bench payloads) */ +static const field_desc_t person_fields[] = { + {(1 << 3) | 2, 1, "name", K_STRING, NULL}, + {(2 << 3) | 0, 2, "age", K_INT32, NULL}, + {(3 << 3) | 2, 3, "emails", K_REPEATED_STRING, NULL}, + {(5 << 3) | 2, 5, "address", K_MESSAGE, &Address_desc}, + {(7 << 3) | 2, 7, "lucky_numbers", K_PACKED_INT32, NULL}, +}; +static const message_desc_t Person_desc = { + "Person", 5, person_fields, +}; + +static int +find_field_idx(const message_desc_t *md, int field_num) +{ + for (int i = 0; i < md->n_fields; i++) + if (md->fields[i].field_num == field_num) + return i; + return -1; +} + +/* Max descriptor fields per message in the spike. Bumped above the + * exercise to keep the stack-allocated index arrays in decode_message + * safe; a real runtime would size dynamically. */ +#define MAX_FIELDS_PER_MSG 16 + +/* ---------------------------------------------------------------- * + * Generic encode. * + * ---------------------------------------------------------------- */ + +static void +encode_message(buf_t *b, lua_State *L, int t, + const message_desc_t *md) +{ + for (int i = 0; i < md->n_fields; i++) { + const field_desc_t *fd = &md->fields[i]; + lua_getfield(L, t, fd->name); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + continue; + } + switch (fd->kind) { + case K_STRING: { + size_t n; + const char *s = lua_tolstring(L, -1, &n); + write_varint(b, fd->tag); + write_varint(b, (uint64_t)n); + write_bytes(b, s, n); + break; + } + case K_INT32: { + write_varint(b, fd->tag); + write_varint(b, + (uint64_t)(int64_t)lua_tointeger(L, -1)); + break; + } + case K_REPEATED_STRING: { + int idx = lua_gettop(L); + int n = (int)lua_objlen(L, idx); + for (int j = 1; j <= n; j++) { + lua_rawgeti(L, idx, j); + size_t slen; + const char *s = + lua_tolstring(L, -1, &slen); + write_varint(b, fd->tag); + write_varint(b, (uint64_t)slen); + write_bytes(b, s, slen); + lua_pop(L, 1); + } + break; + } + case K_MESSAGE: { + int idx = lua_gettop(L); + buf_t sub; + buf_init(&sub); + encode_message(&sub, L, idx, fd->submsg); + write_varint(b, fd->tag); + write_varint(b, (uint64_t)sub.len); + write_bytes(b, sub.data, sub.len); + buf_free(&sub); + break; + } + case K_PACKED_INT32: { + int idx = lua_gettop(L); + int n = (int)lua_objlen(L, idx); + buf_t sub; + buf_init(&sub); + for (int j = 1; j <= n; j++) { + lua_rawgeti(L, idx, j); + write_varint(&sub, + (uint64_t)(int64_t) + lua_tointeger(L, -1)); + lua_pop(L, 1); + } + write_varint(b, fd->tag); + write_varint(b, (uint64_t)sub.len); + write_bytes(b, sub.data, sub.len); + buf_free(&sub); + break; + } + } + lua_pop(L, 1); + } +} + +/* ---------------------------------------------------------------- * + * Generic decode. * + * ---------------------------------------------------------------- */ + +static const uint8_t * +decode_message(lua_State *L, const uint8_t *p, const uint8_t *end, + const message_desc_t *md); + +/* Skip an unknown field given its wire type. Returns new p or NULL. */ +static const uint8_t * +skip_field(const uint8_t *p, const uint8_t *end, int wt) +{ + uint64_t v; + switch (wt) { + case 0: /* varint */ + return read_varint(p, end, &v); + case 1: /* 64-bit */ + if (end - p < 8) return NULL; + return p + 8; + case 2: { /* LEN */ + p = read_varint(p, end, &v); + if (!p || (uint64_t)(end - p) < v) return NULL; + return p + v; + } + case 5: /* 32-bit */ + if (end - p < 4) return NULL; + return p + 4; + default: + return NULL; + } +} + +static const uint8_t * +decode_message(lua_State *L, const uint8_t *p, const uint8_t *end, + const message_desc_t *md) +{ + lua_createtable(L, 0, md->n_fields); + const int result_idx = lua_gettop(L); + + /* Per-field caches for repeated-array fields. Indexed by field + * position in md->fields. arr_stk[i] == 0 means "not yet + * created". Setting to result table happens once at the end so + * we only pay lua_setfield once per repeated field, not per + * element -- matches the hand-written codec's pattern. */ + int arr_stk[MAX_FIELDS_PER_MSG] = {0}; + int arr_n[MAX_FIELDS_PER_MSG] = {0}; + + while (p < end) { + uint64_t tag; + p = read_varint(p, end, &tag); + if (!p) break; + int field_num = (int)(tag >> 3); + int wt = (int)(tag & 7); + int fi = find_field_idx(md, field_num); + if (fi < 0) { + p = skip_field(p, end, wt); + if (!p) break; + continue; + } + const field_desc_t *fd = &md->fields[fi]; + switch (fd->kind) { + case K_STRING: { + uint64_t slen; + p = read_varint(p, end, &slen); + if (!p || (uint64_t)(end - p) < slen) goto done; + lua_pushlstring(L, (const char *)p, (size_t)slen); + lua_setfield(L, result_idx, fd->name); + p += slen; + break; + } + case K_INT32: { + uint64_t v; + p = read_varint(p, end, &v); + if (!p) goto done; + lua_pushinteger(L, (lua_Integer)(int32_t)v); + lua_setfield(L, result_idx, fd->name); + break; + } + case K_REPEATED_STRING: { + uint64_t slen; + p = read_varint(p, end, &slen); + if (!p || (uint64_t)(end - p) < slen) goto done; + if (arr_stk[fi] == 0) { + lua_createtable(L, 4, 0); + arr_stk[fi] = lua_gettop(L); + } + lua_pushlstring(L, (const char *)p, (size_t)slen); + lua_rawseti(L, arr_stk[fi], ++arr_n[fi]); + p += slen; + break; + } + case K_MESSAGE: { + uint64_t slen; + p = read_varint(p, end, &slen); + if (!p || (uint64_t)(end - p) < slen) goto done; + decode_message(L, p, p + slen, fd->submsg); + lua_setfield(L, result_idx, fd->name); + p += slen; + break; + } + case K_PACKED_INT32: { + uint64_t slen; + p = read_varint(p, end, &slen); + if (!p || (uint64_t)(end - p) < slen) goto done; + const uint8_t *fend = p + slen; + if (arr_stk[fi] == 0) { + lua_createtable(L, 8, 0); + arr_stk[fi] = lua_gettop(L); + } + while (p < fend) { + uint64_t v; + p = read_varint(p, fend, &v); + if (!p) break; + lua_pushinteger(L, + (lua_Integer)(int32_t)v); + lua_rawseti(L, arr_stk[fi], ++arr_n[fi]); + } + break; + } + } + } +done: + /* Attach any deferred repeated arrays to the result table. */ + for (int i = 0; i < md->n_fields; i++) { + if (arr_stk[i]) { + lua_pushvalue(L, arr_stk[i]); + lua_setfield(L, result_idx, md->fields[i].name); + } + } + lua_settop(L, result_idx); + return p; +} + +/* ---------------------------------------------------------------- * + * Lua entry points. * + * ---------------------------------------------------------------- */ + +static int +Person_encode(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + buf_t b; + buf_init(&b); + encode_message(&b, L, 1, &Person_desc); + lua_pushlstring(L, (const char *)b.data, b.len); + buf_free(&b); + return 1; +} + +static int +Person_decode(lua_State *L) +{ + size_t len; + const char *buf = luaL_checklstring(L, 1, &len); + decode_message(L, (const uint8_t *)buf, + (const uint8_t *)buf + len, &Person_desc); + return 1; +} + +static const struct luaL_Reg lib[] = { + {"Person_encode", Person_encode}, + {"Person_decode", Person_decode}, + {NULL, NULL}, +}; + +LUA_API int +luaopen_pb_c_generic(lua_State *L) +{ + luaL_register(L, "pb_c_generic", lib); + return 1; +} diff --git a/bench/c_accel/person_codec.c b/bench/c_accel/person_codec.c new file mode 100644 index 0000000000000000000000000000000000000000..6a1725915f6c04670ff9943e53d68212d5041fe6 --- /dev/null +++ b/bench/c_accel/person_codec.c @@ -0,0 +1,397 @@ +/* + * person_codec.c -- hand-written C codec for hello.Person. + * + * Strategy 4 of tarantool-protobuf-04c: upper-bound measurement. + * + * Scope: ONLY the fields exercised by bench/bench.lua's Person payloads: + * name (string, 1), age (int32, 2), emails (repeated string, 3), + * address (Address message, 5), lucky_numbers (packed int32, 7). + * + * Other Person fields (status, friends, avatar, user_id, balance, + * weight_kg, maps) are intentionally absent. The spike measures the + * upper bound of C boundary perf for the bench shapes, not full + * codec coverage. + */ + +#include +#include + +#include +#include +#include + +/* ---------------------------------------------------------------- * + * Growable byte buffer with a 4 KiB stack-backed initial region. * + * ---------------------------------------------------------------- */ + +typedef struct { + uint8_t *data; + size_t len; + size_t cap; + uint8_t stack[4096]; +} buf_t; + +static inline void +buf_init(buf_t *b) +{ + b->data = b->stack; + b->len = 0; + b->cap = sizeof(b->stack); +} + +static inline void +buf_free(buf_t *b) +{ + if (b->data != b->stack) + free(b->data); +} + +static void +buf_grow(buf_t *b, size_t need) +{ + size_t nc = b->cap ? b->cap * 2 : 64; + while (nc < b->len + need) + nc *= 2; + uint8_t *nd = (uint8_t *)malloc(nc); + memcpy(nd, b->data, b->len); + if (b->data != b->stack) + free(b->data); + b->data = nd; + b->cap = nc; +} + +static inline void +buf_reserve(buf_t *b, size_t need) +{ + if (b->len + need > b->cap) + buf_grow(b, need); +} + +/* ---------------------------------------------------------------- * + * Wire primitives. * + * ---------------------------------------------------------------- */ + +static inline void +write_varint(buf_t *b, uint64_t v) +{ + buf_reserve(b, 10); + while (v >= 0x80) { + b->data[b->len++] = (uint8_t)(v | 0x80); + v >>= 7; + } + b->data[b->len++] = (uint8_t)v; +} + +static inline void +write_bytes(buf_t *b, const void *src, size_t n) +{ + buf_reserve(b, n); + memcpy(b->data + b->len, src, n); + b->len += n; +} + +static inline void +write_string_field(buf_t *b, uint32_t tag, const char *s, size_t n) +{ + write_varint(b, tag); + write_varint(b, (uint64_t)n); + write_bytes(b, s, n); +} + +/* Read a varint. Returns new pointer on success, NULL on truncation. */ +static const uint8_t * +read_varint(const uint8_t *p, const uint8_t *end, uint64_t *out) +{ + uint64_t v = 0; + int shift = 0; + while (p < end) { + uint8_t c = *p++; + v |= (uint64_t)(c & 0x7f) << shift; + if (!(c & 0x80)) { + *out = v; + return p; + } + shift += 7; + if (shift >= 64) + return NULL; + } + return NULL; +} + +/* ---------------------------------------------------------------- * + * Address encode/decode helpers (sub-message, fields used in * + * bench payloads: street/1, city/2, zip/3). * + * ---------------------------------------------------------------- */ + +static void +encode_address_body(buf_t *b, lua_State *L, int t) +{ + lua_getfield(L, t, "street"); + if (lua_type(L, -1) == LUA_TSTRING) { + size_t n; + const char *s = lua_tolstring(L, -1, &n); + write_string_field(b, (1 << 3) | 2, s, n); + } + lua_pop(L, 1); + + lua_getfield(L, t, "city"); + if (lua_type(L, -1) == LUA_TSTRING) { + size_t n; + const char *s = lua_tolstring(L, -1, &n); + write_string_field(b, (2 << 3) | 2, s, n); + } + lua_pop(L, 1); + + lua_getfield(L, t, "zip"); + if (lua_type(L, -1) == LUA_TNUMBER) { + write_varint(b, (3 << 3) | 0); + write_varint(b, (uint64_t)(int64_t)lua_tointeger(L, -1)); + } + lua_pop(L, 1); +} + +static const uint8_t * +decode_address(lua_State *L, const uint8_t *p, const uint8_t *end) +{ + lua_createtable(L, 0, 3); + while (p < end) { + uint64_t tag; + p = read_varint(p, end, &tag); + if (!p) + return NULL; + int field = (int)(tag >> 3); + int wt = (int)(tag & 7); + if (wt == 2) { + uint64_t slen; + p = read_varint(p, end, &slen); + if (!p || (size_t)(end - p) < slen) + return NULL; + if (field == 1) { + lua_pushlstring(L, (const char *)p, (size_t)slen); + lua_setfield(L, -2, "street"); + } else if (field == 2) { + lua_pushlstring(L, (const char *)p, (size_t)slen); + lua_setfield(L, -2, "city"); + } + p += slen; + } else if (wt == 0) { + uint64_t v; + p = read_varint(p, end, &v); + if (!p) + return NULL; + if (field == 3) { + lua_pushinteger(L, (lua_Integer)(int32_t)v); + lua_setfield(L, -2, "zip"); + } + } else { + /* Unknown wire types ignored in spike. */ + return NULL; + } + } + return p; +} + +/* ---------------------------------------------------------------- * + * Person_encode(tbl) -> string * + * ---------------------------------------------------------------- */ + +static int +Person_encode(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + const int t = 1; + buf_t b; + buf_init(&b); + + /* name (1, string) */ + lua_getfield(L, t, "name"); + if (lua_type(L, -1) == LUA_TSTRING) { + size_t n; + const char *s = lua_tolstring(L, -1, &n); + write_string_field(&b, (1 << 3) | 2, s, n); + } + lua_pop(L, 1); + + /* age (2, int32) */ + lua_getfield(L, t, "age"); + if (lua_type(L, -1) == LUA_TNUMBER) { + write_varint(&b, (2 << 3) | 0); + write_varint(&b, (uint64_t)(int64_t)lua_tointeger(L, -1)); + } + lua_pop(L, 1); + + /* emails (3, repeated string) */ + lua_getfield(L, t, "emails"); + if (lua_type(L, -1) == LUA_TTABLE) { + int idx = lua_gettop(L); + int n_emails = (int)lua_objlen(L, idx); + for (int i = 1; i <= n_emails; i++) { + lua_rawgeti(L, idx, i); + if (lua_type(L, -1) == LUA_TSTRING) { + size_t n; + const char *s = lua_tolstring(L, -1, &n); + write_string_field(&b, (3 << 3) | 2, s, n); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + /* address (5, sub-message) */ + lua_getfield(L, t, "address"); + if (lua_type(L, -1) == LUA_TTABLE) { + int addr_idx = lua_gettop(L); + buf_t sub; + buf_init(&sub); + encode_address_body(&sub, L, addr_idx); + write_varint(&b, (5 << 3) | 2); + write_varint(&b, (uint64_t)sub.len); + write_bytes(&b, sub.data, sub.len); + buf_free(&sub); + } + lua_pop(L, 1); + + /* lucky_numbers (7, packed int32) */ + lua_getfield(L, t, "lucky_numbers"); + if (lua_type(L, -1) == LUA_TTABLE) { + int idx = lua_gettop(L); + int n = (int)lua_objlen(L, idx); + buf_t sub; + buf_init(&sub); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, idx, i); + write_varint(&sub, (uint64_t)(int64_t)lua_tointeger(L, -1)); + lua_pop(L, 1); + } + write_varint(&b, (7 << 3) | 2); + write_varint(&b, (uint64_t)sub.len); + write_bytes(&b, sub.data, sub.len); + buf_free(&sub); + } + lua_pop(L, 1); + + lua_pushlstring(L, (const char *)b.data, b.len); + buf_free(&b); + return 1; +} + +/* ---------------------------------------------------------------- * + * Person_decode(string) -> tbl * + * ---------------------------------------------------------------- */ + +static int +Person_decode(lua_State *L) +{ + size_t len; + const char *buf = luaL_checklstring(L, 1, &len); + const uint8_t *p = (const uint8_t *)buf; + const uint8_t *end = p + len; + + lua_createtable(L, 0, 5); + const int result_idx = lua_gettop(L); + + /* Lazy arrays: stash stack index of the array table once created. */ + int emails_stkidx = 0; + int n_emails = 0; + int lucky_stkidx = 0; + int n_lucky = 0; + + while (p < end) { + uint64_t tag; + p = read_varint(p, end, &tag); + if (!p) + break; + int field = (int)(tag >> 3); + int wt = (int)(tag & 7); + + if (wt == 2) { + uint64_t slen; + p = read_varint(p, end, &slen); + if (!p || (size_t)(end - p) < slen) + break; + const uint8_t *fend = p + slen; + + switch (field) { + case 1: /* name */ + lua_pushlstring(L, (const char *)p, (size_t)slen); + lua_setfield(L, result_idx, "name"); + break; + case 3: /* emails (repeated string) */ + if (emails_stkidx == 0) { + lua_createtable(L, 4, 0); + emails_stkidx = lua_gettop(L); + } + lua_pushlstring(L, (const char *)p, (size_t)slen); + lua_rawseti(L, emails_stkidx, ++n_emails); + break; + case 5: /* address (sub-message) */ + if (decode_address(L, p, fend) == NULL) + goto done; + lua_setfield(L, result_idx, "address"); + break; + case 7: { /* lucky_numbers (packed int32) */ + if (lucky_stkidx == 0) { + lua_createtable(L, 8, 0); + lucky_stkidx = lua_gettop(L); + } + const uint8_t *q = p; + while (q < fend) { + uint64_t v; + q = read_varint(q, fend, &v); + if (!q) + break; + lua_pushinteger(L, + (lua_Integer)(int32_t)v); + lua_rawseti(L, lucky_stkidx, + ++n_lucky); + } + break; + } + default: + /* Unknown LEN field -- skip silently. */ + break; + } + p = fend; + } else if (wt == 0) { + uint64_t v; + p = read_varint(p, end, &v); + if (!p) + break; + if (field == 2) { /* age */ + lua_pushinteger(L, (lua_Integer)(int32_t)v); + lua_setfield(L, result_idx, "age"); + } + } else { + /* Other wire types not exercised by bench payloads. */ + break; + } + } +done: + if (emails_stkidx) { + lua_pushvalue(L, emails_stkidx); + lua_setfield(L, result_idx, "emails"); + } + if (lucky_stkidx) { + lua_pushvalue(L, lucky_stkidx); + lua_setfield(L, result_idx, "lucky_numbers"); + } + lua_settop(L, result_idx); + return 1; +} + +/* ---------------------------------------------------------------- * + * Module entry. * + * ---------------------------------------------------------------- */ + +static const struct luaL_Reg lib[] = { + {"Person_encode", Person_encode}, + {"Person_decode", Person_decode}, + {NULL, NULL}, +}; + +LUA_API int +luaopen_pb_c_person(lua_State *L) +{ + luaL_register(L, "pb_c_person", lib); + return 1; +} diff --git a/bench/c_accel/prim.c b/bench/c_accel/prim.c new file mode 100644 index 0000000000000000000000000000000000000000..16ca0b58a85d1baea3ef41809df4db33beede5d6 --- /dev/null +++ b/bench/c_accel/prim.c @@ -0,0 +1,121 @@ +/* + * prim.c -- per-primitive wire helpers exposed via plain C ABI. + * + * Strategy 2 of tarantool-protobuf-04c. Lua-side dispatch stays in + * Lua (read t.name, t.age, ... via t-table accesses), but each + * wire-format primitive crosses the FFI boundary. + * + * Compile as a shared lib loaded by ffi.load() -- not luaopen_*. + */ + +#include +#include +#include + +#if defined(_WIN32) +#define EXPORT __declspec(dllexport) +#else +#define EXPORT __attribute__((visibility("default"))) +#endif + +/* ibuf shape mirrors the FFI cdef in prim_ffi.lua. */ +typedef struct ibuf_s { + uint8_t *data; + size_t len; + size_t cap; +} ibuf_t; + +EXPORT void +pb_ibuf_init(ibuf_t *b) +{ + b->cap = 4096; + b->data = (uint8_t *)malloc(b->cap); + b->len = 0; +} + +EXPORT void +pb_ibuf_free(ibuf_t *b) +{ + free(b->data); + b->data = NULL; + b->cap = 0; + b->len = 0; +} + +EXPORT void +pb_ibuf_reset(ibuf_t *b) +{ + b->len = 0; +} + +static void +pb_ibuf_grow(ibuf_t *b, size_t need) +{ + size_t nc = b->cap; + while (nc < b->len + need) + nc *= 2; + b->data = (uint8_t *)realloc(b->data, nc); + b->cap = nc; +} + +static inline void +pb_ibuf_reserve(ibuf_t *b, size_t need) +{ + if (b->len + need > b->cap) + pb_ibuf_grow(b, need); +} + +/* ---------------------------------------------------------------- * + * Primitives. * + * ---------------------------------------------------------------- */ + +EXPORT void +pb_write_varint(ibuf_t *b, uint64_t v) +{ + pb_ibuf_reserve(b, 10); + while (v >= 0x80) { + b->data[b->len++] = (uint8_t)(v | 0x80); + v >>= 7; + } + b->data[b->len++] = (uint8_t)v; +} + +EXPORT void +pb_write_bytes(ibuf_t *b, const uint8_t *src, size_t n) +{ + pb_ibuf_reserve(b, n); + memcpy(b->data + b->len, src, n); + b->len += n; +} + +/* Combined "string field": tag + length + payload. Three primitives + * fused into one to lower FFI call count for the most common field + * shape; honesty note in README. */ +EXPORT void +pb_write_string_field(ibuf_t *b, uint32_t tag, const uint8_t *src, + size_t n) +{ + pb_write_varint(b, tag); + pb_write_varint(b, n); + pb_write_bytes(b, src, n); +} + +/* Returns a pointer past the varint, or NULL on truncation. */ +EXPORT const uint8_t * +pb_read_varint(const uint8_t *p, const uint8_t *end, uint64_t *out) +{ + uint64_t v = 0; + int shift = 0; + while (p < end) { + uint8_t c = *p++; + v |= (uint64_t)(c & 0x7f) << shift; + if (!(c & 0x80)) { + *out = v; + return p; + } + shift += 7; + if (shift >= 64) + return NULL; + } + return NULL; +} diff --git a/bench/c_accel/prim_ffi.lua b/bench/c_accel/prim_ffi.lua new file mode 100644 index 0000000000000000000000000000000000000000..79eb0efe03634656fd7c236e173cadf8dc4ab633 --- /dev/null +++ b/bench/c_accel/prim_ffi.lua @@ -0,0 +1,197 @@ +-- prim_ffi.lua -- Strategy 2: per-primitive FFI bindings. +-- +-- Dispatch (per-field branch logic) is in Lua. Each wire primitive +-- crosses the FFI boundary into prim.c. Used by spike_bench.lua via +-- require('prim_ffi'). + +local ffi = require('ffi') +local bit = require('bit') + +ffi.cdef[[ + typedef struct ibuf_s { + uint8_t *data; + size_t len; + size_t cap; + } ibuf_t; + + void pb_ibuf_init(ibuf_t *b); + void pb_ibuf_free(ibuf_t *b); + void pb_ibuf_reset(ibuf_t *b); + + void pb_write_varint(ibuf_t *b, uint64_t v); + void pb_write_bytes(ibuf_t *b, const uint8_t *src, size_t n); + void pb_write_string_field(ibuf_t *b, uint32_t tag, + const uint8_t *src, size_t n); + const uint8_t *pb_read_varint(const uint8_t *p, + const uint8_t *end, uint64_t *out); +]] + +local function find_lib() + local SCRIPT_DIR = (debug.getinfo(1, 'S').source:match('@?(.*/)') or './') + local UNAME = io.popen('uname -s'):read('*l') + local ext = (UNAME == 'Darwin') and '.dylib' or '.so' + return SCRIPT_DIR .. 'libpb_prim' .. ext +end + +local C = ffi.load(find_lib()) + +local outbuf = ffi.new('ibuf_t') +C.pb_ibuf_init(outbuf) +local subbuf = ffi.new('ibuf_t') +C.pb_ibuf_init(subbuf) +local sub2buf = ffi.new('ibuf_t') -- for one extra level of nesting +C.pb_ibuf_init(sub2buf) + +local v_out = ffi.new('uint64_t[1]') + +local rshift, band = bit.rshift, bit.band + +local M = {} + +-- ---------------------------------------------------------------- +-- Address (sub-message used by Person.address) encode/decode helpers +-- ---------------------------------------------------------------- + +local function address_encode_into(buf, t) + if t.street then + C.pb_write_string_field(buf, 0x0A, t.street, #t.street) + end + if t.city then + C.pb_write_string_field(buf, 0x12, t.city, #t.city) + end + if t.zip then + C.pb_write_varint(buf, 0x18) + C.pb_write_varint(buf, t.zip) + end +end + +-- ---------------------------------------------------------------- +-- Person encode +-- ---------------------------------------------------------------- + +function M.Person_encode(t) + C.pb_ibuf_reset(outbuf) + if t.name then + C.pb_write_string_field(outbuf, 0x0A, t.name, #t.name) + end + if t.age then + C.pb_write_varint(outbuf, 0x10) + C.pb_write_varint(outbuf, t.age) + end + if t.emails then + local emails = t.emails + for i = 1, #emails do + local e = emails[i] + C.pb_write_string_field(outbuf, 0x1A, e, #e) + end + end + if t.address then + C.pb_ibuf_reset(subbuf) + address_encode_into(subbuf, t.address) + C.pb_write_varint(outbuf, 0x2A) + C.pb_write_varint(outbuf, subbuf.len) + C.pb_write_bytes(outbuf, subbuf.data, subbuf.len) + end + if t.lucky_numbers then + C.pb_ibuf_reset(subbuf) + local lucky = t.lucky_numbers + for i = 1, #lucky do + C.pb_write_varint(subbuf, lucky[i]) + end + C.pb_write_varint(outbuf, 0x3A) + C.pb_write_varint(outbuf, subbuf.len) + C.pb_write_bytes(outbuf, subbuf.data, subbuf.len) + end + return ffi.string(outbuf.data, outbuf.len) +end + +-- ---------------------------------------------------------------- +-- Person decode +-- ---------------------------------------------------------------- + +local function decode_address(p, endp) + local result = {} + while p < endp do + p = C.pb_read_varint(p, endp, v_out) + if p == nil then break end + local tag = tonumber(v_out[0]) + local field = rshift(tag, 3) + local wt = band(tag, 7) + if wt == 2 then + p = C.pb_read_varint(p, endp, v_out) + local slen = tonumber(v_out[0]) + if field == 1 then + result.street = ffi.string(p, slen) + elseif field == 2 then + result.city = ffi.string(p, slen) + end + p = p + slen + elseif wt == 0 then + p = C.pb_read_varint(p, endp, v_out) + if field == 3 then + result.zip = tonumber(v_out[0]) + end + else + break + end + end + return result +end + +function M.Person_decode(s) + local p = ffi.cast('const uint8_t*', s) + local endp = p + #s + local result = {} + local emails = nil + local n_emails = 0 + local lucky = nil + local n_lucky = 0 + + while p < endp do + p = C.pb_read_varint(p, endp, v_out) + if p == nil then break end + local tag = tonumber(v_out[0]) + local field = rshift(tag, 3) + local wt = band(tag, 7) + if wt == 2 then + p = C.pb_read_varint(p, endp, v_out) + local slen = tonumber(v_out[0]) + if field == 1 then + result.name = ffi.string(p, slen) + p = p + slen + elseif field == 3 then + if emails == nil then emails = {} end + n_emails = n_emails + 1 + emails[n_emails] = ffi.string(p, slen) + p = p + slen + elseif field == 5 then + result.address = decode_address(p, p + slen) + p = p + slen + elseif field == 7 then + if lucky == nil then lucky = {} end + local fend = p + slen + while p < fend do + p = C.pb_read_varint(p, fend, v_out) + if p == nil then break end + n_lucky = n_lucky + 1 + lucky[n_lucky] = tonumber(v_out[0]) + end + else + p = p + slen + end + elseif wt == 0 then + p = C.pb_read_varint(p, endp, v_out) + if field == 2 then + result.age = tonumber(v_out[0]) + end + else + break + end + end + + if emails then result.emails = emails end + if lucky then result.lucky_numbers = lucky end + return result +end + +return M diff --git a/bench/c_accel/spike_bench.lua b/bench/c_accel/spike_bench.lua new file mode 100644 index 0000000000000000000000000000000000000000..b3725abad60b00b335bfb080c9dc1d8a917633aa --- /dev/null +++ b/bench/c_accel/spike_bench.lua @@ -0,0 +1,168 @@ +#!/usr/bin/env tarantool +-- Spike harness for tarantool-protobuf-04c. +-- +-- Compares pure-Lua `full` mode (current baseline) against the +-- hand-written C codec for hello.Person across 5 payload sizes. +-- Strategies 2 (per-primitive FFI) and 3 (one generic C call) land +-- in follow-up sub-issues. +-- +-- Usage: +-- make -C bench/c_accel +-- tarantool bench/c_accel/spike_bench.lua + +local SCRIPT_DIR = (debug.getinfo(1, 'S').source:match('@?(.*/)') or './') +local REPO_ROOT = SCRIPT_DIR .. '../..' + +package.path = REPO_ROOT .. '/runtime/?.lua;' + .. REPO_ROOT .. '/runtime/?/init.lua;' + .. REPO_ROOT .. '/examples/expected/?.lua;' + .. REPO_ROOT .. '/examples/expected/?/init.lua;' + .. package.path +package.cpath = SCRIPT_DIR .. '?.dylib;' .. SCRIPT_DIR .. '?.so;' .. package.cpath + +local clock = require('clock') + +local full = require('full.hello.hello_pb') + +local ok, c_person = pcall(require, 'pb_c_person') +if not ok then + io.stderr:write('failed to load pb_c_person: ' .. tostring(c_person) .. '\n') + io.stderr:write('run `make -C bench/c_accel` first\n') + os.exit(1) +end + +local ok2, c_generic = pcall(require, 'pb_c_generic') +if not ok2 then + io.stderr:write('failed to load pb_c_generic: ' .. tostring(c_generic) .. '\n') + io.stderr:write('run `make -C bench/c_accel` first\n') + os.exit(1) +end + +local ok3, prim_ffi = pcall(require, 'prim_ffi') +if not ok3 then + io.stderr:write('failed to load prim_ffi: ' .. tostring(prim_ffi) .. '\n') + io.stderr:write('run `make -C bench/c_accel` first\n') + os.exit(1) +end + +-- Payload builder mirrors bench/bench.lua so numbers are comparable. +local function build_person_payload(target) + if target <= 10 then + return {name = 'bigbes', age = 42} + end + if target <= 100 then + return {name = string.rep('a', target - 10), age = 42} + end + local per_email = 36 + local fixed_bytes = 80 + local n_emails = math.max(1, + math.floor((target - fixed_bytes) / per_email)) + local p = { + name = 'bigbes', age = 42, + address = {street = '1 Main St', city = 'Springfield', zip = 12345}, + lucky_numbers = {7, 13, 21, 42, 99}, + emails = {}, + } + for i = 1, n_emails do + p.emails[i] = string.rep('e', 28) .. string.format('%04d', i) + end + return p +end + +local SIZES = { + {label = '10B', target = 10}, + {label = '100B', target = 100}, + {label = '1KB', target = 1024}, + {label = '10KB', target = 10240}, + {label = '100KB', target = 102400}, +} + +local function iter_count(size_bytes) + if size_bytes < 100 then return 200000 end + if size_bytes < 2000 then return 50000 end + if size_bytes < 20000 then return 5000 end + return 500 +end + +local function summarize(samples) + table.sort(samples) + return samples[math.floor((#samples + 1) / 2)] +end + +local function time_loop(fn, n) + local t0 = clock.monotonic64() + for _ = 1, n do fn() end + local t1 = clock.monotonic64() + return tonumber(t1 - t0) / 1e9 +end + +local function bench(fn, n, runs) + for _ = 1, math.min(n, 1000) do fn() end + local samples = {} + for r = 1, runs do + collectgarbage('collect') + samples[r] = time_loop(fn, n) + end + return summarize(samples) / n -- seconds per op +end + +-- Sanity: every encoder must emit byte-equal output, every decoder must +-- return a table. +local function sanity_check() + for _, sz in ipairs(SIZES) do + local p = build_person_payload(sz.target) + local lua_bytes = full.Person_encode(p) + for label, mod in pairs({c_person = c_person, c_generic = c_generic, prim_ffi = prim_ffi}) do + local bytes = mod.Person_encode(p) + if bytes ~= lua_bytes then + io.stderr:write(string.format( + 'sanity FAIL %s at %s: lua=%d bytes %s=%d bytes\n', + label, sz.label, #lua_bytes, label, #bytes)) + end + local back = mod.Person_decode(lua_bytes) + if type(back) ~= 'table' then + io.stderr:write(label .. ' decode non-table at ' .. sz.label .. '\n') + os.exit(2) + end + end + end +end + +sanity_check() + +local function fmt_op(t, bytes) + return string.format('%8.0f / %7.1f', 1 / t, bytes / t / 1e6) +end + +io.write('hello.Person — pure-Lua (full) vs S2 FFI prims vs S3 generic C vs S4 hand C\n') +io.write('×L columns = speedup vs pure-Lua baseline\n\n') +io.write(string.format( + '%-6s %7s %18s %18s %5s %18s %5s %18s %5s\n', + 'size', 'bytes', 'pure-Lua', 'S2 FFI prim', '×L', 'S3 generic C', '×L', 'S4 hand C', '×L')) +io.write(string.rep('-', 124) .. '\n') + +local runs = 5 +local function run_phase(phase_name, get_fn) + io.write(string.format('\n== %s ==\n', phase_name)) + for _, sz in ipairs(SIZES) do + local p = build_person_payload(sz.target) + local bytes = full.Person_encode(p) + local n = iter_count(#bytes) + local t_lua = bench(get_fn(full, p, bytes), n, runs) + local t_s2 = bench(get_fn(prim_ffi, p, bytes), n, runs) + local t_s3 = bench(get_fn(c_generic, p, bytes), n, runs) + local t_s4 = bench(get_fn(c_person, p, bytes), n, runs) + io.write(string.format( + '%-6s %7d %18s %18s %5.2f %18s %5.2f %18s %5.2f\n', + sz.label, #bytes, + fmt_op(t_lua, #bytes), + fmt_op(t_s2, #bytes), t_lua / t_s2, + fmt_op(t_s3, #bytes), t_lua / t_s3, + fmt_op(t_s4, #bytes), t_lua / t_s4)) + end +end + +run_phase('ENCODE', function(mod, p, _) return function() mod.Person_encode(p) end end) +run_phase('DECODE', function(mod, _, bytes) return function() mod.Person_decode(bytes) end end) + +os.exit(0)