Issue #860: Hello World starts faster than Python’s in every way a program is started

Open and answer this issue on GitHub.

ready · opened 2026-10-26 · updated 2026-10-26 · 0 comments

What this is about

print("Hallo Welt") is the smallest program there is, so what it costs is pure overhead: nothing in it computes. Python is the usual yardstick for a scripting start. The goal of this issue: a Nikaia Hello World costs fewer instructions than Python’s Hello World, in every way a program is started.

fn main() {
    print("Hallo Welt")
}

A program is started in four ways, and each has its own overhead:

# Scenario What happens
1 a built binary (nikaia build, then run target/nikaia/bin/<name>) the process starts, nothing else
2 nikaia run f.nika, warm (same file, unchanged, second run) read, check, lower, look the compiled program up in the cache, start it
3 nikaia run f.nika after an edit the same, plus the compile of the changed Rust
4 nikaia run of a program never seen the same as 3 in a fresh cache entry, and the first run on a machine also fills the bundle (run_bundle)

What we see today

Rough shell timings from one session (a loop of runs on a shared machine, so an indicator only, not a measurement in the sense of this repository; the figures below are the method, not numbers to quote):

A run of nikaia run is a program too, so scenario 2 is the one a user meets most (every edit-run loop, every script).

Why it matters

A language that wants to be used for scripts and tools (scripts/*.nika is the rule, CLAUDE.md) is judged by what nikaia run script.nika costs when the script does nothing. Today a Python script starts faster, which is the wrong way round for the stated purpose.

How it is measured

By CLAUDE.md’s rule: instructions and mispredicted branches from callgrind, never seconds; no figure in seconds goes into a document. The comparison is Python’s print("Hallo Welt") against Nikaia’s, both counted by valgrind --tool=callgrind --branch-sim=yes, children included (--trace-children=yes, since nikaia run starts the program and a compiler probe). Same output first, byte for byte. benches/brc/brc.sh is the pattern; the result is a small table in a README of its own under benches/, replaced when re-measured.

To build

  1. A benchmark, benches/hello/: a script that counts instructions for the four scenarios and for Python, checks the outputs are equal, and prints the table. It is the acceptance test of this issue and the baseline for the steps below.
  2. Attribute scenario 2 (callgrind_annotate): which share is the rustc --version child, which the front end, which the cache lookup and the start. Understand it before changing anything. Check the hash in the cache directory name (what goes in).
  3. Take rustc --version off the warm path. Ask for the version once per toolchain, not once per run: resolve the toolchain’s own rustc (as run_fast::compile already does for the compile) and key the cache on what is cheap to read (the toolchain’s path and modification time, or a stamp file next to the cache entry), asking the compiler only when that stamp changed. rustc_version() is also used for the check of the installed version (installed_rustc); keep that answer, from the same stamp.
  4. Shorten the front end on a warm run. Where the source and the nikaia binary are unchanged, check and lower need not run at all: a stamp of (source, dependencies, compiler version) on the cache entry says the compiled program is current. Lowering from cache is already measured as a part; the question is how little a hit has to do. Diagnostics (warnings the program would show) are replayed from the stored messages, as run_fast already does.
  5. Start the cached program without a detour: exec it instead of spawning and waiting where the platform allows, so the process that remains is the program itself.
  6. Scenario 1: build nikaia build’s Hello World with the release profile and with the least start-up work the language adds (no initialisation that a program that uses nothing does not need). Compare its count with Python’s and with a C puts as the floor.
  7. Scenarios 3 and 4: measure what a one-line edit and a new program cost after steps 3-5, and decide in the table whether they need steps of their own (compile flags, the bundle, an entry reused for a changed file instead of a new directory). Each is its own To build entry here, or its own issue, depending on what the counts say.
  8. Pass criteria: the table of step 1 shows Nikaia below Python in scenarios 1 and 2, and says in the README what scenarios 3 and 4 cost and why. If a scenario cannot go below Python, the README says what its floor is and what holds it there; this issue then stays open with a comment naming exactly that.

The commit that builds the last step says Fixes #N. A change to crates/ raises the version (the supervisor’s, see CLAUDE.md); the benchmark under benches/ does not.

Back to Open Issues