~bigbes/tarantool

tarantool-protobuf

ref: 8721548da9dc69a1e0bc110023db3ecadb722f3c tarantool-protobuf/bench/c_accel/spike_bench.lua -rw-r--r-- 5.6 KiB
8721548d — Eugene Blikh lsp: foundational types + annotate pb runtime entry + lazy views 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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)