~bigbes/tarantool

tarantool-protobuf

ref: 035b2adbf9bf91e4cfc031aa1141ab75d0bb30f1 tarantool-protobuf/bench/lazy_bench.lua -rw-r--r-- 4.7 KiB
035b2adb — Eugene Blikh codegen: CHARS[_len] lookup replaces string.char(_len) at length-prefix sites (2ri) 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
#!/usr/bin/env tarantool
-- Lazy-decode micro-benchmark.
--
-- Measures the read+mutate+re-encode workload where lazy decode_lazy
-- (zero-copy index + passthrough on untouched fields) is expected to beat
-- eager decode+encode: large messages where the caller only touches a
-- few fields. Three scenarios:
--
--   1. passthrough: decode_lazy + :encode (no mutation)
--      The strongest claim. Eager has to materialize the whole table
--      and re-walk every field on encode; lazy returns the original
--      bytes verbatim after a single tag-scanning pass.
--
--   2. sparse read: decode_lazy + :get(a) + :get(b)
--      The proxy/router workload — touch a handful of fields out of
--      many. Eager pays for materializing everything, lazy only for
--      the fields read.
--
--   3. one-field rewrite: decode_lazy + :get(a) + :set(b, ...) + :encode
--      The mutation passthrough case. Eager re-encodes every field,
--      lazy splices the original bytes for everything but the dirty one.
--
-- Run:  tarantool bench/lazy_bench.lua
-- Output is human-readable on stderr (varies with CPU load; not committed).

package.path = './runtime/?.lua;./runtime/?/init.lua;'
    .. './examples/expected/?.lua;./examples/expected/?/init.lua;'
    .. package.path

local clock = require('clock')

local SIZES = {
    {label = '1KB',   target = 1024},
    {label = '10KB',  target = 10240},
    {label = '100KB', target = 102400},
}

-- Emails-heavy Person — scales repeated strings to hit target size.
-- Index pass cost ≈ decode cost on this shape: every email has its own
-- tag/len scan, so lazy can't skip-jump over large subtrees. Use this
-- shape to honestly bound the lazy-vs-eager comparison on a workload
-- that gives lazy *no* structural advantage from skip_field.
local function build_payload(target)
    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 function iter_count(size_bytes)
    if size_bytes <  2000  then return  50000 end
    if size_bytes <  20000 then return   5000 end
    return 500
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)
    for _ = 1, math.min(n, 1000) do fn() end  -- warmup
    local best = math.huge
    for _ = 1, 5 do
        collectgarbage('collect')
        local t = time_loop(fn, n)
        if t < best then best = t end
    end
    return n / best  -- msgs/s at best-run
end

local function compare(label, eager_fn, lazy_fn, n)
    local e = bench(eager_fn, n)
    local l = bench(lazy_fn,  n)
    local ratio = l / e
    io.stderr:write(string.format(
        '  %-40s  eager %10.0f msgs/s  lazy %10.0f msgs/s  %s%.2fx\n',
        label, e, l, ratio >= 1 and 'lazy ' or 'lazy ', ratio))
end

for _, mode in ipairs({'full', 'runtime'}) do
    io.stderr:write('\n== mode: ' .. mode .. ' ==\n')
    local hello = require(mode .. '.hello.hello_pb')

    for _, size in ipairs(SIZES) do
        io.stderr:write(string.format('\n  size: %s\n', size.label))
        local payload = build_payload(size.target)
        local bytes   = hello.Person_encode(payload)
        local n       = iter_count(#bytes)

        -- 1. passthrough: decode + re-encode with no mutation
        compare('passthrough (decode + reencode)',
            function() hello.Person_encode(hello.Person_decode(bytes)) end,
            function() return hello.Person_decode_lazy(bytes):encode() end,
            n)

        -- 2. sparse read: read 2 top-level fields
        compare('sparse read (name + age)',
            function()
                local t = hello.Person_decode(bytes)
                local _ = t.name; local _2 = t.age
            end,
            function()
                local v = hello.Person_decode_lazy(bytes)
                local _ = v:get('name'); local _2 = v:get('age')
            end,
            n)

        -- 3. one-field rewrite: change `name`, keep everything else
        compare('rewrite name (decode + set + reencode)',
            function()
                local t = hello.Person_decode(bytes)
                t.name = 'mallory'
                local _ = hello.Person_encode(t)
            end,
            function()
                local v = hello.Person_decode_lazy(bytes)
                v:set('name', 'mallory')
                local _ = v:encode()
            end,
            n)
    end
end