Skip to content
bizzehdee edited this page Jun 24, 2026 · 6 revisions

JIT

DScript ships an optional, opt-in JIT compiler that turns hot bytecode chunks into compiled delegates. It is off by default: with no compiler registered the VM runs the interpreter exactly as before, with no behavioural change.

Enabling the JIT

Register a compiler once at startup:

using DScript;
using DScript.Vm;
using DScript.Jit;

JitRegistry.Register(new ReflectionEmitJitCompiler());

var engine = new ScriptEngine();
engine.Run(ScriptEngine.Compile(@"
    function poly(a, b) { return a * a + b * b; }
    var s = 0;
    for (var i = 0; i < 1000000; i++) s = poly(i, i + 1);
"));

Disable it again with JitRegistry.Clear(). The JIT and interpreter coexist — you choose which operates, and you can switch at runtime.

How it works

A chunk is compiled once it becomes hotJitThresholds.InvocationThreshold (1000) invocations, or JitThresholds.BackEdgeThreshold (10000) loop back-edges. Compiled code is value-identical to the interpreter; the JIT only changes how fast it runs, never what it computes.

The Reflection.Emit back-end compiles in tiers, picking the fastest applicable one:

Tier Applies to What it does
Speculative unboxed int pure, branch-free functions whose arithmetic is profiled integer-only flows raw int through generated code — no per-operation allocation, no dynamic dispatch; boxes only at the return
Speculative unboxed double likewise, for floating-point arithmetic (+ - * /) flows raw double
Conservative (boxed) everything else it can compile (straight-line code, variable/constant reads, arithmetic, property reads, plain calls) mirrors the interpreter's operand semantics exactly

The conservative tier also compiles control flow (if/else, while, for), local variables and assignments, property reads/writes, and calls — and inlines small pure-parameter leaf helpers at monomorphic call sites (no per-call frame allocation). Functions containing try/catch, generators, async, short-circuit (&&/||/??) or for..of, block-scoped let/const, object/array literals, or tail/method calls are still declined and keep running on the interpreter. (The closure back-end additionally declines all control flow.)

Deoptimization

The speculative tiers assume a type (e.g. "always integer"). If that assumption is violated at runtime — a function compiled for ints is called with a string — the compiled code deoptimizes: it abandons the run and re-executes the chunk on the interpreter, returning the correct result. This is safe because only pure (call-free) functions speculate, so no side effect can have occurred first. After JitThresholds.DeoptThreshold (5) deopts the chunk is recompiled with the conservative tier, which never deopts.

Inline cache

JIT-compiled property reads (obj.prop) use a per-site inline cache keyed by object identity and shape version, skipping the property lookup on a repeat read of the same object. Adding or removing a property bumps the object's shape version and invalidates the entry, so reads always stay correct.

Choosing a back-end

The JIT is pluggable through the IJitCompiler interface, and two back-ends ship:

Back-end Mechanism Performance Portability
ReflectionEmitJitCompiler emits CIL via System.Reflection.Emit (the tiered compiler above) highest all CoreCLR platforms; not NativeAOT
ClosureThreadedJitCompiler composes a tree of C# closures — no reflection, no runtime code generation more modest (values stay boxed) fully portable, NativeAOT-safe
JitRegistry.Register(new ClosureThreadedJitCompiler()); // no-reflection, AOT-safe

Both produce interpreter-identical results. Pick ClosureThreadedJitCompiler when you need NativeAOT or want to avoid runtime code generation; otherwise ReflectionEmitJitCompiler is faster.

Writing your own back-end

Implement IJitCompiler:

public sealed class MyJitCompiler : IJitCompiler
{
    public JitDelegate Compile(Chunk chunk)
    {
        // Return a compiled delegate, or null to decline (the VM keeps interpreting).
    }
}

JitDelegate is ScriptVar (VirtualMachine vm, ScriptVar[] args, Environment env): vm is the runtime handle (for calls, deopt, property reads), and env is the full lexical environment for resolving variables. The shared JitDecoder lowers a chunk's bytecode into a uniform instruction list you can build on.

Clone this wiki locally