Skip to main content

Portable Interpreter (No-JIT Runtime)

How ConjureDB runs queries that are delivered as configuration at runtime — on platforms that forbid JIT compilation — without giving up speed or allocation-lean execution.

ConjureDB's default path compiles every query you declare to C# at build time, so there is nothing to interpret at runtime (see Compiled Queries). But some queries are not known at build time — they ship later as content or config (live-ops tuning, server-authored queries). On iOS and Unity/IL2CPP you cannot JIT or emit code at runtime to handle them. The portable interpreter is the lane that executes those queries: it runs a compact, pre-verified bytecode with no JIT, no runtime code generation, and no reflection, so it works everywhere AOT does — and it is still fast and allocation-lean.

See also: Compiled Queries · Performance Tips · Query Language · Database Engine


Table of Contents


Two execution lanes

ConjureDB has two explicit, contract-driven execution lanes for the same query semantics:

LaneWhat it isBuilt for
AOT C# (default)Your query is compiled to C# at build time and runs as native AOT/IL2CPP codeThe hottest, highest-throughput queries you know at build time
Portable interpreterThe query ships as a portable bytecode artifact and is executed by a no-JIT register-based interpreterConfig-delivered queries that are not known at build time, on platforms without a JIT

Both lanes produce bit-identical results — the portable lane is continuously verified against the AOT-compiled path by differential and SQL-parity tests. The portable interpreter is a deliberate second lane, not a silent fallback: the runtime executes only the lane your host policy allows, and fails fast with an explicit reason code if a requested lane is unavailable or incompatible.

When each lane runs

  • A query you write and ship in your build → AOT C#. This is the default and the path the Performance numbers describe. Nothing is interpreted.
  • A query delivered as content or configuration after the build (so there is no C# to compile ahead of time), running on iOS or Unity/IL2CPP where runtime codegen is forbidden → portable interpreter.

You do not pick the lane per call at random — it is determined at compile time and by explicit host policy. Every query you declare in your build compiles to the AOT C# lane (the default). The portable interpreter runs a query only when a portable bytecode artifact is delivered and registered for that operation at runtime (for example a live-ops query override); the runtime's override cache routes matching operations to it. Emitting the portable artifact at all is a compiler emission option, not a per-query author annotation.

Performance expectations

The portable interpreter is built to be the kind of interpreter you can run in a frame budget:

  • Low steady-state allocation. Every operator family (scan, filter, project, sort, top-N, hash and multi-key aggregate, distinct, join, set ops, window) stays allocation-lean in the current corpus: 0 bytes for a point lookup and ≤ 152 bytes per query for the heaviest measured scenario. Pooled hashtables and arena-backed key/accumulator buffers keep the GC out of the hot path.
  • O(1) keyed lookups. A primary-key lookup probes the index in constant time — about 49 ns in the current vs-SQLite harness — versus a full scan that grows with row count. A point lookup is fully allocation-free.
  • Index-aware scans. Secondary-index plans use the same ordered accessors as the compiled path: indexed range top-N is about 433 ns, roughly 18× faster than the portable full-scan RangeTopN variant in the same harness.
  • ~10–50 ns per row. Most queries run in that band per row scanned, so latency tracks how much data the query touches rather than interpreter overhead. On a 1,000-row table a filtered projection is ~7.6 µs and a hash group-by is ~4.3 µs.
  • Faster than SQLite. On the same query over the same in-memory data — with a primary-key index plus matching secondary indexes for index-aware scenarios — the no-JIT lane beats embedded SQLite (whose VDBE is itself a bytecode interpreter) on every one of the 23 benchmarked relational query shapes, from 4.2× (product stock top-N) to ~50× (window running sum), while allocating a fraction of the memory. See the Performance page for the full per-scenario table.
  • Opcode fusion (super-instructions). Opcode fusion is implemented but not yet wired into the shipping emission pipeline. When enabled it fuses adjacent instruction pairs — LoadColumn+compare, LoadColumn+cast, and LoadImm+add/subtract — where the produced register directly feeds the next operation and the column index or immediate fits the narrowed 8-bit encoding. In the A/B benchmark the full corpus improves +1.9% geomean overall and +6.5% on hit scenarios; VM-bound projection casts, null-tests and comparisons are the practical wins.
  • Parity with the AOT lane. Same results, same ordering, same null semantics — the interpreter is a strict consumer of the same planned metadata the AOT lane uses.
note

The portable lane is fast for an interpreter, but the AOT C# lane is still the throughput ceiling — keep your hottest, build-time-known queries there. The portable lane's job is to run the queries the AOT lane structurally cannot (those not known at build time) on platforms a JIT cannot run on, without falling off a performance cliff.

See the Performance page for the measured table: throughput and allocation for the 23 relational scenarios. The project repository keeps the superinstruction A/B evidence in its portable runtime benchmark log.

What it is not

  • It is not a runtime query parser or a dynamic/reflection-based evaluator. The query is compiled and verified ahead of time into a typed bytecode; the device only decodes and executes it.
  • It is not a degraded fallback for failed compilation. Lane selection is explicit; an unsupported or incompatible plan is a hard error with a reason code, never a silent downgrade.
  • It does not replace the AOT C# lane. It complements it for the config-delivered, no-JIT case.