Sisu

How fast is Sisu?

Eight small programs, each written with the same algorithm in Sisu, C, Rust, Go, Java and Python. The numbers show where Sisu stands today, not how fast it will be.

Intel Core i9-14900K under Linux 7.0 · The commit and date are named below the table · Prose checked against the repository at commit ae95ed5

Results

Wall time in seconds, the median of 9 runs. Shorter is faster. Each chart has its own scale. Python takes too long to draw on the same scale as the rest, so its time is given as text.

BenchmarkSisuCRustGoJavaPython
fib0.250.180.160.270.274.71
sieve0.180.190.170.200.242.27
binary_trees0.490.620.731.340.3618.66
hello0.000.000.000.000.010.00
hello, build0.0520.0660.0820.0630.185-
quicksort0.560.530.540.550.6412.85
nbody0.130.140.110.230.2011.23
mandelbrot0.580.580.580.590.6117.18
fannkuch_redux0.120.110.120.120.162.76

All ran at commit 6ff7970 on 2026-10-25, on an Intel Core i9-14900K under Linux 7.0 (Ubuntu 24.04, glibc 2.39). Each figure is the median of 9 runs, pinned to one CPU. Each table runs from fastest to slowest wall time, with Sisu in bold. The machine was quiet: its load average was below 2, on 32 CPUs, at the start and at the end of the run.

Wall time in seconds, the median of 9 runs. The hello, build row gives build time instead. The full tables, with CPU time, peak memory, binary size and build time, are in docs/benchmarks.md.

These numbers come from one machine, at the commit named above. They change as the compiler changes, and they are rerun by hand when a change should move them.

Where the gaps come from

Each gap was traced with callgrind and hyperfine, and by removing one cost at a time, by hand, from the program's LLVM IR.

binary_trees: the allocator and the counting

Sisu takes 0.79 times as long as C here, because it allocates with mimalloc and C calls glibc's malloc (ADR 0031). Callgrind counted 102 instructions in Sisu's allocator for each allocation and free, against 181 in glibc's, and 11.2 billion instructions for the whole program against C's 16.1 billion. What Sisu pays that C does not is mostly reference counting: a store when an object is made, and a load, subtract and store when it is released. ADR 0005 accepts that cost.

Java is fastest on this program. Its collector allocates by bumping a pointer and frees short-lived objects in bulk. It pays for that with five times the memory Sisu holds.

quicksort: the checks

Each element write checks the index against the bounds and the array's count against 1, and each addition checks for overflow. The checks are the whole gap to C. Removing the overflow checks by hand made no measurable difference.

A loop now makes the arrays it writes unique once, before it starts (ADR 0021). In a separate session on a busy machine, that took the fastest run from 690 ms to 640 ms, against C's 610 ms. The bounds checks remain: LLVM cannot prove the index is in range.

hello: the link step

Lexing, parsing and checking hello take 1.4 ms, timed with hyperfine on one pinned CPU after a warm-up. The whole sisu build -O2 build takes 27.6 ms, and the link through cc alone takes 21.9 ms of that. The bench script neither pins nor warms up its builds, so its 0.052 s reads higher. A faster edit-to-output loop starts with the link, not the compiler.

How it is measured

One script, scripts/bench.sh, builds each program in every language and times it. make bench runs it.

  • Each program uses the same algorithm in every language, written plainly, with no tuning for one language.
  • Each figure is the median of 9 runs. taskset pins every run to one CPU, so Go and the JVM see a single CPU, and the JVM picks its serial garbage collector.
  • Wall time is elapsed time. CPU time is user plus system time, so a garbage collector or JIT thread counts too. Both come from /usr/bin/time.
  • Every process runs whole, so startup counts: JVM startup and JIT warm-up for Java, interpreter startup for Python.
  • The inputs are constants in the source. No compiler folded a program to its answer: the run times grow with the input.
  • The checks differ. Sisu checks integer overflow and array bounds. Rust, Go and Java check bounds but not overflow. C checks neither.
  • A Sisu binary links its runtime, written in Rust, statically. That accounts for most of its size.
LanguageToolchain and flags
Sisusisu build -O2, LLVM 23
CUbuntu clang 18.1.3, -O2
Rustrustc 1.99.0, -C opt-level=3
GoGo 1.27.2
JavaOpenJDK 25.0.4.1 (Temurin)
PythonPython 3.14.5

What these numbers cannot tell you

They come from one x86-64 machine, on small single-threaded programs. They show where Sisu stands, not how fast it will be.

Run them yourself

The script builds sisu in release mode, runs each program in every language whose toolchain is on PATH, and skips the rest. If mise installs Go and Java, put them on PATH with mise exec go java -- make bench. It prints the tables with the commit, date and machine.

terminal
$ make bench                       # all benchmarks
$ make bench BENCH='fib sieve'     # some of them
$ RUNS=10 BENCH_CPU=4 make bench

Regression tracking

The tables above compare Sisu with other languages. CodSpeed does something else: it compares Sisu with itself, change by change, so a slowdown shows on the pull request that causes it.

The CodSpeed workflow runs on every push to main. It also runs on a pull request that changes any of these paths:

  • crates/** and bench/**
  • Cargo.toml, Cargo.lock, rust-toolchain.toml and .cargo/config.toml
  • the LLVM install: .github/actions/install-llvm/** and scripts/install-llvm.sh
  • the benchmark setup: codspeed.yml, codspeed-main.yml, scripts/codspeed-prep.sh and the workflow file

It runs in CodSpeed's simulation mode, which counts instructions rather than timing the run. It measures two things:

  • Seven of the programs above, all but hello, at reduced inputs, and six programs that only CodSpeed runs, in bench/features/: strings, enum_match, shared_arrays, compare, tree_rewrite and interp.
  • The compiler itself: an -O2 build of bench/hello, a sisu check and an -O2 build of bench/binary_trees, a sisu check of a generated 200-section input that uses most of the language, and an -O2 build of a 20-section one. Pushes to main also build the 200-section input, from codspeed-main.yml.

Before it measures, scripts/codspeed-prep.sh builds every benchmark and checks what each one prints. CodSpeed comments on a pull request when a result changes.

The check is informational: it never turns a pull request red. On a push to main, a failed step fails the run, so the benchmarks cannot stop running unnoticed. To compare a change before you push, make callgrind counts the instructions of sisu check on the large input, as CodSpeed does.