Repository navigation
Conversation
Most embedded uses compile a script once and run it many times, so the fixed cost of a single Run matters as much as loop throughput. ShortScript builds a small map, indexes it and formats a string, finishing in a few microseconds; it exposes per-Run overheads that the loop-heavy benchmarks hide.
The dispatch loop read and wrote ip, sp, the current instruction slice,
the current frame and the alloc counter through the *VM pointer on every
instruction. They now live in locals for the whole run and are written
back to the VM once when the loop exits (error, OpSuspend or abort),
which is all the error reporting in Run needs. Callees are matched with
a type switch so compiled, builtin and user functions skip the
CanCall/Call interface calls; argument windows are moved with copy
instead of element loops; iterator opcodes replace the stack top in
place instead of pop-then-push.
Measured on its own this is neutral. The Go register allocator spills
the locals to frame slots at the loop header before the jump-table
dispatch (eight stores per instruction), so the memory traffic moved
from *VM fields to the stack frame rather than disappearing. Fib gains
5.5%; ArrayIndex loses 20% without executing any changed code, which is
code-layout sensitivity of this large function, not a cost of the change
(see the hardware-counter comparison in the PR description). The
restructuring is kept because the following commits are written against
it: the single exit point, the in-loop fast paths and the per-VM
allocators all rely on the state being local.
Benchmarks: benchstat over 6 runs, previous commit vs this one
(AMD Ryzen 7 3700X, go1.27.1):
goos: linux
goarch: amd64
pkg: github.com/d5/tengo/v2
cpu: AMD Ryzen 7 3700X 8-Core Processor
│ 0-baseline.txt │ 1-locals.txt │
│ sec/op │ sec/op vs base │
Compile-16 37.51m ± 3% 36.79m ± 2% ~ (p=0.240 n=6)
VM/ArithLoop-16 33.77m ± 1% 33.80m ± 1% ~ (p=0.485 n=6)
VM/FloatLoop-16 50.91m ± 1% 51.08m ± 1% ~ (p=0.093 n=6)
VM/CompareLoop-16 31.85m ± 0% 32.34m ± 0% +1.55% (p=0.002 n=6)
VM/Fib-16 3.678m ± 0% 3.474m ± 1% -5.55% (p=0.002 n=6)
VM/ClosureCalls-16 20.55m ± 1% 21.06m ± 1% +2.44% (p=0.002 n=6)
VM/MapAccess-16 43.95m ± 0% 45.29m ± 1% +3.05% (p=0.002 n=6)
VM/MethodCalls-16 43.40m ± 0% 42.95m ± 1% -1.03% (p=0.002 n=6)
VM/ArrayIter-16 10.83m ± 0% 10.74m ± 1% -0.87% (p=0.009 n=6)
VM/ArrayIndex-16 26.32m ± 1% 31.49m ± 2% +19.67% (p=0.002 n=6)
VM/StringOps-16 11.98m ± 1% 12.42m ± 0% +3.67% (p=0.002 n=6)
VM/Builtins-16 32.08m ± 1% 32.81m ± 1% +2.30% (p=0.002 n=6)
VM/StdlibCalls-16 14.41m ± 3% 13.63m ± 1% -5.41% (p=0.002 n=6)
VM/MapIter-16 24.17m ± 1% 24.84m ± 0% +2.76% (p=0.002 n=6)
VM/ShortScript-16 12.56µ ± 15% 13.51µ ± 8% ~ (p=0.065 n=6)
geomean 13.91m 14.16m +1.76%
│ 0-baseline.txt │ 1-locals.txt │
│ B/op │ B/op vs base │
Compile-16 20.09Mi ± 0% 20.08Mi ± 0% ~ (p=0.258 n=6)
VM/ArithLoop-16 9.226Mi ± 0% 9.226Mi ± 0% ~ (p=0.160 n=6)
VM/FloatLoop-16 16.86Mi ± 0% 16.86Mi ± 0% ~ (p=0.253 n=6)
VM/CompareLoop-16 3.507Mi ± 0% 3.507Mi ± 0% ~ (p=0.870 n=6)
VM/Fib-16 88.16Ki ± 0% 88.16Ki ± 0% ~ (p=1.000 n=6)
VM/ClosureCalls-16 3.122Mi ± 0% 3.122Mi ± 0% ~ (p=0.156 n=6)
VM/MapAccess-16 6.166Mi ± 0% 6.166Mi ± 0% ~ (p=0.669 n=6)
VM/MethodCalls-16 3.122Mi ± 0% 3.122Mi ± 0% ~ (p=0.506 n=6)
VM/ArrayIter-16 1.677Mi ± 0% 1.677Mi ± 0% ~ (p=0.139 n=6)
VM/ArrayIndex-16 3.186Mi ± 0% 3.186Mi ± 0% +0.00% (p=0.050 n=6)
VM/StringOps-16 5.445Mi ± 0% 5.445Mi ± 0% ~ (p=0.266 n=6)
VM/Builtins-16 3.122Mi ± 0% 3.122Mi ± 0% ~ (p=0.965 n=6)
VM/StdlibCalls-16 3.893Mi ± 0% 3.511Mi ± 0% -9.80% (p=0.002 n=6)
VM/MapIter-16 14.40Mi ± 0% 14.40Mi ± 0% ~ (p=0.623 n=6)
VM/ShortScript-16 88.56Ki ± 0% 88.56Ki ± 0% ~ (p=1.000 n=6) ¹
geomean 3.113Mi 3.092Mi -0.69%
¹ all samples are equal
│ 0-baseline.txt │ 1-locals.txt │
│ allocs/op │ allocs/op vs base │
Compile-16 273.2k ± 0% 273.2k ± 0% ~ (p=0.892 n=6)
VM/ArithLoop-16 1.198M ± 0% 1.198M ± 0% ~ (p=1.000 n=6) ¹
VM/FloatLoop-16 2.199M ± 0% 2.199M ± 0% ~ (p=1.000 n=6) ¹
VM/CompareLoop-16 448.5k ± 0% 448.5k ± 0% ~ (p=1.000 n=6) ¹
VM/Fib-16 21.00 ± 0% 21.00 ± 0% ~ (p=1.000 n=6) ¹
VM/ClosureCalls-16 398.0k ± 0% 398.0k ± 0% ~ (p=1.000 n=6) ¹
VM/MapAccess-16 796.9k ± 0% 796.9k ± 0% ~ (p=1.000 n=6) ¹
VM/MethodCalls-16 398.0k ± 0% 398.0k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIter-16 201.2k ± 0% 201.2k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIndex-16 399.9k ± 0% 399.9k ± 0% ~ (p=1.000 n=6) ¹
VM/StringOps-16 203.0k ± 0% 203.0k ± 0% ~ (p=1.000 n=6) ¹
VM/Builtins-16 398.0k ± 0% 398.0k ± 0% ~ (p=1.000 n=6) ¹
VM/StdlibCalls-16 299.0k ± 0% 249.0k ± 0% -16.72% (p=0.002 n=6)
VM/MapIter-16 411.1k ± 0% 411.1k ± 0% ~ (p=1.000 n=6)
VM/ShortScript-16 12.00 ± 0% 12.00 ± 0% ~ (p=1.000 n=6) ¹
geomean 115.1k 113.7k -1.21%
¹ all samples are equal
…l tests
Arithmetic and comparisons on two Ints or two Floats are handled in the
loop without the BinaryOp interface call and the operand type switch
behind it. A comparison immediately followed by OpJumpFalsy (every loop
condition and most ifs) jumps directly without materializing a Bool on
the stack. Falsiness tests read the Bool field directly, and == / != on
two Ints compare values inline. Division and remainder keep their
previous behaviour for a zero divisor by falling back to BinaryOp.
Measured on its own this is within layout noise (geomean -1.2%). The
interface call and type switch it removes are a small part of the ~75
native instructions per bytecode op; allocation dominates, and this
commit still allocates every result. FloatLoop is 7.8% slower here
because Float.BinaryOp returned the left operand unchanged when the
result equalled it, saving an allocation that the fast path does not
save; the next commit makes the allocation itself cheap. The fast paths
are where the slab allocator of the next commit plugs in, which is where
the measured gain appears.
Benchmarks: benchstat over 6 runs, previous commit vs this one
(AMD Ryzen 7 3700X, go1.27.1):
goos: linux
goarch: amd64
pkg: github.com/d5/tengo/v2
cpu: AMD Ryzen 7 3700X 8-Core Processor
│ 1-locals.txt │ 2-fast.txt │
│ sec/op │ sec/op vs base │
Compile-16 36.79m ± 2% 37.57m ± 1% +2.10% (p=0.041 n=6)
VM/ArithLoop-16 33.80m ± 1% 35.10m ± 0% +3.85% (p=0.002 n=6)
VM/FloatLoop-16 51.08m ± 1% 55.05m ± 1% +7.76% (p=0.002 n=6)
VM/CompareLoop-16 32.34m ± 0% 32.73m ± 0% +1.20% (p=0.002 n=6)
VM/Fib-16 3.474m ± 1% 3.475m ± 1% ~ (p=0.818 n=6)
VM/ClosureCalls-16 21.06m ± 1% 19.76m ± 0% -6.17% (p=0.002 n=6)
VM/MapAccess-16 45.29m ± 1% 46.54m ± 1% +2.77% (p=0.002 n=6)
VM/MethodCalls-16 42.95m ± 1% 44.85m ± 0% +4.44% (p=0.002 n=6)
VM/ArrayIter-16 10.74m ± 1% 10.95m ± 1% +1.97% (p=0.002 n=6)
VM/ArrayIndex-16 31.49m ± 2% 28.44m ± 1% -9.70% (p=0.002 n=6)
VM/StringOps-16 12.42m ± 0% 11.99m ± 1% -3.48% (p=0.002 n=6)
VM/Builtins-16 32.81m ± 1% 31.84m ± 0% -2.97% (p=0.002 n=6)
VM/StdlibCalls-16 13.63m ± 1% 12.67m ± 3% -7.07% (p=0.002 n=6)
VM/MapIter-16 24.84m ± 0% 24.67m ± 2% -0.69% (p=0.041 n=6)
VM/ShortScript-16 13.51µ ± 8% 12.12µ ± 10% -10.30% (p=0.015 n=6)
geomean 14.16m 13.99m -1.22%
│ 1-locals.txt │ 2-fast.txt │
│ B/op │ B/op vs base │
Compile-16 20.08Mi ± 0% 20.08Mi ± 0% ~ (p=0.732 n=6)
VM/ArithLoop-16 9.226Mi ± 0% 9.226Mi ± 0% ~ (p=0.673 n=6)
VM/FloatLoop-16 16.86Mi ± 0% 16.86Mi ± 0% +0.00% (p=0.002 n=6)
VM/CompareLoop-16 3.507Mi ± 0% 3.507Mi ± 0% ~ (p=0.201 n=6)
VM/Fib-16 88.16Ki ± 0% 88.16Ki ± 0% ~ (p=1.000 n=6)
VM/ClosureCalls-16 3.122Mi ± 0% 3.122Mi ± 0% ~ (p=0.452 n=6)
VM/MapAccess-16 6.166Mi ± 0% 6.166Mi ± 0% -0.00% (p=0.004 n=6)
VM/MethodCalls-16 3.122Mi ± 0% 3.122Mi ± 0% ~ (p=0.292 n=6)
VM/ArrayIter-16 1.677Mi ± 0% 1.677Mi ± 0% ~ (p=0.058 n=6)
VM/ArrayIndex-16 3.186Mi ± 0% 3.186Mi ± 0% ~ (p=0.139 n=6)
VM/StringOps-16 5.445Mi ± 0% 5.445Mi ± 0% ~ (p=0.095 n=6)
VM/Builtins-16 3.122Mi ± 0% 3.122Mi ± 0% ~ (p=0.907 n=6)
VM/StdlibCalls-16 3.511Mi ± 0% 3.511Mi ± 0% ~ (p=0.870 n=6)
VM/MapIter-16 14.40Mi ± 0% 14.40Mi ± 0% ~ (p=0.535 n=6)
VM/ShortScript-16 88.56Ki ± 0% 88.56Ki ± 0% ~ (p=1.000 n=6) ¹
geomean 3.092Mi 3.092Mi -0.00%
¹ all samples are equal
│ 1-locals.txt │ 2-fast.txt │
│ allocs/op │ allocs/op vs base │
Compile-16 273.2k ± 0% 273.2k ± 0% ~ (p=1.000 n=6)
VM/ArithLoop-16 1.198M ± 0% 1.198M ± 0% ~ (p=1.000 n=6) ¹
VM/FloatLoop-16 2.199M ± 0% 2.199M ± 0% +0.00% (p=0.002 n=6)
VM/CompareLoop-16 448.5k ± 0% 448.5k ± 0% ~ (p=1.000 n=6) ¹
VM/Fib-16 21.00 ± 0% 21.00 ± 0% ~ (p=1.000 n=6) ¹
VM/ClosureCalls-16 398.0k ± 0% 398.0k ± 0% ~ (p=1.000 n=6) ¹
VM/MapAccess-16 796.9k ± 0% 796.9k ± 0% ~ (p=1.000 n=6) ¹
VM/MethodCalls-16 398.0k ± 0% 398.0k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIter-16 201.2k ± 0% 201.2k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIndex-16 399.9k ± 0% 399.9k ± 0% ~ (p=1.000 n=6) ¹
VM/StringOps-16 203.0k ± 0% 203.0k ± 0% ~ (p=1.000 n=6) ¹
VM/Builtins-16 398.0k ± 0% 398.0k ± 0% ~ (p=1.000 n=6) ¹
VM/StdlibCalls-16 249.0k ± 0% 249.0k ± 0% ~ (p=1.000 n=6) ¹
VM/MapIter-16 411.1k ± 0% 411.1k ± 0% ~ (p=1.000 n=6)
VM/ShortScript-16 12.00 ± 0% 12.00 ± 0% ~ (p=1.000 n=6) ¹
geomean 113.7k 113.7k +0.00%
¹ all samples are equal
After the previous change, allocation was the largest remaining cost in
numeric code: every Int above the small-int table and every Float is a
separate 8-byte heap object, and mallocgc plus GC accounting cost far
more than the arithmetic itself. The arithmetic fast paths now carve Ints
and Floats out of 32-element slabs owned by the VM, so one malloc serves
32 results.
Trade-off: a slab stays alive while any number in it is referenced, so a
script that keeps one result out of every 32 it computes retains 256
bytes instead of 8 for it. The alloc-limit accounting is unchanged; it
still counts objects.
Benchmarks: benchstat over 6 runs, previous commit vs this one
(AMD Ryzen 7 3700X, go1.27.1):
goos: linux
goarch: amd64
pkg: github.com/d5/tengo/v2
cpu: AMD Ryzen 7 3700X 8-Core Processor
│ 2-fast.txt │ 3-slab.txt │
│ sec/op │ sec/op vs base │
Compile-16 37.57m ± 1% 37.02m ± 3% ~ (p=0.132 n=6)
VM/ArithLoop-16 35.10m ± 0% 29.48m ± 0% -16.03% (p=0.002 n=6)
VM/FloatLoop-16 55.05m ± 1% 42.75m ± 2% -22.34% (p=0.002 n=6)
VM/CompareLoop-16 32.73m ± 0% 32.52m ± 0% -0.62% (p=0.002 n=6)
VM/Fib-16 3.475m ± 1% 3.758m ± 0% +8.14% (p=0.002 n=6)
VM/ClosureCalls-16 19.76m ± 0% 21.11m ± 0% +6.86% (p=0.002 n=6)
VM/MapAccess-16 46.54m ± 1% 40.85m ± 1% -12.24% (p=0.002 n=6)
VM/MethodCalls-16 44.85m ± 0% 40.62m ± 0% -9.43% (p=0.002 n=6)
VM/ArrayIter-16 10.950m ± 1% 9.319m ± 1% -14.90% (p=0.002 n=6)
VM/ArrayIndex-16 28.44m ± 1% 26.22m ± 2% -7.80% (p=0.002 n=6)
VM/StringOps-16 11.99m ± 1% 12.15m ± 0% +1.39% (p=0.002 n=6)
VM/Builtins-16 31.84m ± 0% 30.23m ± 1% -5.04% (p=0.002 n=6)
VM/StdlibCalls-16 12.67m ± 3% 12.76m ± 0% ~ (p=0.065 n=6)
VM/MapIter-16 24.67m ± 2% 23.37m ± 0% -5.27% (p=0.002 n=6)
VM/ShortScript-16 12.12µ ± 10% 12.39µ ± 5% ~ (p=0.485 n=6)
geomean 13.99m 13.23m -5.44%
│ 2-fast.txt │ 3-slab.txt │
│ B/op │ B/op vs base │
Compile-16 20.08Mi ± 0% 20.09Mi ± 0% ~ (p=0.699 n=6)
VM/ArithLoop-16 9.226Mi ± 0% 9.226Mi ± 0% +0.00% (p=0.002 n=6)
VM/FloatLoop-16 16.86Mi ± 0% 16.86Mi ± 0% +0.00% (p=0.002 n=6)
VM/CompareLoop-16 3.507Mi ± 0% 3.508Mi ± 0% +0.01% (p=0.002 n=6)
VM/Fib-16 88.16Ki ± 0% 88.25Ki ± 0% +0.11% (p=0.002 n=6)
VM/ClosureCalls-16 3.122Mi ± 0% 3.122Mi ± 0% +0.01% (p=0.002 n=6)
VM/MapAccess-16 6.166Mi ± 0% 6.166Mi ± 0% +0.00% (p=0.013 n=6)
VM/MethodCalls-16 3.122Mi ± 0% 3.123Mi ± 0% +0.01% (p=0.002 n=6)
VM/ArrayIter-16 1.677Mi ± 0% 1.677Mi ± 0% +0.01% (p=0.002 n=6)
VM/ArrayIndex-16 3.186Mi ± 0% 3.186Mi ± 0% +0.00% (p=0.002 n=6)
VM/StringOps-16 5.445Mi ± 0% 5.258Mi ± 0% -3.43% (p=0.002 n=6)
VM/Builtins-16 3.122Mi ± 0% 3.122Mi ± 0% +0.01% (p=0.002 n=6)
VM/StdlibCalls-16 3.511Mi ± 0% 3.512Mi ± 0% +0.01% (p=0.002 n=6)
VM/MapIter-16 14.40Mi ± 0% 14.40Mi ± 0% +0.00% (p=0.002 n=6)
VM/ShortScript-16 88.56Ki ± 0% 88.56Ki ± 0% ~ (p=1.000 n=6) ¹
geomean 3.092Mi 3.085Mi -0.22%
¹ all samples are equal
│ 2-fast.txt │ 3-slab.txt │
│ allocs/op │ allocs/op vs base │
Compile-16 273.2k ± 0% 273.2k ± 0% ~ (p=1.000 n=6)
VM/ArithLoop-16 1197.95k ± 0% 37.44k ± 0% -96.87% (p=0.002 n=6)
VM/FloatLoop-16 2198.98k ± 0% 68.72k ± 0% -96.87% (p=0.002 n=6)
VM/CompareLoop-16 448.45k ± 0% 14.02k ± 0% -96.87% (p=0.002 n=6)
VM/Fib-16 21.000 ± 0% 2.000 ± 0% -90.48% (p=0.002 n=6)
VM/ClosureCalls-16 397.96k ± 0% 12.44k ± 0% -96.87% (p=0.002 n=6)
VM/MapAccess-16 796.90k ± 0% 24.91k ± 0% -96.87% (p=0.002 n=6)
VM/MethodCalls-16 397.96k ± 0% 12.44k ± 0% -96.87% (p=0.002 n=6)
VM/ArrayIter-16 201.168k ± 0% 7.462k ± 0% -96.29% (p=0.002 n=6)
VM/ArrayIndex-16 399.94k ± 0% 13.48k ± 0% -96.63% (p=0.002 n=6)
VM/StringOps-16 203.0k ± 0% 151.7k ± 0% -25.28% (p=0.002 n=6)
VM/Builtins-16 397.96k ± 0% 12.44k ± 0% -96.87% (p=0.002 n=6)
VM/StdlibCalls-16 249.0k ± 0% 153.1k ± 0% -38.51% (p=0.002 n=6)
VM/MapIter-16 411.1k ± 0% 214.5k ± 0% -47.82% (p=0.002 n=6)
VM/ShortScript-16 12.00 ± 0% 12.00 ± 0% ~ (p=1.000 n=6) ¹
geomean 113.7k 11.23k -90.12%
¹ all samples are equal
- indexed assignment (OpSetSel*) read its selectors into a fresh slice on
every store; they are now read in place from the operand stack, which
nothing writes to until indexAssign has returned
- OpArray grew its element slice with append; it is presized and copied
- OpMap sized its map for twice the number of entries
- Array/ImmutableArray Copy and String presize their slices; Map copies
presize the map
- Bytes indexing, bytes/string iterator keys and time subtraction use the
shared small-int table instead of allocating
Benchmarks: benchstat over 6 runs, previous commit vs this one
(AMD Ryzen 7 3700X, go1.27.1):
goos: linux
goarch: amd64
pkg: github.com/d5/tengo/v2
cpu: AMD Ryzen 7 3700X 8-Core Processor
│ 3-slab.txt │ 4-alloc.txt │
│ sec/op │ sec/op vs base │
Compile-16 37.02m ± 3% 36.39m ± 5% ~ (p=0.394 n=6)
VM/ArithLoop-16 29.48m ± 0% 37.16m ± 0% +26.07% (p=0.002 n=6)
VM/FloatLoop-16 42.75m ± 2% 49.78m ± 0% +16.44% (p=0.002 n=6)
VM/CompareLoop-16 32.52m ± 0% 35.54m ± 1% +9.30% (p=0.002 n=6)
VM/Fib-16 3.758m ± 0% 3.579m ± 0% -4.76% (p=0.002 n=6)
VM/ClosureCalls-16 21.11m ± 0% 19.98m ± 0% -5.39% (p=0.002 n=6)
VM/MapAccess-16 40.85m ± 1% 44.90m ± 1% +9.93% (p=0.002 n=6)
VM/MethodCalls-16 40.62m ± 0% 41.35m ± 0% +1.78% (p=0.002 n=6)
VM/ArrayIter-16 9.319m ± 1% 9.726m ± 0% +4.36% (p=0.002 n=6)
VM/ArrayIndex-16 26.22m ± 2% 33.49m ± 0% +27.74% (p=0.002 n=6)
VM/StringOps-16 12.15m ± 0% 12.52m ± 0% +3.04% (p=0.002 n=6)
VM/Builtins-16 30.23m ± 1% 34.23m ± 1% +13.24% (p=0.002 n=6)
VM/StdlibCalls-16 12.76m ± 0% 13.15m ± 0% +3.12% (p=0.002 n=6)
VM/MapIter-16 23.37m ± 0% 22.72m ± 0% -2.78% (p=0.002 n=6)
VM/ShortScript-16 12.39µ ± 5% 12.33µ ± 2% ~ (p=0.554 n=6)
geomean 13.23m 14.05m +6.20%
│ 3-slab.txt │ 4-alloc.txt │
│ B/op │ B/op vs base │
Compile-16 20.09Mi ± 0% 20.09Mi ± 0% ~ (p=0.818 n=6)
VM/ArithLoop-16 9.226Mi ± 0% 9.226Mi ± 0% ~ (p=0.264 n=6)
VM/FloatLoop-16 16.86Mi ± 0% 16.86Mi ± 0% ~ (p=0.056 n=6)
VM/CompareLoop-16 3.508Mi ± 0% 3.508Mi ± 0% ~ (p=1.000 n=6)
VM/Fib-16 88.25Ki ± 0% 88.25Ki ± 0% ~ (p=1.000 n=6) ¹
VM/ClosureCalls-16 3.122Mi ± 0% 3.122Mi ± 0% +0.00% (p=0.006 n=6)
VM/MapAccess-16 6.166Mi ± 0% 6.166Mi ± 0% ~ (p=0.242 n=6)
VM/MethodCalls-16 3.123Mi ± 0% 3.123Mi ± 0% ~ (p=0.727 n=6)
VM/ArrayIter-16 1.677Mi ± 0% 1.677Mi ± 0% ~ (p=0.165 n=6)
VM/ArrayIndex-16 3.186Mi ± 0% 3.186Mi ± 0% ~ (p=0.361 n=6)
VM/StringOps-16 5.258Mi ± 0% 5.258Mi ± 0% ~ (p=0.781 n=6)
VM/Builtins-16 3.122Mi ± 0% 3.122Mi ± 0% -0.00% (p=0.002 n=6)
VM/StdlibCalls-16 3.512Mi ± 0% 3.512Mi ± 0% ~ (p=0.167 n=6)
VM/MapIter-16 14.40Mi ± 0% 14.40Mi ± 0% ~ (p=0.084 n=6)
VM/ShortScript-16 88.56Ki ± 0% 88.55Ki ± 0% -0.02% (p=0.002 n=6)
geomean 3.085Mi 3.085Mi -0.00%
¹ all samples are equal
│ 3-slab.txt │ 4-alloc.txt │
│ allocs/op │ allocs/op vs base │
Compile-16 273.2k ± 0% 273.2k ± 0% ~ (p=1.000 n=6)
VM/ArithLoop-16 37.44k ± 0% 37.44k ± 0% ~ (p=1.000 n=6) ¹
VM/FloatLoop-16 68.72k ± 0% 68.72k ± 0% ~ (p=1.000 n=6) ¹
VM/CompareLoop-16 14.02k ± 0% 14.02k ± 0% ~ (p=1.000 n=6) ¹
VM/Fib-16 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=6) ¹
VM/ClosureCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/MapAccess-16 24.91k ± 0% 24.91k ± 0% ~ (p=1.000 n=6) ¹
VM/MethodCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIter-16 7.462k ± 0% 7.462k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIndex-16 13.48k ± 0% 13.48k ± 0% ~ (p=1.000 n=6) ¹
VM/StringOps-16 151.7k ± 0% 151.7k ± 0% ~ (p=1.000 n=6) ¹
VM/Builtins-16 12.44k ± 0% 12.44k ± 0% -0.02% (p=0.002 n=6)
VM/StdlibCalls-16 153.1k ± 0% 153.1k ± 0% ~ (p=1.000 n=6) ¹
VM/MapIter-16 214.5k ± 0% 214.5k ± 0% ~ (p=1.000 n=6) ¹
VM/ShortScript-16 12.00 ± 0% 11.00 ± 0% -8.33% (p=0.002 n=6)
geomean 11.23k 11.16k -0.58%
¹ all samples are equal
Compiled.Run and RunContext allocated a fresh VM per call. A VM is about
80KB (2048 operand-stack slots and 1024 frames), so every run of even a
trivial script paid for a large zeroed allocation and the GC work to free
it. VMs now come from a sync.Pool; before a VM is returned, its stack and
the frames it used are cleared so a pooled VM does not keep the last
run's objects alive. NewVM is unchanged for callers that drive a VM
directly.
Benchmarks: benchstat over 6 runs, previous commit vs this one
(AMD Ryzen 7 3700X, go1.27.1):
goos: linux
goarch: amd64
pkg: github.com/d5/tengo/v2
cpu: AMD Ryzen 7 3700X 8-Core Processor
│ 4-alloc.txt │ 5-pool.txt │
│ sec/op │ sec/op vs base │
Compile-16 36.39m ± 5% 37.33m ± 3% ~ (p=0.180 n=6)
VM/ArithLoop-16 37.16m ± 0% 31.77m ± 0% -14.51% (p=0.002 n=6)
VM/FloatLoop-16 49.78m ± 0% 45.31m ± 0% -8.97% (p=0.002 n=6)
VM/CompareLoop-16 35.54m ± 1% 33.42m ± 0% -5.98% (p=0.002 n=6)
VM/Fib-16 3.579m ± 0% 3.610m ± 1% +0.86% (p=0.009 n=6)
VM/ClosureCalls-16 19.98m ± 0% 18.39m ± 1% -7.96% (p=0.002 n=6)
VM/MapAccess-16 44.90m ± 1% 43.78m ± 0% -2.51% (p=0.002 n=6)
VM/MethodCalls-16 41.35m ± 0% 42.45m ± 0% +2.67% (p=0.002 n=6)
VM/ArrayIter-16 9.726m ± 0% 10.647m ± 1% +9.47% (p=0.002 n=6)
VM/ArrayIndex-16 33.49m ± 0% 27.66m ± 0% -17.42% (p=0.002 n=6)
VM/StringOps-16 12.52m ± 0% 11.53m ± 0% -7.88% (p=0.002 n=6)
VM/Builtins-16 34.23m ± 1% 30.79m ± 1% -10.06% (p=0.002 n=6)
VM/StdlibCalls-16 13.15m ± 0% 13.84m ± 1% +5.20% (p=0.002 n=6)
VM/MapIter-16 22.72m ± 0% 23.14m ± 0% +1.87% (p=0.002 n=6)
VM/ShortScript-16 12327.0n ± 2% 967.5n ± 1% -92.15% (p=0.002 n=6)
geomean 14.05m 11.40m -18.81%
│ 4-alloc.txt │ 5-pool.txt │
│ B/op │ B/op vs base │
Compile-16 20.09Mi ± 0% 20.09Mi ± 0% ~ (p=0.855 n=6)
VM/ArithLoop-16 9.226Mi ± 0% 9.144Mi ± 0% -0.88% (p=0.002 n=6)
VM/FloatLoop-16 16.86Mi ± 0% 16.78Mi ± 0% -0.48% (p=0.002 n=6)
VM/CompareLoop-16 3.508Mi ± 0% 3.426Mi ± 0% -2.32% (p=0.002 n=6)
VM/Fib-16 90368.0 ± 0% 438.0 ± 62% -99.52% (p=0.002 n=6)
VM/ClosureCalls-16 3.122Mi ± 0% 3.040Mi ± 0% -2.65% (p=0.002 n=6)
VM/MapAccess-16 6.166Mi ± 0% 6.086Mi ± 0% -1.30% (p=0.002 n=6)
VM/MethodCalls-16 3.123Mi ± 0% 3.042Mi ± 0% -2.59% (p=0.002 n=6)
VM/ArrayIter-16 1.677Mi ± 0% 1.593Mi ± 0% -5.01% (p=0.002 n=6)
VM/ArrayIndex-16 3.186Mi ± 0% 3.104Mi ± 0% -2.56% (p=0.002 n=6)
VM/StringOps-16 5.258Mi ± 0% 5.176Mi ± 0% -1.58% (p=0.002 n=6)
VM/Builtins-16 3.122Mi ± 0% 3.041Mi ± 0% -2.61% (p=0.002 n=6)
VM/StdlibCalls-16 3.512Mi ± 0% 3.429Mi ± 0% -2.36% (p=0.002 n=6)
VM/MapIter-16 14.40Mi ± 0% 14.32Mi ± 0% -0.57% (p=0.002 n=6)
VM/ShortScript-16 90672.0 ± 0% 560.0 ± 0% -99.38% (p=0.002 n=6)
geomean 3.085Mi 1.515Mi -50.90%
│ 4-alloc.txt │ 5-pool.txt │
│ allocs/op │ allocs/op vs base │
Compile-16 273.2k ± 0% 273.2k ± 0% ~ (p=1.000 n=6)
VM/ArithLoop-16 37.44k ± 0% 37.44k ± 0% ~ (p=1.000 n=6) ¹
VM/FloatLoop-16 68.72k ± 0% 68.72k ± 0% ~ (p=1.000 n=6) ¹
VM/CompareLoop-16 14.02k ± 0% 14.02k ± 0% ~ (p=1.000 n=6) ¹
VM/Fib-16 2.000 ± 0% 0.000 ± 0% -100.00% (p=0.002 n=6)
VM/ClosureCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/MapAccess-16 24.91k ± 0% 24.91k ± 0% +0.00% (p=0.002 n=6)
VM/MethodCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIter-16 7.462k ± 0% 7.461k ± 0% -0.01% (p=0.002 n=6)
VM/ArrayIndex-16 13.48k ± 0% 13.48k ± 0% ~ (p=1.000 n=6) ¹
VM/StringOps-16 151.7k ± 0% 151.7k ± 0% ~ (p=1.000 n=6) ¹
VM/Builtins-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/StdlibCalls-16 153.1k ± 0% 153.1k ± 0% ~ (p=1.000 n=6) ¹
VM/MapIter-16 214.5k ± 0% 214.5k ± 0% +0.00% (p=0.002 n=6)
VM/ShortScript-16 11.00 ± 0% 10.00 ± 0% -9.09% (p=0.002 n=6)
geomean 11.16k ? ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean
Deep non-tail recursion could overflow the 2048-slot operand stack long
before reaching MaxFrames, and the result was a Go index-out-of-range
panic in the host process rather than ErrStackOverflow:
f := func(n) {
a := 1; b := 2; c := 3; d := 4
if n == 0 { return 0 }
return f(n-1) + a + b + c + d
}
f(2000)
The compiler now records, per function, the operand-stack depth its body
needs above its locals (maxStackDepth: every opcode has a fixed stack
effect and compiler output is structured, so a linear pass is exact). A
call is rejected with ErrStackOverflow when base + locals + depth would
exceed StackSize, the main function is checked once in Run, and spread
calls, whose expansion is only known at run time, are checked when the
array is expanded. Bytecode decoded from gob recomputes the depth since
the field is not serialized. CompiledFunction.Copy also now preserves
SourceMap.
Not a performance change; the numbers are included to show the check is
free.
Benchmarks: benchstat over 6 runs, previous commit vs this one
(AMD Ryzen 7 3700X, go1.27.1):
goos: linux
goarch: amd64
pkg: github.com/d5/tengo/v2
cpu: AMD Ryzen 7 3700X 8-Core Processor
│ 5-pool.txt │ 6-depth.txt │
│ sec/op │ sec/op vs base │
Compile-16 37.33m ± 3% 38.12m ± 7% ~ (p=0.310 n=6)
VM/ArithLoop-16 31.77m ± 0% 32.01m ± 0% +0.76% (p=0.002 n=6)
VM/FloatLoop-16 45.31m ± 0% 42.79m ± 1% -5.55% (p=0.002 n=6)
VM/CompareLoop-16 33.42m ± 0% 32.57m ± 1% -2.55% (p=0.002 n=6)
VM/Fib-16 3.610m ± 1% 3.644m ± 0% +0.94% (p=0.002 n=6)
VM/ClosureCalls-16 18.39m ± 1% 18.57m ± 1% +0.99% (p=0.002 n=6)
VM/MapAccess-16 43.78m ± 0% 41.82m ± 0% -4.47% (p=0.002 n=6)
VM/MethodCalls-16 42.45m ± 0% 39.17m ± 0% -7.72% (p=0.002 n=6)
VM/ArrayIter-16 10.647m ± 1% 9.801m ± 0% -7.95% (p=0.002 n=6)
VM/ArrayIndex-16 27.66m ± 0% 27.44m ± 0% -0.79% (p=0.002 n=6)
VM/StringOps-16 11.53m ± 0% 12.29m ± 0% +6.57% (p=0.002 n=6)
VM/Builtins-16 30.79m ± 1% 30.30m ± 0% -1.59% (p=0.002 n=6)
VM/StdlibCalls-16 13.84m ± 1% 12.83m ± 2% -7.31% (p=0.002 n=6)
VM/MapIter-16 23.14m ± 0% 22.57m ± 0% -2.47% (p=0.002 n=6)
VM/ShortScript-16 967.5n ± 1% 952.1n ± 0% -1.59% (p=0.002 n=6)
geomean 11.40m 11.16m -2.12%
│ 5-pool.txt │ 6-depth.txt │
│ B/op │ B/op vs base │
Compile-16 20.09Mi ± 0% 20.10Mi ± 0% +0.08% (p=0.002 n=6)
VM/ArithLoop-16 9.144Mi ± 0% 9.144Mi ± 0% ~ (p=0.323 n=6)
VM/FloatLoop-16 16.78Mi ± 0% 16.78Mi ± 0% -0.00% (p=0.002 n=6)
VM/CompareLoop-16 3.426Mi ± 0% 3.426Mi ± 0% -0.01% (p=0.015 n=6)
VM/Fib-16 438.0 ± 62% 441.0 ± 62% ~ (p=0.113 n=6)
VM/ClosureCalls-16 3.040Mi ± 0% 3.040Mi ± 0% ~ (p=0.448 n=6)
VM/MapAccess-16 6.086Mi ± 0% 6.086Mi ± 0% ~ (p=0.383 n=6)
VM/MethodCalls-16 3.042Mi ± 0% 3.042Mi ± 0% -0.00% (p=0.002 n=6)
VM/ArrayIter-16 1.593Mi ± 0% 1.593Mi ± 0% -0.01% (p=0.002 n=6)
VM/ArrayIndex-16 3.104Mi ± 0% 3.104Mi ± 0% ~ (p=0.974 n=6)
VM/StringOps-16 5.176Mi ± 0% 5.176Mi ± 0% +0.00% (p=0.041 n=6)
VM/Builtins-16 3.041Mi ± 0% 3.040Mi ± 0% ~ (p=1.000 n=6)
VM/StdlibCalls-16 3.429Mi ± 0% 3.429Mi ± 0% ~ (p=0.058 n=6)
VM/MapIter-16 14.32Mi ± 0% 14.32Mi ± 0% -0.00% (p=0.045 n=6)
VM/ShortScript-16 560.0 ± 0% 560.0 ± 0% ~ (p=0.455 n=6)
geomean 1.515Mi 1.515Mi +0.05%
│ 5-pool.txt │ 6-depth.txt │
│ allocs/op │ allocs/op vs base │
Compile-16 273.2k ± 0% 273.2k ± 0% ~ (p=0.697 n=6)
VM/ArithLoop-16 37.44k ± 0% 37.44k ± 0% ~ (p=1.000 n=6) ¹
VM/FloatLoop-16 68.72k ± 0% 68.72k ± 0% ~ (p=1.000 n=6) ¹
VM/CompareLoop-16 14.02k ± 0% 14.02k ± 0% ~ (p=1.000 n=6) ¹
VM/Fib-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹
VM/ClosureCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/MapAccess-16 24.91k ± 0% 24.91k ± 0% ~ (p=1.000 n=6) ¹
VM/MethodCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIter-16 7.461k ± 0% 7.461k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIndex-16 13.48k ± 0% 13.48k ± 0% ~ (p=1.000 n=6) ¹
VM/StringOps-16 151.7k ± 0% 151.7k ± 0% ~ (p=1.000 n=6) ¹
VM/Builtins-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/StdlibCalls-16 153.1k ± 0% 153.1k ± 0% ~ (p=1.000 n=6) ¹
VM/MapIter-16 214.5k ± 0% 214.5k ± 0% ~ (p=1.000 n=6) ¹
VM/ShortScript-16 10.00 ± 0% 10.00 ± 0% ~ (p=1.000 n=6) ¹
geomean ² +0.00% ²
¹ all samples are equal
² summaries must be >0 to compute geomean
xnacly
force-pushed
the
perf-improvements
branch
from
October 4, 2026 11:03
1093ee5 to
0584ab9
Compare
Int / Int and Int % Int with a zero divisor executed a Go integer
division by zero, which panics. Compiled.Run does not recover, so a
script could crash the host process:
x := 0
y := 1 / x
Both operators now return ErrDivisionByZero, which the VM reports as an
ordinary runtime error with position information. Float division is
unchanged (IEEE infinity). The VM's int fast path already fell back to
BinaryOp for a zero divisor, so it inherits the check.
The tail-call optimization reused the frame both for `return f(x)` and
for `f(x)` followed by an implicit return. In the second form the
function's own result is undefined by the language rules, but the
optimization returned whatever the final callee returned:
f := func(n) {
if n == 0 { return 42 }
f(n-1)
}
f(3) // was 42, is now undefined
The frame remembers that a tail call dropped its result; OpReturn then
yields undefined for that frame. This keeps frame reuse (the existing
deep-recursion tests rely on it) and costs one store per call and one
branch per return. Inherited from upstream tengo.
…tack
Calling a UserFunction copied the arguments into a fresh slice on every
call because the VM cannot know whether the Go function keeps the slice.
UserFunction gains a StackArgs flag with the same meaning as the
builtins' internal one: the function promises not to retain args, and
the VM passes a window of its operand stack instead. The stdlib sets the
flag for every module function in an init, since none of them keeps
args (audited: the only slices passed on are to Format, which consumes
them synchronously). Host code can opt in the same way.
Benchmarks: benchstat over 4 runs, previous commit vs this one:
│ sec/op │ sec/op vs base │
VM/ArithLoop-16 30.74m ± ∞ ¹ 31.01m ± ∞ ¹ +0.91% (p=0.029 n=4)
VM/FloatLoop-16 41.39m ± ∞ ¹ 42.26m ± ∞ ¹ +2.10% (p=0.029 n=4)
VM/CompareLoop-16 31.32m ± ∞ ¹ 30.19m ± ∞ ¹ -3.59% (p=0.029 n=4)
VM/Fib-16 3.434m ± ∞ ¹ 3.598m ± ∞ ¹ +4.78% (p=0.029 n=4)
VM/ClosureCalls-16 17.12m ± ∞ ¹ 18.44m ± ∞ ¹ +7.70% (p=0.029 n=4)
VM/MapAccess-16 39.03m ± ∞ ¹ 40.57m ± ∞ ¹ +3.95% (p=0.029 n=4)
VM/MethodCalls-16 36.88m ± ∞ ¹ 37.84m ± ∞ ¹ +2.62% (p=0.029 n=4)
VM/ArrayIter-16 9.341m ± ∞ ¹ 10.072m ± ∞ ¹ +7.82% (p=0.029 n=4)
VM/ArrayIndex-16 25.45m ± ∞ ¹ 26.66m ± ∞ ¹ +4.74% (p=0.029 n=4)
VM/StringOps-16 12.00m ± ∞ ¹ 12.23m ± ∞ ¹ +1.94% (p=0.029 n=4)
VM/Builtins-16 28.60m ± ∞ ¹ 30.36m ± ∞ ¹ +6.16% (p=0.029 n=4)
VM/StdlibCalls-16 11.84m ± ∞ ¹ 10.25m ± ∞ ¹ -13.45% (p=0.029 n=4)
VM/MapIter-16 22.16m ± ∞ ¹ 23.15m ± ∞ ¹ +4.47% (p=0.029 n=4)
VM/ShortScript-16 940.6n ± ∞ ¹ 938.5n ± ∞ ¹ ~ (p=0.486 n=4)
geomean 9.727m 9.921m +1.99%
│ x_full.txt │ 8-stackargs.txt │
│ B/op │ B/op vs base │
VM/ArithLoop-16 9.144Mi ± ∞ ¹ 9.144Mi ± ∞ ¹ ~ (p=0.314 n=4)
VM/FloatLoop-16 16.78Mi ± ∞ ¹ 16.78Mi ± ∞ ¹ ~ (p=0.886 n=4)
VM/CompareLoop-16 3.426Mi ± ∞ ¹ 3.426Mi ± ∞ ¹ ~ (p=0.057 n=4)
VM/Fib-16 447.5 ± ∞ ¹ 461.5 ± ∞ ¹ ~ (p=0.257 n=4)
VM/ClosureCalls-16 3.040Mi ± ∞ ¹ 3.040Mi ± ∞ ¹ +0.00% (p=0.029 n=4)
VM/MapAccess-16 6.086Mi ± ∞ ¹ 6.086Mi ± ∞ ¹ ~ (p=0.314 n=4)
VM/MethodCalls-16 3.042Mi ± ∞ ¹ 3.042Mi ± ∞ ¹ ~ (p=0.257 n=4)
VM/ArrayIter-16 1.593Mi ± ∞ ¹ 1.593Mi ± ∞ ¹ ~ (p=0.286 n=4)
VM/ArrayIndex-16 3.104Mi ± ∞ ¹ 3.104Mi ± ∞ ¹ +0.00% (p=0.029 n=4)
VM/StringOps-16 5.176Mi ± ∞ ¹ 5.176Mi ± ∞ ¹ ~ (p=0.057 n=4)
VM/Builtins-16 3.041Mi ± ∞ ¹ 3.041Mi ± ∞ ¹ +0.01% (p=0.029 n=4)
VM/StdlibCalls-16 3.429Mi ± ∞ ¹ 1.138Mi ± ∞ ¹ -66.81% (p=0.029 n=4)
VM/MapIter-16 14.32Mi ± ∞ ¹ 14.32Mi ± ∞ ¹ ~ (p=0.200 n=4)
VM/ShortScript-16 560.0 ± ∞ ¹ 560.0 ± ∞ ¹ ~ (p=1.000 n=4)
geomean 1.261Mi 1.168Mi -7.37%
│ x_full.txt │ 8-stackargs.txt │
│ allocs/op │ allocs/op vs base │
VM/ArithLoop-16 37.44k ± ∞ ¹ 37.44k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/FloatLoop-16 68.72k ± ∞ ¹ 68.72k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/CompareLoop-16 14.02k ± ∞ ¹ 14.02k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/Fib-16 0.000 ± ∞ ¹ 0.000 ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/ClosureCalls-16 12.44k ± ∞ ¹ 12.44k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/MapAccess-16 24.91k ± ∞ ¹ 24.91k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/MethodCalls-16 12.44k ± ∞ ¹ 12.44k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/ArrayIter-16 7.461k ± ∞ ¹ 7.461k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/ArrayIndex-16 13.48k ± ∞ ¹ 13.48k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/StringOps-16 151.7k ± ∞ ¹ 151.7k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/Builtins-16 12.44k ± ∞ ¹ 12.44k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/StdlibCalls-16 153.10k ± ∞ ¹ 53.09k ± ∞ ¹ -65.32% (p=0.029 n=4)
VM/MapIter-16 214.5k ± ∞ ¹ 214.5k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/ShortScript-16 10.00 ± ∞ ¹ 10.00 ± ∞ ¹ ~ (p=1.000 n=4) ²
geomean ³ -7.29% ³
² all samples are equal
Iterating a map allocated a []string of the keys and then a new String
object for every key handed out by Key(). The iterator now snapshots the
keys straight into a []String and Key returns a pointer into it, so an
iteration costs two allocations instead of len(map)+1. The key objects
are only reachable through the iterator or through variables the script
assigned them to, and they stay valid for as long as either does.
Benchmarks: benchstat over 4 runs, previous commit vs this one:
│ sec/op │ sec/op vs base │
VM/ArithLoop-16 31.01m ± ∞ ¹ 31.23m ± ∞ ¹ ~ (p=0.343 n=4)
VM/FloatLoop-16 42.26m ± ∞ ¹ 42.97m ± ∞ ¹ ~ (p=0.057 n=4)
VM/CompareLoop-16 30.19m ± ∞ ¹ 30.23m ± ∞ ¹ ~ (p=0.886 n=4)
VM/Fib-16 3.598m ± ∞ ¹ 3.601m ± ∞ ¹ ~ (p=0.886 n=4)
VM/ClosureCalls-16 18.44m ± ∞ ¹ 18.91m ± ∞ ¹ ~ (p=0.886 n=4)
VM/MapAccess-16 40.57m ± ∞ ¹ 42.98m ± ∞ ¹ +5.93% (p=0.029 n=4)
VM/MethodCalls-16 37.84m ± ∞ ¹ 40.17m ± ∞ ¹ +6.14% (p=0.029 n=4)
VM/ArrayIter-16 10.07m ± ∞ ¹ 10.72m ± ∞ ¹ +6.45% (p=0.029 n=4)
VM/ArrayIndex-16 26.66m ± ∞ ¹ 28.06m ± ∞ ¹ +5.26% (p=0.029 n=4)
VM/StringOps-16 12.23m ± ∞ ¹ 13.02m ± ∞ ¹ +6.41% (p=0.029 n=4)
VM/Builtins-16 30.36m ± ∞ ¹ 31.20m ± ∞ ¹ ~ (p=0.343 n=4)
VM/StdlibCalls-16 10.25m ± ∞ ¹ 10.07m ± ∞ ¹ -1.70% (p=0.029 n=4)
VM/MapIter-16 23.15m ± ∞ ¹ 19.56m ± ∞ ¹ -15.50% (p=0.029 n=4)
VM/ShortScript-16 938.5n ± ∞ ¹ 932.2n ± ∞ ¹ ~ (p=0.686 n=4)
geomean 9.921m 10.05m +1.29%
│ 8-stackargs.txt │ 9-mapiter.txt │
│ B/op │ B/op vs base │
VM/ArithLoop-16 9.144Mi ± ∞ ¹ 9.145Mi ± ∞ ¹ ~ (p=1.000 n=4)
VM/FloatLoop-16 16.78Mi ± ∞ ¹ 16.78Mi ± ∞ ¹ ~ (p=0.886 n=4)
VM/CompareLoop-16 3.426Mi ± ∞ ¹ 3.426Mi ± ∞ ¹ ~ (p=0.657 n=4)
VM/Fib-16 461.5 ± ∞ ¹ 312.0 ± ∞ ¹ ~ (p=0.657 n=4)
VM/ClosureCalls-16 3.040Mi ± ∞ ¹ 3.040Mi ± ∞ ¹ ~ (p=0.343 n=4)
VM/MapAccess-16 6.086Mi ± ∞ ¹ 6.086Mi ± ∞ ¹ ~ (p=0.457 n=4)
VM/MethodCalls-16 3.042Mi ± ∞ ¹ 3.042Mi ± ∞ ¹ ~ (p=0.286 n=4)
VM/ArrayIter-16 1.593Mi ± ∞ ¹ 1.593Mi ± ∞ ¹ ~ (p=0.229 n=4)
VM/ArrayIndex-16 3.104Mi ± ∞ ¹ 3.104Mi ± ∞ ¹ ~ (p=1.000 n=4)
VM/StringOps-16 5.176Mi ± ∞ ¹ 5.176Mi ± ∞ ¹ ~ (p=0.886 n=4)
VM/Builtins-16 3.041Mi ± ∞ ¹ 3.041Mi ± ∞ ¹ +0.00% (p=0.029 n=4)
VM/StdlibCalls-16 1.138Mi ± ∞ ¹ 1.138Mi ± ∞ ¹ ~ (p=0.914 n=4)
VM/MapIter-16 14.318Mi ± ∞ ¹ 9.557Mi ± ∞ ¹ -33.25% (p=0.029 n=4)
VM/ShortScript-16 560.0 ± ∞ ¹ 560.0 ± ∞ ¹ ~ (p=1.000 n=4)
geomean 1.168Mi 1.104Mi -5.52%
│ 8-stackargs.txt │ 9-mapiter.txt │
│ allocs/op │ allocs/op vs base │
VM/ArithLoop-16 37.44k ± ∞ ¹ 37.44k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/FloatLoop-16 68.72k ± ∞ ¹ 68.72k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/CompareLoop-16 14.02k ± ∞ ¹ 14.02k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/Fib-16 0.000 ± ∞ ¹ 0.000 ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/ClosureCalls-16 12.44k ± ∞ ¹ 12.44k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/MapAccess-16 24.91k ± ∞ ¹ 24.91k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/MethodCalls-16 12.44k ± ∞ ¹ 12.44k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/ArrayIter-16 7.461k ± ∞ ¹ 7.461k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/ArrayIndex-16 13.48k ± ∞ ¹ 13.48k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/StringOps-16 151.7k ± ∞ ¹ 151.7k ± ∞ ¹ ~ (p=0.429 n=4)
VM/Builtins-16 12.44k ± ∞ ¹ 12.44k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/StdlibCalls-16 53.09k ± ∞ ¹ 53.09k ± ∞ ¹ ~ (p=1.000 n=4) ²
VM/MapIter-16 214.50k ± ∞ ¹ 14.50k ± ∞ ¹ -93.24% (p=0.029 n=4)
VM/ShortScript-16 10.00 ± ∞ ¹ 10.00 ± ∞ ¹ ~ (p=1.000 n=4) ²
geomean ³ -17.50% ³
² all samples are equal
The VM no longer interprets CompiledFunction.Instructions byte by byte.
Each function is translated once (vm_code.go) into fixed-width instrs in
which operand stack slots have become frame-relative registers: the stack
depth before every instruction is static, so the slot an instruction
reads or writes is known at translation time, no stack pointer is kept
while running, OpPop disappears, and operands are decoded once instead
of on every execution. Operands carry a kind (temp slot, local, constant,
global, free variable, literal), which is what the next commit's fusion
pass builds on; on its own this commit keeps one instruction per opcode,
with the common loads and stores specialized into dedicated opcodes.
The serialized bytecode, the compiler and all bytecode-level tests are
unchanged. The translation happens after RemoveDuplicates (which
rewrites constant indexes in place) in Script.Compile and in Decode, and
lazily for bytecode assembled by hand. Error positions come from the
originating opcode offset recorded on each instr. The tail-call and
stack-overflow checks carry over.
On its own this is not faster: the numbers below are the price of the
operand-kind dispatch without the fusion that pays for it.
Benchmarks: benchstat over 6 interleaved runs, previous commit vs this one:
│ sec/op │ sec/op vs base │
VM/ArithLoop-16 30.73m ± 1% 33.44m ± 2% +8.83% (p=0.002 n=6)
VM/FloatLoop-16 42.09m ± 1% 45.40m ± 3% +7.86% (p=0.002 n=6)
VM/CompareLoop-16 30.02m ± 1% 34.44m ± 1% +14.73% (p=0.002 n=6)
VM/Fib-16 3.583m ± 1% 4.391m ± 1% +22.55% (p=0.002 n=6)
VM/ClosureCalls-16 18.25m ± 1% 19.76m ± 1% +8.27% (p=0.002 n=6)
VM/MapAccess-16 40.43m ± 1% 45.71m ± 0% +13.06% (p=0.002 n=6)
VM/MethodCalls-16 37.64m ± 0% 49.20m ± 1% +30.73% (p=0.002 n=6)
VM/ArrayIter-16 9.918m ± 1% 10.008m ± 1% ~ (p=0.240 n=6)
VM/ArrayIndex-16 26.24m ± 1% 28.90m ± 1% +10.12% (p=0.002 n=6)
VM/StringOps-16 12.07m ± 1% 13.78m ± 1% +14.22% (p=0.002 n=6)
VM/Builtins-16 30.12m ± 2% 36.03m ± 0% +19.62% (p=0.002 n=6)
VM/StdlibCalls-16 9.404m ± 1% 11.090m ± 0% +17.93% (p=0.002 n=6)
VM/MapIter-16 20.19m ± 1% 22.40m ± 2% +10.92% (p=0.002 n=6)
VM/ShortScript-16 934.5n ± 1% 975.6n ± 1% +4.39% (p=0.002 n=6)
geomean 9.696m 10.95m +12.91%
│ ab_prev.txt │ ab_A.txt │
│ B/op │ B/op vs base │
VM/ArithLoop-16 9.145Mi ± 0% 9.145Mi ± 0% +0.00% (p=0.002 n=6)
VM/FloatLoop-16 16.78Mi ± 0% 16.78Mi ± 0% +0.00% (p=0.009 n=6)
VM/CompareLoop-16 3.426Mi ± 0% 3.427Mi ± 0% +0.01% (p=0.028 n=6)
VM/Fib-16 314.0 ± 53% 527.0 ± 68% ~ (p=0.078 n=6)
VM/ClosureCalls-16 3.040Mi ± 0% 3.040Mi ± 0% +0.01% (p=0.002 n=6)
VM/MapAccess-16 6.086Mi ± 0% 6.086Mi ± 0% +0.01% (p=0.017 n=6)
VM/MethodCalls-16 3.042Mi ± 0% 3.043Mi ± 0% +0.04% (p=0.026 n=6)
VM/ArrayIter-16 1.593Mi ± 0% 1.593Mi ± 0% ~ (p=0.554 n=6)
VM/ArrayIndex-16 3.104Mi ± 0% 3.105Mi ± 0% ~ (p=0.061 n=6)
VM/StringOps-16 5.176Mi ± 0% 5.175Mi ± 0% ~ (p=0.937 n=6)
VM/Builtins-16 3.041Mi ± 0% 3.041Mi ± 0% ~ (p=0.061 n=6)
VM/StdlibCalls-16 1.138Mi ± 0% 1.138Mi ± 0% ~ (p=0.680 n=6)
VM/MapIter-16 9.557Mi ± 0% 9.557Mi ± 0% ~ (p=0.394 n=6)
VM/ShortScript-16 560.0 ± 0% 560.0 ± 0% ~ (p=1.000 n=6) ¹
geomean 1.104Mi 1.146Mi +3.77%
│ ab_prev.txt │ ab_A.txt │
│ allocs/op │ allocs/op vs base │
VM/ArithLoop-16 37.44k ± 0% 37.44k ± 0% ~ (p=1.000 n=6) ¹
VM/FloatLoop-16 68.72k ± 0% 68.72k ± 0% ~ (p=1.000 n=6) ¹
VM/CompareLoop-16 14.02k ± 0% 14.02k ± 0% ~ (p=1.000 n=6) ¹
VM/Fib-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹
VM/ClosureCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/MapAccess-16 24.91k ± 0% 24.91k ± 0% ~ (p=1.000 n=6) ¹
VM/MethodCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6)
VM/ArrayIter-16 7.461k ± 0% 7.461k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIndex-16 13.48k ± 0% 13.48k ± 0% ~ (p=1.000 n=6) ¹
VM/StringOps-16 151.7k ± 0% 151.7k ± 0% ~ (p=1.000 n=6) ¹
VM/Builtins-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/StdlibCalls-16 53.09k ± 0% 53.09k ± 0% ~ (p=1.000 n=6) ¹
VM/MapIter-16 14.50k ± 0% 14.50k ± 0% ~ (p=1.000 n=6) ¹
VM/ShortScript-16 10.00 ± 0% 10.00 ± 0% ~ (p=1.000 n=6) ¹
geomean ² +0.00% ²
A peephole pass over the translated code folds a load into the
instruction that consumes it, a store into the instruction that produces
its value, and a comparison into the conditional jump that tests it.
`s += (i * 3 + 1) % 7` becomes four instructions instead of eleven stack
ops; the loop body of the ArithLoop benchmark goes from 20 stack ops to 8
instructions. Fusion never crosses a jump target, except that a load
which is itself a target may be absorbed by its consumer (a jump to it
then executes the same work), and nothing before such a load is absorbed.
Numbers are from interleaved runs of the two binaries (A B A B ...) so
that clock-frequency and background drift cancels; per-benchmark swings
of around ten percent in either direction on code the change does not
touch (iterators) remain and are layout effects of the large dispatch
function.
Benchmarks: benchstat over 6 interleaved runs, previous commit vs this one:
│ sec/op │ sec/op vs base │
VM/ArithLoop-16 33.32m ± 0% 24.68m ± 0% -25.93% (p=0.002 n=6)
VM/FloatLoop-16 45.03m ± 1% 35.07m ± 1% -22.11% (p=0.002 n=6)
VM/CompareLoop-16 34.12m ± 1% 23.50m ± 0% -31.13% (p=0.002 n=6)
VM/Fib-16 4.397m ± 1% 3.354m ± 1% -23.72% (p=0.002 n=6)
VM/ClosureCalls-16 19.82m ± 0% 14.85m ± 0% -25.06% (p=0.002 n=6)
VM/MapAccess-16 45.50m ± 1% 38.17m ± 1% -16.11% (p=0.002 n=6)
VM/MethodCalls-16 49.13m ± 1% 34.43m ± 1% -29.91% (p=0.002 n=6)
VM/ArrayIter-16 9.973m ± 1% 9.329m ± 1% -6.46% (p=0.002 n=6)
VM/ArrayIndex-16 28.90m ± 0% 21.97m ± 0% -23.99% (p=0.002 n=6)
VM/StringOps-16 13.71m ± 1% 11.60m ± 1% -15.36% (p=0.002 n=6)
VM/Builtins-16 35.99m ± 0% 29.61m ± 0% -17.74% (p=0.002 n=6)
VM/StdlibCalls-16 11.089m ± 2% 9.198m ± 0% -17.06% (p=0.002 n=6)
VM/MapIter-16 22.16m ± 1% 20.73m ± 1% -6.45% (p=0.002 n=6)
VM/ShortScript-16 977.1n ± 1% 963.8n ± 1% -1.37% (p=0.015 n=6)
geomean 10.91m 8.820m -19.20%
│ ab_A2.txt │ ab_B.txt │
│ B/op │ B/op vs base │
VM/ArithLoop-16 9.145Mi ± 0% 9.144Mi ± 0% -0.01% (p=0.035 n=6)
VM/FloatLoop-16 16.78Mi ± 0% 16.78Mi ± 0% -0.01% (p=0.002 n=6)
VM/CompareLoop-16 3.426Mi ± 0% 3.426Mi ± 0% -0.03% (p=0.017 n=6)
VM/Fib-16 531.0 ± 1% 166.0 ± 167% -68.74% (p=0.002 n=6)
VM/ClosureCalls-16 3.040Mi ± 0% 3.040Mi ± 0% -0.01% (p=0.002 n=6)
VM/MapAccess-16 6.086Mi ± 0% 6.086Mi ± 0% ~ (p=0.331 n=6)
VM/MethodCalls-16 3.043Mi ± 0% 3.041Mi ± 0% -0.05% (p=0.037 n=6)
VM/ArrayIter-16 1.593Mi ± 0% 1.593Mi ± 0% -0.00% (p=0.041 n=6)
VM/ArrayIndex-16 3.105Mi ± 0% 3.104Mi ± 0% ~ (p=0.061 n=6)
VM/StringOps-16 5.176Mi ± 0% 5.176Mi ± 0% -0.00% (p=0.041 n=6)
VM/Builtins-16 3.041Mi ± 0% 3.041Mi ± 0% ~ (p=0.058 n=6)
VM/StdlibCalls-16 1.138Mi ± 0% 1.138Mi ± 0% ~ (p=1.000 n=6)
VM/MapIter-16 9.557Mi ± 0% 9.557Mi ± 0% ~ (p=0.065 n=6)
VM/ShortScript-16 560.0 ± 0% 560.0 ± 0% ~ (p=1.000 n=6) ¹
geomean 1.147Mi 1.055Mi -7.98%
│ ab_A2.txt │ ab_B.txt │
│ allocs/op │ allocs/op vs base │
VM/ArithLoop-16 37.44k ± 0% 37.44k ± 0% ~ (p=1.000 n=6) ¹
VM/FloatLoop-16 68.72k ± 0% 68.72k ± 0% ~ (p=1.000 n=6) ¹
VM/CompareLoop-16 14.02k ± 0% 14.02k ± 0% ~ (p=1.000 n=6) ¹
VM/Fib-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹
VM/ClosureCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/MapAccess-16 24.91k ± 0% 24.91k ± 0% ~ (p=1.000 n=6) ¹
VM/MethodCalls-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIter-16 7.461k ± 0% 7.461k ± 0% ~ (p=1.000 n=6) ¹
VM/ArrayIndex-16 13.48k ± 0% 13.48k ± 0% ~ (p=1.000 n=6) ¹
VM/StringOps-16 151.7k ± 0% 151.7k ± 0% ~ (p=0.455 n=6)
VM/Builtins-16 12.44k ± 0% 12.44k ± 0% ~ (p=1.000 n=6) ¹
VM/StdlibCalls-16 53.09k ± 0% 53.09k ± 0% ~ (p=1.000 n=6) ¹
VM/MapIter-16 14.50k ± 0% 14.50k ± 0% ~ (p=1.000 n=6) ¹
VM/ShortScript-16 10.00 ± 0% 10.00 ± 0% ~ (p=1.000 n=6) ¹
geomean ² +0.00% ²
Translating a function used half a dozen scratch slices sized by its
instruction count and kept the fusion pass's slack in the retained code.
Bytecode.prepare now carries one translator whose buffers are reused for
every function, and only an exact-size copy of the result is kept. Scanned
depths are int32 instead of int.
Compile-time cost of the translation, BenchmarkCompile (40 modules, 1000
functions), benchstat, previous commit vs this one:
sec/op 41.13m ± 4% 41.87m ± 5% ~ (p=0.635)
B/op 32.09Mi 23.09Mi -28.04%
allocs/op 284.6k 277.4k -2.54%
Against the baseline before the register form, compiling is still about
16% slower and uses 15% more memory: that is the translation itself,
paid once per compile, with the retained instrs accounting for the
memory (about 20 bytes per original instruction).
golint rejects a leading k on identifiers (kTemp, kConst, ...); the operand kinds in vm_code.go are now kindTemp, kindConst and so on. No behaviour change.
xnacly
marked this pull request as ready for review
October 4, 2026 13:08
… decode Decode recomputed the unserialized stack bound only for the functions in Constants, so a decoded main function ran with a zero bound and could index past the operand stack instead of returning ErrStackOverflow. Decoding into a Bytecode that an earlier Decode had already prepared also left the prepared flag set, so the new functions were never translated and the first call into them dereferenced nil code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Baseline (master) vs branch tip, benchstat over 6 interleaved runs, AMD Ryzen 7 3700X, go1.27.1.