# Performance

## Goal

Gizlang code must be faster than Rust where this costs no safety. Overflow, bounds and stack
checks stay. Bun is the minimum, and nginx is the reference for serving.

This page tells how to measure performance, and gives the rules that the measurements taught.
Each result number has one owner: the published file of its suite, `bench/results/AREA/SUITE.json`
([published results](#published-results)). The site, the roadmap and other documents read it or link to it, and do
not copy it.

## Measurement

### Instructions for each unit of work

Instructions under callgrind are the stable measure on a shared machine. For a request, use:

```sh
bin/giz bench/web/http/run.giz instructions PORT COMMAND...
```

Measure the margin, never a total over a short run. Make two runs of different lengths and
subtract them, so that the startup cost is not divided among the requests. A total once gave
nginx 6,974 instructions for each request. The real cost is 596.

### CPU time for each unit

Read user and system time from `/proc/PID/stat` on one pinned worker. This shows where time
goes when the instruction counts agree. For example, nginx uses approximately 330 ns of user
time for each request, and Gizlang uses 650 ns. Both use 1 to 3 µs in the kernel.

Wall-clock rates only confirm this measure. Take the best of several runs, alternated with
the rival at the same time, on pinned and separate CPUs. The load generator must not be the
bottleneck (`bench/web/lib/load.c`, epoll).

### System calls for each unit

Use `strace -c`. A keep-alive request costs one read and one write, as in nginx. Kernel time
is noise for the language. Extra calls are not noise.

### Profile by owner

Names: `sudo -n perf record -g` (a build host and backup have perf_event_paranoid 4), then
`perf report --stdio --sort symbol | gizlang tools/perf/names.giz BIN` names each `gizlang_methodx<H>`, `gx`, `derivedx`
and `unsharex` symbol from the binary's own `gizlang_framex<H>` string; no special build.

1. Find the self cost of each function.
2. Find the callers of the top entries.
3. Fix the largest owner first.
4. If a change does not decrease the count, revert it, however probable it seems. Examples:
   an `nsw` hint that LLVM had already inferred, and a direct read whose data was present for
   0.1% of the time.

## Rules

Each rule removed measured work from the HTTP path: from 23,200 instructions for each request to
8,776 (2026-10-22, `bench/web/http/run.giz instructions`, one worker). On one core, nginx uses
6,709 user instructions and the kernel 30,253; Gizlang serves 2.5 to 4% more requests (bench/web/http
README). Each rule applies to all code that moves data.

1. **Copy one time.** Data goes from the kernel into the bytes of the program in one copy.
   Parse in place and keep positions. Cut a string only for a value that the caller receives
   (a method, a target, a header name and value). Transform in place when the source is yours
   (header names are changed to lower case in the read buffer). Compare in place (the HTTP
   version is checked in the buffer, never cut).
2. **No work for the absent case.** Use one scan for a set of bytes (`bytes.indexOfAny`), not
   one scan for each byte. Equality compares the length before it reads bytes. Trimming is a
   walk of an index, not a new string.
3. **Try the common case first, and measure it.** A write goes directly to the socket
   (`uv_try_write`). Only a remainder goes into the queue. Thus nearly every response needs no
   copy, operation or task switch. Keep a fast path only when its measured hit rate is high.
4. **Do not wrap hot results.** `Result<Option<T>>` makes and drops two layers for each call.
   When a type has a value that cannot occur (an empty start line, an empty method), use that
   value as the "nothing here" answer.
5. **Allocate rarely, and from size classes.** Runtime and program buffers come from size
   classes with free lists for each thread (`runtime/memory.c`). The capacity of a buffer is
   its whole class, so appends fill it in place before it grows. A connection uses its read
   buffer again. A response is built in one buffer and written one time. A thread gives back its
   memory before it waits (`gizlang_memory_idle`): the blocks other threads freed for it, its
   empty pages, and glibc's freed large blocks. Before, a KV server held 212 MB of pages for 19 MB
   of blocks.
6. **Borrow by default.** A read of a value takes no reference. A binding of a place borrows
   when nothing writes the place while the binding lives. Only consumers (a stored field, an
   element, a returned value) take a reference.
7. **Tell the optimizer what the language guarantees,** and do not check it again.
   - The status is clear on entry, and no other pointer reaches it (`llvm.assume`, `noalias`).
     Thus status checks on paths without calls fold away.
   - A function outside every call cycle does not check the stack, because recursion always
     goes through a function that does (7% of a request). A function on a cycle compares the
     address of its frame with the floor of the stack. It does not store a counter on entry
     and restore it on each exit (fib is 22% faster).
   - Failure branches are marked unlikely (`llvm.expect`). Without the mark, each check halved
     the estimated frequency of the code after it. After a few checks, small calls looked cold
     and were not inlined. With the mark, gzip used 17% fewer instructions.
   - After its overflow check, the result of an operation is `add nsw` (`nuw` for unsigned),
     not the field of the intrinsic. Thus ranges flow through it, and the bound of a contract
     removes the index checks that it implies.
8. **Build constants one time.** A Vec literal of constants is an immortal global, as a
   string literal is. Common methods and header names are interned. Thus tables and defaults
   cost nothing for each call, and they compare by address.
9. **Let the optimizer see everything.** A process is one module whose functions are
   internal. Thus small functions and functions with one call are inlined. A runtime operation
   is divided into a small inline common case and an out-of-line remainder (`concat`: owned,
   with space, not an alias; `push`: owned, with space. This decreased the fill of a table
   from 28 instructions for each element).
10. **Use the width of the hardware.** Byte scans compare 16 bytes at a time (SSE2). They never
    read past the end: the last bytes go through a block of zeros. A check that runs on every
    call (thread ownership) is a comparison of an address, not a library call.
11. **Keep the event loop quiet.** A socket stays registered between reads. `TCP_NODELAY` is
    set. Workers share a port with `SO_REUSEPORT` (`GIZLANG_WORKERS`). The backlog is `SOMAXCONN`.
12. **Make the work as large as the input.** A table for each call is as large as the input
    needs, not the maximum of the format. Deflate cleared 512 KB of hash tables for a 2 KB
    response and was ten times slower. Append in order what is indexed by position. Do not
    fill it first.
13. **Serialize typed values field by field.** Data read at startup becomes records. A
    response appends the bytes of each field (`writeString`, `writeNumber`, integers as digits).
    It does not build and walk a generic tree for each request (HttpArena JSON: 6.4 times
    faster). Integers are written with division by the constant 10 (a multiplication), lowest
    digit first. A byte loop checks a short string for escapes: a C set scan built its table
    for each call.
14. **Pay for ownership only when it is shared** (LangArena). The release on each drop and the
    uniqueness check before each element write are inlined (count 1: nothing to do). Free of a
    last reference and copy of a shared buffer are out-of-line calls (`Calculator` 4 times
    faster, NeuralNet 22% fewer instructions). A call to a function that cannot fail (no
    `fail`, `io` or `console` effect, outside every call cycle) has no status check after it.
    A `mut` parameter is `noalias`, so its fields stay in registers across stores.
15. **Keep small blocks together.** Blocks of a maximum of 1 KiB come from 64 KiB pages of one
    size class, each with its own free list (the sharding of mimalloc). When blocks came from
    one list in free order, a tree built after many unrelated frees was 2 to 4 times slower.
16. **Hash one word at a time, and look up one time.** Strings and bytes hash eight bytes for
    each multiplication (the folded 128-bit product of wyhash, `mul_high`). A hash of one byte
    at a time waits on its multiplication. `Map.merge` updates or inserts in one probe (the
    `entry` of Rust). When the table grows, only the slot array is built again (word counts
    23%, n-grams 55% fewer instructions).
17. **Use threads for independent work.** `Task.parallel(n, body)` runs on a pool of process
    threads, one for each CPU. The body can only compute and fail. What it captures is marked
    shared (the multi-threaded marking of Lean 4: counts become atomic one time, and the walk
    visits only what is not shared yet).
    - 200 parallel queries over a captured Vec of one million rows take 3 ms, not 542 ms for
      a freeze and thaw on each call.
    - Matmul at n=600 takes approximately the same time on 1 thread, and less on 4 and 16
      threads (80-103 ms and 63-77 ms, before 149-184 ms and 79-86 ms).
    - Matmul on 1, 4, 8 and 16 threads: 3.1, 1.0, 0.53 and 0.43 s.
    - The cost is contention on buffers that every thread copies. 64 bodies that each push
      500,000 references to the same 64 strings take 1.1 s. With frozen counts, LLVM removes
      the counting (19 ms).
18. **Lay out values as tightly as Rust.** The cases of a union share storage. An AST node of
    four cases went from 88 to 40 bytes. Before, a tree walk missed the last-level cache 300
    times as often as Rust (Calculator 2.0 s to 0.7 s). Dup and drop examine only the active
    case. A record of more than six words passes by pointer, as `&T`. A local or element passes
    where it is. A pattern binding of a place that is not written borrows its field.
19. **Copy shared buffers one time, outside the loop.** A `p.f[i] = x` in a loop checks the
    uniqueness of `p.f` before the loop, when nothing in the loop can share `p.f`. An
    out-of-line call copies a shared buffer and returns the new one. A page whose blocks are
    all free starts again at its beginning. Thus a phase that frees everything uses memory
    again in address order (Calculator after a graph phase: 1.77 s to 0.67 s).
20. **Use eight bytes for each step where the format allows it.** Decimal digits convert
    eight at a time (SWAR, as in fast_float). The end of a JSON string is found eight bytes at
    a time. `readU64LE` is one check and one load. A length is known to be non-negative, so
    contracts on it fold index checks. Sorting keeps existing runs (natural merge sort) and
    merges without branches. Base64 uses AVX2 when it is available (Muła-Lemire), and is faster
    than the Rust crate. A Gizlang version took 33/38 ms for 30 MB against 9/7 ms, so base64
    went back to C (the rule: within 1.3 times). The base64 entry of `bench/compiler/perf` tracks the
    scalar gap (2.2 times C). Refer to [RUNTIME](runtime.md#base64-in-c).

## Rules for new code

- A library function on a hot path states its cost: what it allocates and what it copies.
- Inside a function, prefer positions and borrowed views. Make owned values at its edge.
- Before you add a cache, a fast path or a special case, measure what it saves on the
  benchmark that owns it. Put the measurement in the commit message.

## Published results

A benchmark suite is `bench/AREA/NAME/suite.giz`, a program that ends in `Bench.main` (`gizlang:bench`). It names its
items, subjects (Gizlang and what it is compared with), groups (one table each, with its method) and goals.

```sh
bin/giz bench list                          # the suites and their published files
bin/giz bench run SUITE [--size quick]      # measure; print beside the published values
bin/giz bench compare SUITE/ITEM            # one row
bin/giz bench record SUITE [ITEM...]        # a full run, merged into the published file
```

- `record` writes `bench/results/AREA/SUITE.json`, with the date, machine, build and size of each row. A
  row with source `migrated` came from a README before its suite existed; the site marks it.
- gizstack.com shows the files only through `:::bench{suite=...}`, `:::stats{items="SUITE/ITEM ..."}`,
  `:::goals{suites=...}` and the value `bench.SUITE.ITEM.SUBJECT` in double braces in prose (`stack/site/bench.giz`;
  `/` in a suite id becomes `-`). `tests/stack/site/bench.test.giz` fails when a page or doc writes a published value
  of three or more digits by hand.
- A release ships each suite that imports only `gizlang:` modules and its own folder, with the published files, under
  `share/gizlang/bench` and `share/gizlang/results` (`tools/cli/bench.giz` pack), so a user measures again with no
  product source. A suite that imports a product source stays in the checkout, and its tables say so.
- A bench README holds the method and dated analyses. It does not repeat a published number.

## Benchmark rules

These rules apply to every folder in `bench/`. The owner team of a folder applies them to its
folder.

| Folder | Owner |
| --- | --- |
| `bench/db`, `bench/db/kv` | database |
| `bench/fs` | fs |
| `bench/web`, `bench/web/http`, `bench/web/httparena` | web |
| `bench/stack/builder` | GizStack |
| `bench/compiler/agents`, `bench/compiler/regressions`, `bench/compiler/langarena`, `bench/sdk/hot`, `bench/compiler/perf`, `bench/runtime/tasks`, `bench/compiler/live` | language |

1. **Keep what a result cites.** Keep a benchmark when `bench/AREA/README.md` or a document
   cites its result. Also keep its comparison sources in other languages (C, Rust, TypeScript,
   Go, Java, Next.js, Svelte, Solid) and the command that reproduces the result. A published
   number is only in its results file ([published results](#published-results)).
2. **Remove the rest.** Remove a benchmark that no result cites, a benchmark that does the same
   work as another, a copy of library code, a description of a file that does not exist (for
   example `run.py`), and an old result. Before you remove a benchmark, make sure that the
   results file or the README keeps the results that it gave.
3. **One harness.** Tests and benchmarks use one harness module for processes, ports, scratch
   folders and resource readings. Find descendant processes in one way (through `/proc`), not
   with `ps` or `pgrep`.
4. **No imports between `tests/` and `bench/`.** Neither imports the other. A fixture that both
   use belongs to one owner module: the package under test, or a fixture module that both
   import.
5. **Every benchmark builds in the tests.** `tests/bench/run.giz` checks that each benchmark
   program builds, so that a benchmark cannot go stale without a failed test.
6. **One comparison per answer format.** Gizlang code compares answers. A program in another
   language is not kept: a Gizlang client speaks the other system's protocol (for example
   `tests/db/oracle/golden.giz`, which collects answers from Oracle over TNS).
