Performance Notes​

How fast JaiScript is, what fast script looks like, and how to measure it. This is the one page on the site that holds benchmark numbers.

Read ratios within one table, never across tables. Every comparison with another runtime was measured on the reference machine (Core Ultra 9 285HX, MSVC x64 Release with LTCG, pinned to one P core). The embedder cost tables were measured on the report machine, a 2016-era 4-core/8-thread laptop, with a different build of the VM. The reference machine runs everything several times faster.

The headline​

Whole programs, each written the ordinary way in every language, hash-checked against each other, timing their own workload so start-up and compilation stay out. Lower is faster; the last column is the VM's time over Lua 5.4's.

programJai VMLua 5.4Squirrel 3.2VM / Lua
OSSUARY (raymarched scene, float math)127.1 ms/frame167.7388.50.76
bytecode VM written in script (dispatch loop over integer arrays)673 ms7441,1040.90
Gator Quest simulation (6,000 ticks, 4 bots)1.354 ms/tick1.151n/a1.18
GLOOM (software-rendered shooter)0.634 ms/tick0.3971.1651.60
Richards (scheduler: small methods over linked objects)788 ms3346092.36
DeltaBlue (constraint solver: deep hierarchy, virtual dispatch)948 ms3985882.38
spatial hash, 100,000 entities (a map of per-cell arrays)2,644 ms9721,1302.72

The pattern: typed loops over locals and typed arrays run level with or ahead of Lua; programs made of many small calls over shared objects take about 2.4 to 2.7 times Lua's time. Per-call and per-object cost is the VM's slowest ground. The four app rows are medians of seven interleaved processes; the game rows are detailed below.

Kernels and microbenchmarks​

The kernel suite. Over the sixteen interpreter kernels of daScript's dasProfile, run under its protocol (one process per sample repeating the kernel for 0.5 s, median of five, implementations interleaved, checksums equal), idiomatic typed JaiScript takes a geometric mean of 1.09× Lua 5.5.1's time, and the same algorithms with var everywhere 1.35×.

