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:
| Lane | What it is | Built for |
|---|---|---|
| AOT C# (default) | Your query is compiled to C# at build time and runs as native AOT/IL2CPP code | The hottest, highest-throughput queries you know at build time |
| Portable interpreter | The query ships as a portable bytecode artifact and is executed by a no-JIT register-based interpreter | Config-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
RangeTopNvariant 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.
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.