Scripting language benchmarks

Goblin Core embeds six independent scripting interpreters. Each is reached through its own command prefix, has its own VM and its own precompiled script cache, and shares nothing with the others but the key space (one Store):

Interpreter Language Commands
PUC-Lua 5.1 the dialect real Redis scripts target EVAL · EVALSHA · SCRIPT
Luau Roblox's typed, sandboxed Lua LUAU.EVAL · …
Wren a small class-based language WREN.EVAL · …
Jim Tcl a small embeddable Tcl TCL.EVAL · …
MicroPython a lean Python UPYTHON.EVAL · …
QuickJS JavaScript (quickjs-ng) QUICKJS.EVAL · …

Every engine precompiles: SCRIPT LOAD (or the first EVAL) compiles the script once and caches the compiled artifact — Lua/Luau bytecode, QuickJS bytecode, a Jim script object, a MicroPython function, a Wren function handle — so EVALSHA executes with no re-parse or re-compile. The full command surface is in docs/commands.

Methodology

Both benchmarks drive one server with a single Python connection, via the bundled drivers (benchmarks/cad_benchmark.py, benchmarks/leaderboard_rescore_benchmark.py). A single Python connection is client-bound, so these are relative comparisons — read the ratios, not absolute peak throughput (for absolute server numbers a C load generator is needed, as in BENCHMARKS). Every script is SCRIPT LOAD-compiled before timing and run by EVALSHA, and each driver verifies all six engines return identical output before timing. Numbers are from a developer machine and are illustrative; reproduce with the commands shown. µs/op, lower is better.

Workload 1 — compare-and-delete (a trivial script)

The Redlock unlock idiom — if GET key == token then DEL key — two commands, no branching or allocation, so this measures per-call overhead, not compute. Four numbers per row: sequential (one request at a time) and pipelined (256 in flight), each over TCP loopback and a Unix domain socket. For context: the native GOBLIN.CAD command and the naive, racy, non-atomic GET+DEL.

implementation RTT TCP seq UDS seq TCP pipe (256) UDS pipe (256)
GOBLIN.CAD (native C++) 1 19.7 9.9 1.5 1.4
WREN.EVAL 1 23.2 10.8 2.5 2.5
TCL.EVAL 1 22.9 10.6 2.6 2.6
UPYTHON.EVAL 1 23.1 10.9 2.8 2.7
LUAU.EVAL 1 22.7 10.8 2.7 2.8
QUICKJS.EVAL 1 22.6 11.7 3.2 3.1
EVAL (PUC-Lua 5.1) 1 22.6 11.7 3.2 3.2
GET + DEL (client-side, racy) 2 40.1 20.3

On this trivial op every interpreter clusters within ~30% of the native command once pipelined (2.5–3.2 µs vs 1.4) — per-call VM entry, not script execution, dominates. The Unix socket roughly halves sequential latency vs TCP (~10 vs ~20 µs); pipelined, the transport stops mattering. The naive GET+DEL is ~2× any atomic form on both transports (its second round trip), and it is racy — another client can change the key between the read and the delete.

python3 benchmarks/cad_benchmark.py --goblin-bin build/goblin-core --keys 20000 --rounds 9

Workload 2 — time-decay leaderboard rescore (a heavy script, + a native command)

A read-only real-time rescore: read a 1000-member leaderboard (score = each member's last-activity timestamp), recompute every entry's recency weight, and return the top-k, kept in a bounded insertion-sorted array (no full sort). Three decay modes — LINEAR = 1/(1 + age/hl), EXP = 0.5^(age/hl), STEP = in/out of a window. ~1000 iterations of parse + arithmetic + branch + list ops per call, so this measures the VM's execution speed — and here the same idiom also has a native command, GOBLIN.TD_LEADERBOARD_RESCORE. All implementations return the same top-k ordering per mode. Pipelined over UDS, median of 5 rounds, µs/op:

implementation LINEAR EXP STEP
native GOBLIN.TD_LEADERBOARD_RESCORE 48 49 19
LUAU.EVAL (Luau) 477 479 127
EVAL (PUC-Lua 5.1) 770 774 223
QUICKJS.EVAL (JavaScript) 938 952 164
WREN.EVAL (Wren) 970 971 197
UPYTHON.EVAL (MicroPython) 2456 2454 656
TCL.EVAL (Jim Tcl) 5834 n/a 852

Three things. The native command is ~10× faster than the fastest interpreter (Luau) and up to ~50× the slowest — it skips the interpreter and the ZRANGE copy and streams the board once. The interpreters separate by >10× on this real workload (Luau leads, Jim Tcl trails). EXP's pow is not the bottleneck (EXP ≈ LINEAR everywhere) — per-member interpreter overhead is; STEP is cheapest (a compare, no arithmetic). One engine can't even express EXP: Jim Tcl's minimal expr has no pow/exp, so it does LINEAR and STEP only.

python3 benchmarks/leaderboard_rescore_benchmark.py --goblin-bin build/goblin-core --members 1000 --k 10

Takeaway

The ranking flips between the two workloads. Jim Tcl is near the top on the trivial compare-and-delete (per-call overhead is low) but slowest by far — >10× the fastest interpreter — on the heavy rescore; Luau is middling on the trivial op but fastest on real compute. So pick the interpreter for the shape of your scripts — light glue between commands vs. heavy in-script computation. And where a hot idiom has a native command — GOBLIN.CAD for lock release, GOBLIN.TD_LEADERBOARD_RESCORE for the rescore — it beats every interpreter by ~10× or more, since it skips the VM entirely. Either way every script is precompiled, so EVALSHA never pays for compilation regardless of engine (which for MicroPython would otherwise be ~960 µs per call).

The same two idioms, translated into each language, are on the command pages: EVAL, LUAU.EVAL, WREN.EVAL, TCL.EVAL, UPYTHON.EVAL, QUICKJS.EVAL.