VM time / Lua timekernels
widest winsmandelbrot 0.04 (two float locals against Lua's complex-number tables), sha256 0.12, float-to-string 0.30, string-to-float 0.88
widest lossestreap 5.5, host calls 4.4, n-queens 3.3, dictionary 2.6, n-bodies 2.1, recursive fibonacci 1.8
LuaJIT's JIT, for scale0.01 to 0.73 across the suite

Microbenchmarks, µs per iteration, fastest of five runs:

benchmarkJai VMLua 5.4SquirrelChaiScript
Hot loop, 1000 iterations (sum += i)9310n/a
BST 15 nodes, & parameters18811n/a
Fibonacci(15)692555n/a
For loop, 100 iterations11312
String concat, 20 iterations11319

This harness prints whole microseconds, so it resolves only the rows above. Method invocation, class creation, array push/pop and map insert-and-lookup print 0 for the VM, 1 to 2 for Lua, 2 to 3 for Squirrel and 10 to 17 for ChaiScript: that orders the runtimes and gives no ratio. What the table does show is Lua ahead on the tight loop (about 3×), call-dense recursion (2.8×) and the script-built tree (2.3×), Squirrel ahead on the last two (1.25× and 1.6×), and ChaiScript 12 to 19 times slower wherever it runs.

Whole games​

Measured 2026-11-04 on the reference machine; every run's frame or state hash matches across runtimes. Each row is the median of three to six runs, the builds interleaved. Small differences are code layout, not speed: relinking the same program with unused padding moves most rows by under 2%, OSSUARY's serial row and GLOOM's parallel row by up to 9%, and LuaJIT's by over 20%.

workloadresult
DOOM: E1M1, textured software render at 120×78 plus status bar, six monsters awake and firing, 300-frame autopilot session1.3 ms/frame (sim 0.39, render 0.71, blit 0.21): 21 times inside the 28.5 ms frame budget, with enemy brains written as coroutines. The 3,984-tic timedemo replays at 1.389 ms/tic, about 20× real time.
GLOOM: 600 ticks after 600 warm-up, 100×40 outputVM serial 0.634 ms/tick, VM 8 workers 0.461; Lua 5.4.8 0.397, Lua 5.5.1 0.389, Squirrel 3.2 1.165, LuaJIT JIT 0.097. Eight workers give 1.38×: only the stages whose loops the engine proves safe run in parallel.
OSSUARY: the demoreel's raymarched scene at 100×40, five frames after five warm-upVM serial 127.1 ms/frame, VM 6 workers 23.7 (5.4×); Lua 5.4.8 167.7, Squirrel 3.2 388.5.
Demoreel: all seventeen scenes headless, 1,700 frames at 100×40, one coreVM 9.9 ms/frame, under a third of the 33.3 ms budget at 30 fps.
Gator Quest: simulation only (mobiles, combat, items, quests, bots), 6,000 ticksVM 1.354 ms/tick; Lua 5.4.8 and 5.5.1 1.151, Luau 0.741 interpreter 1.126, Luau native 0.655, LuaJIT interpreter 0.777, LuaJIT JIT 0.190.

LuaJIT's JIT generates machine code at run time, which some shipping targets (iOS) forbid: it is a ceiling, not a target. Style is nearly free: a flat rewrite of GLOOM in free functions and globals produces identical hashes and runs within 2% of the class-based version, so choose the architecture you want to maintain.

Patterns that matter​

jaiscript
// The fast shape: typed locals, typed arrays, & params, compound element stores.
array<int> pix = [];
for (int i = 0; i < 64; ++i) { pix.push(0); }

function shade(array<int>& dst, int base) {      // & = no copy, writes land in the caller
    for (int i = 0; i < dst.size(); ++i) {
        dst[i] += base + (i & 7);                // compound element store: one fused op
    }
}

shade(pix, 100);
int sum = 0;
for (int i = 0; i < pix.size(); ++i) { sum += pix[i]; }
print("sum = {}", sum);

function square(int x) { return x * x; }
array<int> nums = [];
for (int i = 1; i <= 8; ++i) { nums.push(i); }
var squares = parallel_transform(nums, square);   // deterministic at any worker count
print(to_json(squares));
output
sum = 6624
[1,4,9,16,25,36,49,64]

In priority order:

  1. Declare types. int, float and auto locals take the typed fast paths; var is the dynamic opt-out and costs accordingly (1.09× against 1.35× Lua on the kernel suite).
  2. Pass and hold anything heavy by &. Assignment and by-value parameters copy, as in C++. A copy that is only read binds in place for free, but treat that as a bonus (Value Semantics). Walk containers with for (auto& u : units).
  3. Use array<int> and array<float> for bulk numeric data. Same API, raw primitive storage: a 13,600-element grid takes 109 KB instead of 435 KB, a copy is a memcpy, and element access skips boxing.
  4. Write compound element stores. a[i] += v is one instruction, cheaper than the spelled-out read and write.
  5. Keep calls out of the innermost loop. A call costs about ten fused loop iterations. A self tail call (return f(args)) is the exception: it reuses the frame (about 40 ns a call) and runs at any depth.
  6. Parallelize the bulk stages. parallel_for and parallel_transform run bodies the engine proves safe on a worker pool, with identical results at any worker count.
  7. Define classes once. Executing a class declaration does redefinition and hot-reload bookkeeping (about 250 µs for a 30-line class script on the report machine). Per-frame scripts call; they do not declare.
  8. Reuse engines. Creating one and registering the stdlib costs hundreds of warm executes.

Strings need no builder: s += x, s = s + a + "-" + b, template strings and format() append into the string's own buffer, amortized O(1), whether s is a local, a global, a field, an element or a string& parameter.

What is slower, and what to use instead​

shapecostinstead
Call-dense non-tail recursion; object graphs walked through many small methodsfib(15) at 2.8× Lua, Richards and DeltaBlue at 2.4×: per-call frame setupiterate, or use accumulator-style tail calls
By-value parameters on big structuresthe BST benchmark by value runs 74 µs against 18 µs with &: a subtree copy at every level& parameters, var& declarations
Per-pixel loops (tens of thousands of scripted element operations a frame)GLOOM at 1.6× Luatyped arrays, then parallel_transform for the bulk stages, then one C++ function for the innermost loop
Re-executing class declarations every frameabout 250 µs per 30-line class scriptdeclare once, call per frame

The cost model for embedders​

What an operation costs​

Report machine, VM, Release. Use the orders of magnitude.

operationcost
fused loop iterationabout 45 ns (about 9 ns on the reference machine)
script function call in a tight loop0.44 µs; a method call about 1 µs
warm execute of a repeated one-line string0.60 µs (0.38 µs as a jaibite)
warm execute of a string never seen before, one line8.4 µs: parse 3.8 µs, compile about 1.5 µs
warm execute of a 6-line function script, as a jaibite1.69 µs
30-line class-and-loop script, warmabout 241 µs, nearly all class redefinition
engine creation plus first trivial executeabout 90 µs
stdlib registrationabout 311 µs

Compilation costs about a third of parsing and is cached with the parsed source, so a repeated script pays both once. The automatic jaibite disk cache makes file loads IO-bound: a 535-line game file's load-and-run drops from 5.8 ms to 2.4 ms.

What each aliasing strategy costs​

One computation over a mid-sized object (a string, an 8-int array, 4 scalars), VM, µs per call, report machine:

strategyµs/callagainst &
read-only, Creature c by value6.61.4×: the copy, about 1.8 µs here, grows with the payload
read-only, Creature& c4.81.0
read-only, shared_ptr<Creature> c4.9tie
mutate through Unit& or shared_ptr<Unit>1.4tie
method call on a value, an auto& or a shared_ptr4.7tie: the holder does not matter

On the 15-node BST, & parameters run 1.35× faster than returning and reassigning a shared_ptr, and both about 3.5× faster than by value.

What the debugger costs​

The 1000-iteration hot loop, VM, report machine, µs per run:

configurationµs
no debugger constructed47
controller constructed, no session46
session armed, no breakpoints50
session armed, breakpoint in a cold file51
session armed, breakpoint in another file on the hot statement's line number64

The debugger is free until a session arms, then costs about 10%. The last row is the worst case: one string compare per line-number collision.

Measuring yourself​

  1. Release only. A Debug build of the engine runs script 25 to 40 times slower.
  2. Pin to one core, interleave the builds you compare, take the median of five or more, and check that every run prints the same result hash.
  3. Time the workload inside the script after a warm-up, so start-up and compilation stay out.
  4. Distrust small deltas. A change smaller than a relink's spread (2% on most programs) is code layout.
toolwhat it gives
jaiscript_tests "Performance Benchmarks" --verboseJaiScript's own microbenchmarks, in whole µs. Configure with -DJAISCRIPT_ENABLE_BENCHMARKS=ON (docs/BUILD.md).
jai_gloom --smoke, jai_demoreel --smokeeach game headless, ms per tick or frame beside its hash (flags in each example's README). DOOM, GLOOM, the demoreel and Gator Quest all ship in examples/.
-DJAISCRIPT_VM_COVERAGE=ONa diagnostic build that counts every instruction by function and source line, and why the compiler left it on the general path. engine::vm_coverage_report(top = 25) ranks the reasons with their top sites, vm_coverage_summary() returns the totals as numbers, vm_coverage_reset() drops a warm-up, and the runner's --coverage=FILE writes the report at exit. engine::vm_coverage_available() says whether the build counts; without it the report and reset calls throw std::logic_error.

The ports of these programs to Lua, Squirrel, Luau and ChaiScript and the harness that drives them are not part of the JaiScript package, so the package reproduces the JaiScript column of every table and not the others.