Sakura Lang

Benchmarks

Instructions retired per call for every benchmark program on the reference Mac, how they are counted, and what they do and do not say.

The blessed baseline

These are instruction counts from one run of the benchmark suite on the reference Mac (an Apple silicon MacBook, host d88970189598) at commit 320c9980, 2026-10-19. They are blessed as the baseline (2026-10-20, bench/baselines/d88970189598.json), and a counted check of the v0.1.0 candidate on fib and shapes passed against it on every Node cell. Later releases are compared with these counts. They are counts, not times: no speed claim follows from them. The cross-language envelope in design §17.109 is not met by these counts, and Sakura Lang makes no cross-language speed claim at 0.1.0 (design §17.265).

The benchmark suite runs the same workloads written in Sakura Lang and by hand in JavaScript, Rust, Go, OCaml and Koka. Every program has an oracle: a cell is measured only after it returns the recorded checksum for every input. The numbers are specific to these workloads and this one machine. They are not a language ranking.

What is measured

The primary metric is instructions retired per call, read from macOS's /usr/bin/time -l. As with rustc-perf, the reason is repeatability: the count barely moves when other programs share the machine, so the suite runs while other work goes on and needs no idle night (design §17.264).

How bench/bench run --instructions measures one cell (design §17.264). Start-up cancels in the difference.calibrate k: double k until k more calls retire 2³² instructions. Then run(n) × 1 or run(n) × (1 + k). run(n) × 1: a fresh process: start-up, loading, the first call. Then (many − one)/k. run(n) × (1 + k): a fresh process: the same, then k warm calls. (many − one)/k: instructions retired per warm call. median of 5 pairs: reported with its spread, (max − min) / median.calibrate kdouble k until kmore calls retire2³² instructionsrun(n) × 1a fresh process:start-up, loading,the first callrun(n) ×(1 + k)a fresh process:the same, then kwarm calls(many − one)/kinstructionsretired per warmcallmedian of 5pairsreported with itsspread, (max −min) / median

The counts come from macOS's /usr/bin/time -l: every thread of the one process it runs, none of its children.

How bench/bench run --instructions measures one cell (design §17.264). Start-up cancels in the difference.

Each cell is one program at one input size n on one engine. The harness calibrates a call count k, then runs five pairs of fresh processes: one calls run(n) once, the other 1 + k times. Start-up, module loading and the first call's compilation are in both, so the difference over k is the cost of a warm call. The cell is the median of the five pairs, and its spread is (max − min) / median.

An instruction count is not a time. It does not see cache misses, branch mispredictions or how many instructions a core retires per cycle, and two engines can retire the same instructions at different speeds: in the first counted runs one Wasmtime cell's ratio of counts and its ratio of times differed by half. A ratio below is a ratio of counts, never of times.

Results

Every value is instructions retired per call (or per process, for the compiler) on the reference Mac at commit 320c9980, at each program's largest size. A ratio is Sakura's count over hand JavaScript's on the engine named. K, M and G are thousands, millions and billions. Node runs with V8's --predictable flag, which keeps its garbage collector and compiler on the main thread so that their counts do not depend on scheduling.

JavaScript backend

ProgramnSakura, Nodehand JS, NoderatioSakura, Bunhand JS, Bunratio
binary_trees14957 M680 M1.41×1.28 G503 M2.54×
contracts1,000,000119 M44.3 M2.69×97.5 M54 M1.81×
contracts_plain1,000,00072.4 M24.2 M2.99×47.4 M32 M1.48×
countdown300,00016 M10.2 M1.57×5.43 M5.15 M1.05×
deltablue10,0001.92 G317 M6.06×2.74 G441 M6.23×
fib30191 M137 M1.39×212 M91.5 M2.31×
handled_d11,000,00050 M57.1 M0.88×21.1 M18.4 M1.14×
handled_d161,000,00050.3 M57.1 M0.88×21.1 M18.4 M1.14×
handled_d41,000,00050 M57.1 M0.88×21.1 M18.5 M1.14×
handled_fwd1,000,000373 M76.2 M4.90×452 M38.8 M11.63×
handled_reentry1,000,000548 M782 M0.70×627 M87 M7.21×
handled_suspend1,000,00093 M86.1 M1.08×26.3 M124 M0.21×
json_validate30,000791 M569 M1.39×613 M204 M3.01×
map_set100,0003.18 G968 M3.28×2.86 G744 M3.85×
map_set:int100,000840 M135 M6.20×865 M65.4 M13.23×
map_set:string100,000980 M433 M2.26×927 M428 M2.17×
map_set:tuple100,0001 G366 M2.75×1.1 G238 M4.63×
nbody100,0001.34 G113 M11.90×1.24 G140 M8.89×
pipeline300,00087.2 M302 M0.29×73.2 M27.7 M2.65×
rbtree100,0002.63 G1.05 G2.52×1.53 G619 M2.47×
shapes100,0001.21 G540 M2.24×869 M379 M2.29×
sieve200,00080.5 M17.4 M4.62×87.6 M12.5 M6.98×
spectral_norm400755 M168 M4.49×485 M203 M2.39×
tail_clique10,000,000293 M334 M0.88×141 M176 M0.81×
tokenizer100,000159 M526 M0.30×222 M190 M1.17×

