diff --git a/bench/alloc_probe.lua b/bench/alloc_probe.lua new file mode 100644 index 0000000000000000000000000000000000000000..eddde647b4a54b823d14776587a83a54e469898a --- /dev/null +++ b/bench/alloc_probe.lua @@ -0,0 +1,167 @@ +#!/usr/bin/env tarantool +-- Surgical alloc probe. Strips the encode path apart, measures KB-delta +-- per primitive operation, so we can attribute the ~136 B/op encode floor +-- to a specific source (table, varint string, result string, ...). +-- +-- All cases run with GC stopped so allocations accumulate. We divide by +-- N to get bytes/op. To eliminate string-interning noise we use *unique* +-- input bytes per iteration where appropriate (suffix counter). + +package.path = './runtime/?.lua;./runtime/?/init.lua;' + .. './examples/expected/?.lua;./examples/expected/?/init.lua;' + .. package.path + +local wire = require('pb.wire') +local hello = require('full.hello.hello_pb') + +local N = 50000 + +local function alloc_per_op(label, fn, mk_arg) + -- Warmup: let the JIT compile and any one-shot allocations settle. + for i = 1, 1000 do fn(mk_arg and mk_arg(i) or nil) end + collectgarbage('collect') + collectgarbage('stop') + local before = collectgarbage('count') + for i = 1, N do fn(mk_arg and mk_arg(i) or nil) end + local after = collectgarbage('count') + collectgarbage('restart') + collectgarbage('collect') + local per_op = (after - before) * 1024 / N + print(string.format(' %-50s %8.1f B/op', label, per_op)) +end + +print('=== Baseline: noop ===') +alloc_per_op('empty function', function() end) +alloc_per_op('return nil', function() return nil end) + +print('\n=== Table allocation ===') +alloc_per_op('local t = {}', function() local t = {} end) +alloc_per_op('local t, n = {}, 0', function() local t, n = {}, 0 end) +alloc_per_op('local t = {}; t[1]=1; t[2]=2; t[3]=3', function() + local t = {}; t[1] = 1; t[2] = 2; t[3] = 3 +end) +alloc_per_op('local t = {}; for i=1,5 do t[i]=i end', function() + local t = {}; for i = 1, 5 do t[i] = i end +end) +alloc_per_op('local t = {}; for i=1,10 do t[i]=i end', function() + local t = {}; for i = 1, 10 do t[i] = i end +end) + +print('\n=== wire.encode_varint (interned: same input) ===') +alloc_per_op('encode_varint(42) [1-byte fast]', function() + local s = wire.encode_varint(42) +end) +alloc_per_op('encode_varint(200) [2-byte slow]', function() + local s = wire.encode_varint(200) +end) +alloc_per_op('encode_varint(1e6) [3-byte slow]', function() + local s = wire.encode_varint(1000000) +end) + +print('\n=== wire.encode_varint (unique per iter) ===') +alloc_per_op('encode_varint(i % 128) [1-byte, unique-ish]', function(i) + local s = wire.encode_varint(i % 128) +end, function(i) return i end) +alloc_per_op('encode_varint(128 + i) [2-byte, unique]', function(i) + local s = wire.encode_varint(128 + i) +end, function(i) return i end) + +print('\n=== string.char (intern check) ===') +alloc_per_op('string.char(42) [1-byte, same]', function() + local s = string.char(42) +end) +alloc_per_op('string.char(i % 256) [1-byte, varying]', function(i) + local s = string.char(i % 256) +end, function(i) return i end) + +print('\n=== table.concat ===') +alloc_per_op('concat of 3 short literal strings', function() + local s = table.concat({"\x0a", "\x06", "bigbes"}) +end) +alloc_per_op('concat of 5 short literal strings', function() + local s = table.concat({"\x0a", "\x06", "bigbes", "\x10", "\x2a"}) +end) + +print('\n=== Full encode (Person fixtures) ===') +local p10 = {name = 'bigbes', age = 42} +alloc_per_op('Person_encode 10B (same input each iter)', function() + local s = hello.Person_encode(p10) +end) + +-- Unique per iter via the lucky_numbers field (varying). +alloc_per_op('Person_encode 10B (varying age field)', function(i) + p10.age = 42 + (i % 100) + local s = hello.Person_encode(p10) +end, function(i) return i end) + +local p100 = {name = string.rep('a', 90), age = 42} +alloc_per_op('Person_encode 100B (same input)', function() + local s = hello.Person_encode(p100) +end) + +-- Decode floor probe. +local p10_bytes = hello.Person_encode(p10) +local p100_bytes = hello.Person_encode(p100) +print('\n=== Full decode (Person fixtures) ===') +alloc_per_op('Person_decode 10B (same input)', function() + local t = hello.Person_decode(p10_bytes) +end) +alloc_per_op('Person_decode 100B (same input)', function() + local t = hello.Person_decode(p100_bytes) +end) + +print('\n=== Drill-down: encode pieces (10B Person) ===') +-- Imitate Person_encode body manually so we can attribute each step. +alloc_per_op('table {} + n', function() + local out, n = {}, 0 +end) +alloc_per_op('+ append tag literal', function() + local out, n = {}, 0 + n = n + 1; out[n] = "\x0a" +end) +alloc_per_op('+ append name varlen + name string', function() + local out, n = {}, 0 + local v = 'bigbes' + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_varint(#v) + n = n + 1; out[n] = v +end) +alloc_per_op('+ append age tag + varint', function() + local out, n = {}, 0 + local v = 'bigbes' + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_varint(#v) + n = n + 1; out[n] = v + n = n + 1; out[n] = "\x10" + n = n + 1; out[n] = wire.encode_varint(42) +end) +alloc_per_op('+ final table.concat (full path)', function() + local out, n = {}, 0 + local v = 'bigbes' + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_varint(#v) + n = n + 1; out[n] = v + n = n + 1; out[n] = "\x10" + n = n + 1; out[n] = wire.encode_varint(42) + local r = table.concat(out) +end) + +print('\n=== Large payload encode (unique output to defeat interning) ===') +-- Build a 1KB+ Person; vary an integer field so the encoded output is +-- unique per iter and the result string can't be interned. +local p1k = { + name = 'bigbes', age = 42, + address = {street = '1 Main St', city = 'Springfield', zip = 12345}, + lucky_numbers = {7, 13, 21, 42, 99}, + emails = {}, +} +for i = 1, 26 do p1k.emails[i] = string.rep('e', 28) .. string.format('%04d', i) end +print(string.format(' 1KB Person encoded size = %d bytes', #hello.Person_encode(p1k))) + +alloc_per_op('Person_encode 1KB (same input — interned)', function() + local s = hello.Person_encode(p1k) +end) +alloc_per_op('Person_encode 1KB (varying age — NOT interned)', function(i) + p1k.age = 42 + i + local s = hello.Person_encode(p1k) +end, function(i) return i end)