From 5c5414165838069d5fcfc199bf1ebf9690c7c3c8 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sat, 23 May 2026 12:17:51 +0300 Subject: [PATCH] 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 and friends -- mirrors runtime/pb/c/compile_flags.txt. --- bench/c_accel/README.md | 65 +++++++++++++++++++++------------ bench/c_accel/compile_flags.txt | 7 ++++ bench/c_accel/person_codec.c | 32 +++++++++------- 3 files changed, 67 insertions(+), 37 deletions(-) create mode 100644 bench/c_accel/compile_flags.txt diff --git a/bench/c_accel/README.md b/bench/c_accel/README.md index a8715c20288b4a3bed86756137d1576403d76730..6c1867677bf3864f100a750770a71ff890cb2d67 100644 --- a/bench/c_accel/README.md +++ b/bench/c_accel/README.md @@ -57,7 +57,7 @@ Override the Tarantool include dir if auto-detection fails: make -C bench/c_accel TT_INC=/path/to/include/tarantool ``` -## Results — 2026-05-18 +## Results — 2026-05-23 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 @@ -67,45 +67,62 @@ 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 | +| 10B | 10 | 3,008,967 (30.1) | 3,034,763 (30.3) | 1.01 | 10,407,993 (104.1) | 3.46 | 11,633,995 (116.3) | 3.87 | +| 100B | 94 | 2,936,858 (276.1) | 2,888,587 (271.5) | 0.98 | 9,989,012 (939.0) | 3.40 | 11,122,852 (1045.5) | 3.79 | +| 1KB | 930 | 374,964 (348.7) | 298,388 (277.5) | 0.80 | 1,942,426 (1806.5) | 5.18 | 1,992,349 (1852.9) | 5.31 | +| 10KB | 9,634 | 73,390 (707.0) | 49,761 (479.4) | 0.68 | 297,018 (2861.5) | 4.05 | 287,786 (2772.5) | 3.92 | +| 100KB | 96,674 | 8,857 (856.2) | 5,341 (516.4) | 0.60 | 30,244 (2923.8) | 3.41 | 29,459 (2847.9) | 3.33 | ### 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 | +| 10B | 10 | 3,368,932 (33.7) | 922,203 (9.2) | 0.27 | 8,162,932 (81.6) | 2.42 | 8,099,789 (81.0) | 2.40 | +| 100B | 94 | 2,801,552 (263.3) | 915,311 (86.0) | 0.33 | 7,825,645 (735.6) | 2.79 | 8,029,549 (754.8) | 2.87 | +| 1KB | 930 | 157,212 (146.2) | 58,902 (54.8) | 0.37 | 1,049,098 (975.7) | 6.67 | 1,149,822 (1069.3) | 7.31 | +| 10KB | 9,634 | 21,627 (208.4) | 8,179 (78.8) | 0.38 | 206,740 (1991.7) | 9.56 | 211,077 (2033.5) | 9.76 | +| 100KB | 96,674 | 2,242 (216.7) | 839 (81.1) | 0.37 | 22,901 (2213.9) | 10.22 | 23,956 (2315.9) | 10.69 | ### 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 + the C boundary *once* per message wins 2.4–10.7×. 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 ±5% at every size.** The descriptor-walk + dispatch overhead is in the noise. This is the most important + result for the `pf6` architecture call: shipping the generic + one-call codec (`ra6`) lands within noise of the hand-written + ceiling, so codegen-emitted per-message C (`c0i`) buys nothing. +- **C encode plateaus at ~2.8–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. + Pure-Lua decode is per-byte cliff-y (157 k msg/s @ 1KB → + 2.2 k @ 100KB); C decode degrades roughly linearly with size, + hitting 2.3 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. +### History: why S4 once looked slower than S3 at 1KB+ + +The 2026-05-18 snapshot of these numbers showed S4 encode trailing +S3 by 10–20% at 1 KB+, and we hypothesised it was branch prediction +on the hand-written codec's divergent per-field paths. That was +wrong. The actual cause was that `person_codec.c`'s emails loop did +a defensive `lua_type(L, -1) == LUA_TSTRING` check on each element +before `lua_tolstring`, while `generic_codec.c` skipped it. With +~2,700 emails at 100 KB that's 2,700 extra C calls per message in +the hot path. Replacing the per-element `lua_type` check with no +check (and the field-level type checks with `lua_isnil`, matching +the generic codec's semantics) closed the gap. The lesson generalises: +when comparing two C codecs that look "the same shape", measure +their per-element work, not their dispatch shape — the boundary +crossings to the Lua stack dominate everything else. + ### What this means for the architecture (`pf6`) - **Ship `ra6` (generic C runtime, one C call per message).** It's diff --git a/bench/c_accel/compile_flags.txt b/bench/c_accel/compile_flags.txt new file mode 100644 index 0000000000000000000000000000000000000000..3f7c031b53264e8efceda6425caa46bd1db308e1 --- /dev/null +++ b/bench/c_accel/compile_flags.txt @@ -0,0 +1,7 @@ +-std=c99 +-Wall +-Wextra +-I/opt/homebrew/include/tarantool +-I/opt/homebrew/opt/tarantool/include/tarantool +-I/usr/local/include/tarantool +-I/usr/include/tarantool diff --git a/bench/c_accel/person_codec.c b/bench/c_accel/person_codec.c index 6a1725915f6c04670ff9943e53d68212d5041fe6..2b3347fc7fdba469452bd9ece0044479e2564602 100644 --- a/bench/c_accel/person_codec.c +++ b/bench/c_accel/person_codec.c @@ -11,6 +11,14 @@ * weight_kg, maps) are intentionally absent. The spike measures the * upper bound of C boundary perf for the bench shapes, not full * codec coverage. + * + * Type-check parity with generic_codec.c: field presence is via + * lua_isnil, with no per-element lua_type check inside repeated + * loops. The original (2026-05-18) version did defensive lua_type + * checks per element, which added ~2700 extra C calls per message + * at 100KB and made S4 look slower than S3 at 1KB+. Don't add them + * back without re-measuring; see README for the post-correction + * numbers. */ #include @@ -127,7 +135,7 @@ 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) { + if (!lua_isnil(L, -1)) { size_t n; const char *s = lua_tolstring(L, -1, &n); write_string_field(b, (1 << 3) | 2, s, n); @@ -135,7 +143,7 @@ encode_address_body(buf_t *b, lua_State *L, int t) lua_pop(L, 1); lua_getfield(L, t, "city"); - if (lua_type(L, -1) == LUA_TSTRING) { + if (!lua_isnil(L, -1)) { size_t n; const char *s = lua_tolstring(L, -1, &n); write_string_field(b, (2 << 3) | 2, s, n); @@ -143,7 +151,7 @@ encode_address_body(buf_t *b, lua_State *L, int t) lua_pop(L, 1); lua_getfield(L, t, "zip"); - if (lua_type(L, -1) == LUA_TNUMBER) { + if (!lua_isnil(L, -1)) { write_varint(b, (3 << 3) | 0); write_varint(b, (uint64_t)(int64_t)lua_tointeger(L, -1)); } @@ -205,7 +213,7 @@ Person_encode(lua_State *L) /* name (1, string) */ lua_getfield(L, t, "name"); - if (lua_type(L, -1) == LUA_TSTRING) { + if (!lua_isnil(L, -1)) { size_t n; const char *s = lua_tolstring(L, -1, &n); write_string_field(&b, (1 << 3) | 2, s, n); @@ -214,7 +222,7 @@ Person_encode(lua_State *L) /* age (2, int32) */ lua_getfield(L, t, "age"); - if (lua_type(L, -1) == LUA_TNUMBER) { + if (!lua_isnil(L, -1)) { write_varint(&b, (2 << 3) | 0); write_varint(&b, (uint64_t)(int64_t)lua_tointeger(L, -1)); } @@ -222,16 +230,14 @@ Person_encode(lua_State *L) /* emails (3, repeated string) */ lua_getfield(L, t, "emails"); - if (lua_type(L, -1) == LUA_TTABLE) { + if (!lua_isnil(L, -1)) { 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); - } + size_t n; + const char *s = lua_tolstring(L, -1, &n); + write_string_field(&b, (3 << 3) | 2, s, n); lua_pop(L, 1); } } @@ -239,7 +245,7 @@ Person_encode(lua_State *L) /* address (5, sub-message) */ lua_getfield(L, t, "address"); - if (lua_type(L, -1) == LUA_TTABLE) { + if (!lua_isnil(L, -1)) { int addr_idx = lua_gettop(L); buf_t sub; buf_init(&sub); @@ -253,7 +259,7 @@ Person_encode(lua_State *L) /* lucky_numbers (7, packed int32) */ lua_getfield(L, t, "lucky_numbers"); - if (lua_type(L, -1) == LUA_TTABLE) { + if (!lua_isnil(L, -1)) { int idx = lua_gettop(L); int n = (int)lua_objlen(L, idx); buf_t sub;