WebAssembly backend

Sakura compiled with --target wasm, run on Node's WebAssembly engine and on Wasmtime, over the same hand JavaScript on Node. json_validate has no Wasmtime cell: it needs JSON decoding, which only the Node host implements.

ProgramnSakura WASM, NodeSakura WASM, Wasmtimehand JS, Noderatio, Noderatio, Wasmtime
binary_trees144.1 G4.82 G680 M6.03×7.09×
contracts1,000,0003.02 G4 G44.2 M68.29×90.47×
contracts_plain1,000,0001.66 G2.04 G24.2 M68.77×84.29×
countdown300,00019.7 M19.2 M10.2 M1.94×1.89×
deltablue10,00033.6 G39.5 G318 M105.70×124.48×
fib30270 M259 M137 M1.96×1.88×
handled_d11,000,0001.27 G1.6 G57.1 M22.30×28.11×
handled_d161,000,0001.25 G1.6 G57.1 M21.95×28.11×
handled_d41,000,0001.27 G1.61 G57.1 M22.30×28.13×
handled_fwd1,000,0006.16 G7.5 G76.3 M80.83×98.42×
handled_reentry1,000,0008.77 G10.6 G782 M11.21×13.57×
handled_suspend1,000,0001.43 G1.84 G86.3 M16.58×21.33×
json_validate30,00013.6 Gabsent570 M23.94×—
map_set100,0007.36 G8.33 G972 M7.57×8.57×
map_set:int100,0002.25 G2.57 G136 M16.52×18.87×
map_set:string100,0002.56 G2.98 G434 M5.91×6.87×
map_set:tuple100,0002.66 G2.94 G366 M7.26×8.03×
nbody100,0005.38 G6.68 G113 M47.58×59.15×
pipeline300,0001.85 G2.3 G302 M6.13×7.61×
rbtree100,0007.84 G10.1 G1.05 G7.48×9.60×
shapes100,0003.61 G4.33 G542 M6.66×7.99×
sieve200,000894 M1.13 G17.4 M51.28×64.69×
spectral_norm4005.58 G7.18 G168 M33.17×42.66×
tail_clique10,000,0001.22 G1.16 G333 M3.67×3.49×
tokenizer100,00018.1 G18.5 G526 M34.51×35.27×

Native controls

The same workloads in Rust (with overflow checks, the matched lane), Go, OCaml and Koka. A dash means the program has no port in that language.

ProgramnRustGoOCamlKokaKoka, no reuse
binary_trees141.29 G806 M206 M306 M305 M
contracts1,000,0005.38 M15.8 M———
contracts_plain1,000,0005.13 M10.5 M———
countdown300,0001.13 M2.4 M———
deltablue10,000171 M203 M———
fib3031 M41.8 M———
handled_d11,000,00011 M17 M—148 M148 M
handled_d161,000,00011 M17 M—148 M148 M
handled_d41,000,00011 M17 M—148 M148 M
handled_fwd1,000,00011 M18 M—256 M256 M
handled_reentry1,000,00014 M23.3 M—174 M174 M
handled_suspend1,000,00016 M20 M—245 M245 M
json_validate30,000271 M689 M———
map_set100,000907 M887 M1.82 G7.16 G7.17 G
nbody100,00040.9 M78.7 M———
pipeline300,0007.69 M17.8 M358 M49.4 M60 M
rbtree100,000863 M1.19 G700 M340 M340 M
shapes100,00033.8 M270 M1.08 G339 M337 M
sieve200,0003.58 M4.51 M———
spectral_norm400112 M125 M———
tail_clique10,000,000150 M233 M———
tokenizer100,0006.4 G21.3 G———

