Skip to content

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 + b where a is BIGINT and b is DECIMAL) 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 = -9223372036854775808 promotes to DECIMAL because the magnitude exceeds BIGINT range.
  • 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

Last updated · 319 distilled / 1,201 read