PATTERN Cited by 1 source
Static type-specialized bytecode¶
Problem¶
Dynamic languages type-switch inside every opcode at runtime —
ADD has to check whether operands are ints, floats, decimals,
or strings, and branch accordingly. Type switches are:
- Slow. Every opcode pays the type-branch cost on every execution.
- Allocation-heavy. Generic opcodes operate on boxed
values (
interface{}/Object/PyObject) because the opcode doesn't know what unboxed type to use. - Cache-unfriendly. Type-branching code has poor icache locality.
Quickening solves this by observing types at runtime and rewriting opcodes to type-specialized variants once types stabilise. But quickening adds runtime rewrite machinery, warm-up cost, and deoptimization complexity.
Solution¶
If the source language has strong static type information available at compile time — from a schema, semantic analyzer, type annotations, or constrained inputs — the compiler can emit already-specialized opcodes directly.
Instead of one generic ADD opcode with a runtime type switch,
the compiler emits ADD_INT64_INT64, ADD_FLOAT_FLOAT,
ADD_DECIMAL_DECIMAL, etc. — one for each operand-type
combination. Each specialized opcode:
- Operates on unboxed native types (no boxing, no allocation).
- Has no runtime type check — the check happened at compile time.
- Is small and tight — one operation, one path.
Canonical instance: Vitess evalengine¶
The Vitess evalengine rewrite is the canonical wiki instance (Source: sources/2025-04-05-planetscale-faster-interpreters-in-go-catching-up-with-cpp).
Vitess's semantic analyzer integrates with the upstream MySQL server's information schema to derive static types for every sub-expression in a query:
- Column types come from the schema tracker.
- Literal types come from the parsed AST.
- Derived types (e.g.
a + bwhereaisBIGINTandbisDECIMAL) are computed by SQL type-promotion rules applied at compile time.
The VM compiler then emits a type-specialized closure for each operation — see patterns/callback-slice-vm-go for the compile-to-closure mechanics:
// Push a BIGINT column from input row at offset.
func (c *compiler) emitPushColumn_i(offset int) {
c.emit(func(vm *VirtualMachine) int {
vm.stack[vm.sp] = &evalInt64{i: vm.row[offset].Int64()}
vm.sp++
return 1
})
}
// Negate a BIGINT on top of stack.
func (c *compiler) emitNeg_i() {
c.emit(func(vm *VirtualMachine) int {
arg := vm.stack[env.vm.sp-1].(*evalInt64)
if arg.i == math.MinInt64 {
vm.err = errDeoptimize
} else {
arg.i = -arg.i
}
return 1
})
}
Note: no type switch inside the opcode; the type *evalInt64
is statically known.
The deoptimization contract¶
Static specialization assumes compile-time-derived types remain valid at runtime. This is mostly true, but SQL (and many other languages) have value-dependent type promotions:
-BIGINT_MIN = -9223372036854775808promotes toDECIMALbecause the magnitude exceedsBIGINTrange.- Integer arithmetic overflow promotes silently.
- String operations can produce wider/narrower character sets depending on collation.
Static specialization must pair with VM deoptimization to handle these cases — the specialized opcode bails out to the AST interpreter when it encounters a value-dependent case. See patterns/vm-ast-dual-interpreter-fallback.
Properties¶
| Property | Value |
|---|---|
| Runtime type switch cost | Zero |
| Runtime allocation | Zero for most opcodes (Vitess: 4/5 benchmarks) |
| Compile-time analysis cost | Moderate (type inference) |
| Opcode count | Large (one per type combination) |
| Deoptimization required | Yes (for value-dependent cases) |
| Fits | Constrained languages with strong compile-time type info |
When to use¶
- SQL expression engines. Schema provides column types; semantic analyzer derives sub-expression types; static specialization is a natural fit.
- Typed template engines where context types are known at compile time.
- DSLs embedded in typed hosts. Config languages, rule engines, expression evaluators inside typed codebases.
- Any language where the compiler can know operand types ahead of time.
When not to use¶
- General-purpose dynamically-typed languages. Python, JavaScript, Ruby — types can only be known at runtime. Quickening or JIT type speculation is the right approach.
- Languages where type inference is too expensive to do at compile time. The specialization win must exceed the compilation cost.
Caveats¶
- Opcode count explodes. N types × M arithmetic operations = N*M specialized opcodes. Vitess's evalengine has hundreds of specialized operations.
- Deoptimization path must exist. Without a runtime- type-switching fallback, value-dependent type promotions can't be handled correctly. See patterns/vm-ast-dual-interpreter-fallback.
- Compile-time type inference must be sound. A bug in the type inferencer produces an incorrect specialized opcode, corrupting results.
- Requires information-schema integration for SQL. If you can't know column types at compile time (e.g. cross-shard schema drift), specialization must defer to runtime.
Seen in¶
- sources/2025-04-05-planetscale-faster-interpreters-in-go-catching-up-with-cpp — canonical wiki instance. Vitess evalengine compiles each SQL sub-expression's AST into a slice of type-specialized closures; types flow from the MySQL information schema through the semantic analyzer into the compiler. Zero runtime type switches; zero memory allocations on 4/5 benchmarks.