Compiler

One sakura process per run, after a warm-up: check and build of generated packages and of a new project. test_hello counts the CLI's own process, not the Node process it starts.

CaseLinesInstructions per processSpread
check_hello1170.9 M0.28%
test_hello11113 M1.52%
check_cold_10k10,084641 M0.16%
build_10k10,084682 M0.19%
check_cold_100k100,8045.78 G0.41%
check_wide_match9,012701 M0.24%

Against the targets

Design §17.109 set a ceiling per program on the ratio of Sakura to hand-written JavaScript on the same engine, and asked for a median of at most 1.5 over the fifteen programs. Those are same-engine ratios, so a ratio of counts can be read against them (design §17.265). At the largest size, six of the fifteen meet their ceiling on Node and five on Bun; the median is 2.24 on Node and 2.47 on Bun.

ProgramCeilingNodeBun
fib1.51.39×met2.31×miss
countdown1.31.57×miss1.05×met
sieve1.54.62×miss6.98×miss
nbody1.311.90×miss8.89×miss
spectral_norm1.34.49×miss2.39×miss
binary_trees1.51.41×met2.54×miss
pipeline2.00.29×met2.65×miss
rbtree1.52.52×miss2.47×miss
map_set3.0 (Bun 5.0)3.28×miss3.85×met
shapes1.52.24×miss2.29×miss
tokenizer1.50.30×met1.17×met
json_validate2.01.39×met3.01×miss
deltablue2.06.06×miss6.23×miss
handled (depth 1)2.00.88×met1.14×met
contracts2.02.69×miss1.81×met

Read plainly: on the JavaScript backend, most of the suite runs within 1.5–3× of hand-written JavaScript, and five programs are 4.5–12× behind: nbody, deltablue, map_set:int, handled_fwd and sieve. Where Sakura is ahead (pipeline, tokenizer, handled on Node) the cause is a specific mechanism, fusion of the pipeline, ownership of the consumed string, or a handler compiled to a direct call, and it says nothing about other programs. The WebAssembly backend is first-release code: 2–124× behind hand JavaScript by count on this suite, far from native, with its worst cells on contracts, handlers and scalar loops. The native backend is experimental, left out of release binaries, and not measured here.

Design §17.109 also set an envelope against Rust, Go, OCaml and Koka. Those ceilings were set on wall time, and a count across different engines is not a time, so the envelope is reported as information and not as a release gate (design §17.265). On these counts it is not met: Sakura WebAssembly on Node is a geometric mean of about 27× Rust and 25× Go on the numeric kernels, 11× OCaml and 12× Koka on the allocation kernels, with tokenizer the one kernel inside its ceiling. The native backend, experimental today, is where that comparison will be made again.

What comes next, in order, each landing with a counted before and after against the blessed baseline: flat Float, Bool and Int lists and in-place Float records (nbody, spectral_norm, sieve); the Int-key path of Map (map_set:int); what remains in deltablue; forwarding handlers (handled_fwd); then the WebAssembly cliffs, starting with contracts and the superlinear tokenizer cost (design §17.265 (6)).

Spread

The spread is (max − min) / median over a cell's five pairs.

EngineCellsMedian spreadCells over 1%
Node, JavaScript backend1980.14%4
Node, WebAssembly backend and its hand JS1980.30%12
Wasmtime950.11%1
Native (Rust, Go, OCaml, Koka)2820.09%26
Bun1981.35%116
Compiler processes60.28%1

On Node and Wasmtime every cell over 1% is one pair out of five; the middle three agree within 1%, so the median holds. Most native cells over 1% are Go, whose garbage collector runs concurrently. Bun is noisier: 48 of its cells spread over 1% even in their middle three, and JavaScriptCore's concurrency switches did not narrow them. Read a Bun count with its spread.

Reproduce

On a Mac with the toolchains pinned in bench/toolchains.toml:

git checkout 320c9980
bench/bench run --suite js --instructions --json js.json
bench/bench run --suite wasm --instructions --json wasm.json
bench/bench run --suite native --lane matched --instructions --json native.json
bench/bench compile --instructions --json compile.json

Counts from another machine are not comparable with these: a different CPU, OS or engine build retires different instructions. The full result files, with every size, every raw count and the wall time beside it, are in bench/results (2026-10-19-320c9980-*). The procedure that turns a run into a baseline is docs/release.md §2.

Edit on GitHub

Last updated

On this page