Skip to content

CONCEPT Cited by 1 source

Static type specialization

Static type specialization is an interpreter/compiler optimisation that eliminates runtime type dispatching by emitting already-type-specific instructions at compile time.

Instead of a single generic ADD opcode that checks operand types at runtime and branches to the correct implementation, the compiler emits a specific opcode for the specific types — ADD_INT64_INT64, ADD_FLOAT_FLOAT, ADD_DECIMAL_DECIMAL, etc. Each specialized opcode does exactly one thing, doesn't branch on type, and doesn't box operands.

Contrast with runtime specialization ("quickening")

The alternative is quickening — Brunthaler's technique where the VM observes operand types at runtime and rewrites the bytecode to use specialized opcodes once types stabilise. Quickening is what JavaScript engines and PyPy's tracing JIT do, in various forms.

Both approaches win the same optimisation. Static specialization wins it at compile time; quickening wins it at runtime by observation.

When static specialization is possible

Static specialization requires the compiler to know the types of all operands before emitting code. For a general-purpose dynamic language this isn't possible — operand types depend on runtime values. But for constrained languages with good static type information, it is:

  • SQL expressions — columns have declared types in the database schema; constants are literally typed; operators have well-defined type rules. The Vitess evalengine example: "the semantic analysis we perform in Vitess is advanced enough that, through careful integration with the upstream MySQL server and its information schema, it can be used to statically type the AST of the SQL expressions we were executing." (Source: sources/2025-04-05-planetscale-faster-interpreters-in-go-catching-up-with-cpp)
  • Typed template engines where variable types come from schema-defined context.
  • Statically-typed DSLs — configuration languages, rule languages, expression builders inside typed codebases.

Consequences on performance

  • No runtime type switches. Each specialized opcode's hot path is straight-line code.
  • No runtime type boxing. Operands live on the stack as their native types, not as interface{} / Object / PyObject. In Go, this frees callers from allocating boxed values — Vitess's VM allocates zero memory on 4/5 benchmarks.
  • Simpler VM. The VM doesn't need a type-switch tree inside every opcode.

Consequences on design

  • Compile time moves up. Type inference is now part of compilation, not dispatch — the compiler is more complex, the VM is simpler.
  • Deoptimization becomes mandatory. Some operations have value-dependent types — canonical Vitess example: -BIGINT_MIN = -9223372036854775808 produces DECIMAL, not BIGINT, because |MIN_INT64| exceeds INT64 range. The compile-time specialization is invalidated by the runtime value, so the VM must fall back to a runtime-type-switching interpreter (typically the AST interpreter).

Relation to JIT speculation

JIT compilers emit type-specialized native code using runtime type observations, with guards that bail out to the VM on failure. Static specialization does the same thing at a different layer — using compile-time type derivations, with deoptimization to the AST interpreter on value-dependent type promotions. The guard-and-bail pattern is identical; the layer changes.

Seen in

Last updated · 319 distilled / 1,201 read