# Builds, backends and performance

> The dev, release and debug profiles, the native x86-64 and AArch64 backends and the C backend, build caching, compile speed, and how Brine's run time compares with C.

Source: https://docs.brinelang.org/tooling/builds-and-backends/

Brine has three build profiles: **dev** (the default, built for a fast edit-run loop), **release** (`--release`, optimized through a C compiler) and **debug** (`--debug`, for a debugger). On x86-64 Windows and Linux and on AArch64 Linux, dev and debug builds go straight to machine code, so the whole 34,000-line compiler builds itself in under a second; release builds go through gcc or clang and run at about the speed of hand-written C. Every profile keeps every runtime check the prover couldn’t remove: there is no unchecked mode.

## Profiles

[Section titled “Profiles”](#profiles)

| Profile | Command                    | Code generator on x86-64 Windows/Linux, AArch64 Linux | Elsewhere                                         | Output            |
| ------- | -------------------------- | ----------------------------------------------------- | ------------------------------------------------- | ----------------- |
| dev     | `brine build`, `brine run` | native backend                                        | C at `-O2`, split into units compiled in parallel | `target/dev/`     |
| release | `--release`                | C at `-O3`, one unit                                  | C at `-O3`, one unit                              | `target/release/` |
| debug   | `--debug`, `brine debug`   | native backend with DWARF, no optimizations           | C at `-O0 -g` with `#line`                        | `target/debug/`   |

What stays the same in every profile:

* **The checks.** Overflow, bounds, division by zero, narrowing conversions and contracts behave identically. A program that panics in a release build panics in a dev build at the same place with the same message. See [Runtime checks](/proofs/runtime-checks/).
* **The results.** Integer and float semantics are Brine’s, not the C compiler’s: no fused multiply-adds, the same rounding, the same [reduction order](/parallelism/deterministic-reductions/). A release build prints the same output as a dev build.
* **The runtime.** The runtime (`brine_rt.c`) is always C, compiled once with the profile’s flags and cached.

C builds use `-std=c11 -fno-strict-aliasing -w` plus the profile’s flags. `brine test --release` runs the tests with the release profile.

## Backends

[Section titled “Backends”](#backends)

All backends read the same **Brine IR**, which sits after the type checker and the prover. The IR keeps only the checks the prover couldn’t remove, and records what Brine knows that C doesn’t (no aliasing between values, race-free `par`).

| Backend | Source                      | Writes                                                                    | Used for                                                                                                                                |
| ------- | --------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| x86-64  | `compiler/x64.br`, `obj.br` | COFF objects (Windows), ELF objects (Linux), with unwind tables and DWARF | dev and debug builds on x86-64 Windows and Linux                                                                                        |
| AArch64 | `compiler/a64.br`, `obj.br` | ELF objects with `.eh_frame` and DWARF                                    | dev and debug builds on AArch64 Linux                                                                                                   |
| C       | `compiler/ir_c.br`          | C11, one statement per IR instruction                                     | every `--release` build; every build on macOS and other platforms; every [WebAssembly](/tooling/webassembly/) build; the bootstrap seed |
| PTX     | `compiler/ptx.br`           | NVIDIA GPU kernels                                                        | `par` constructs that can run on a GPU, on Windows and Linux                                                                            |
| Metal   | `compiler/msl.br`           | Metal Shading Language                                                    | the same, on Apple GPUs (macOS)                                                                                                         |

The native backends write the object file themselves; the system C compiler is still used to link it with the runtime. The GPU backends run next to whichever CPU backend is used: the program carries both versions of each eligible construct, and the runtime picks one per launch. See [GPUs](/parallelism/gpu/).

| Platform            | dev           | release | debug          |
| ------------------- | ------------- | ------- | -------------- |
| Windows x86-64      | native (COFF) | C       | native + DWARF |
| Linux x86-64        | native (ELF)  | C       | native + DWARF |
| Linux ARM64         | native (ELF)  | C       | native + DWARF |
| macOS Apple silicon | C             | C       | C with `#line` |
| macOS Intel         | C             | C       | C with `#line` |

macOS builds go through C until Brine writes Mach-O objects (on the [roadmap](/reference/roadmap/)). [`BRINE_BACKEND`](/tooling/environment-variables/#brine_backend)`=c` or `=native` overrides the choice; such builds go to their own folders (`target/dev-c/`, …).

### WebAssembly

[Section titled “WebAssembly”](#webassembly)

`--target wasm` and `--target web` compile the generated C with [wasi-sdk](https://github.com/WebAssembly/wasi-sdk) instead of the system C compiler, for every profile: dev at `-O2` in parallel units, release at `-O3` in one unit, debug at `-O0 -g`. They go to their own folders (`target/dev-wasm/`, `target/release-web/`, …). The checks and the results are the same as natively; `par` constructs run on one thread. See [WebAssembly](/tooling/webassembly/).

### Why release goes through C

[Section titled “Why release goes through C”](#why-release-goes-through-c)

gcc and clang at `-O2`/`-O3` already produce code close to the best hand-written C gets, with decades of optimization passes behind them. The native backends are built for compile speed: they turn the IR into machine code in one quick pass, with locals and floats kept in registers but without the loop optimizations, inlining across functions and vectorization a C compiler does. So Brine uses its own backends where you build often (dev, debug) and the C compiler where you want the fastest program (release). Optimization passes on the IR are on the [roadmap](/reference/roadmap/).

A native backend’s advantages are elsewhere: builds take a fraction of a second, they will eventually need no C toolchain, and they can do what C can’t express well (exact overflow handling, guaranteed tail calls, debug info for Brine names and types).

### The generated C

[Section titled “The generated C”](#the-generated-c)

`brine build --emit-c` writes the program as a single C file next to the executable (`target/dev/hello.c`), as the C backend generates it, even when the executable was built by a native backend:

```sh
brine build --emit-c
```

```text
     Wrote target/dev/hello.c
     Fresh hello v0.1.0 [dev] in 0.02s
    Output target/dev/hello.exe
```

It includes `brine_rt.h`, so compiling it by hand needs the runtime from the Brine source tree (`runtime/brine_rt.h`, `runtime/brine_rt.c`). It’s for reading and for curiosity: don’t edit it and build from it, since the next build regenerates it.

## Caching

[Section titled “Caching”](#caching)

Builds do as little work as they can:

* **The runtime** is compiled once per runtime version, C compiler and flags, into `rt-<hash>/` in the cache. The first build of a new project takes about two seconds for this; later builds skip it.
* **Objects** are named by a hash of what they’re made from. A C dev build splits the program into one translation unit per core (up to 16), assigning each function to a unit by a hash of its name, with each unit declaring only what it uses. The units compile in parallel, and an edit recompiles only the units it touches. Release builds stay a single unit, so the C compiler sees the whole program.
* **The link** is skipped when nothing changed: the `.brine-hash` file next to the executable records what it was linked from, and a matching build prints `Fresh`.

The cache is `target/cache/` in a project and `<temp>/brine-cache/` for single files. See [Projects and brine.toml](/tooling/projects/#the-cache).

The compiler uses `par` itself: imported modules are parsed a wave of imports at a time, in parallel, and the prover checks every function on its own thread. Results are combined in a fixed order, so diagnostics are the same whatever the thread count.

## Compile speed

[Section titled “Compile speed”](#compile-speed)

The compiler is about 34,000 lines of Brine. Building it with itself on a Ryzen 9 9950X3D (Windows 11, gcc 16.2), with the runtime already cached:

| Build                                                   | Time   |
| ------------------------------------------------------- | ------ |
| dev, native x86-64                                      | 0.92 s |
| dev, nothing changed (`Fresh`)                          | 0.66 s |
| dev through C (`BRINE_BACKEND=c`, 16 units in parallel) | 2.65 s |
| release through C (`-O3`, one unit)                     | 27.4 s |

`BRINE_TIMES=1` shows where the time goes in the native build:

```sh
BRINE_TIMES=1 brine build compiler/main.br -o brine2
```

```text
  parse      0.263s
  check      0.122s
  uniform    0.013s
  verify     0.157s
  lower      0.071s
  x86-64     0.162s
  link       0.089s
     Built main [dev] in 0.92s
```

`brine check` stops after `verify`, which is why it is the fastest way to find errors. A small program checks in a few milliseconds and builds in about 0.1 s, almost all of it linking.

## Performance

[Section titled “Performance”](#performance)

The `bench/` folder in the Brine repository has eight single-threaded programs, each written in Brine and in C with the same algorithm. Times are relative to the C version compiled with `gcc -O2`; lower is better, below 1.00x is faster than C.

| Program     | What it exercises                                     | Brine release | Brine dev (native) |
| ----------- | ----------------------------------------------------- | ------------- | ------------------ |
| nbody       | float math on an array of structs (C: 0.717 s)        | 1.01x         | 3.42x              |
| spectral    | float math, small function calls, array reads         | 1.02x         | 2.03x              |
| fannkuch    | small int arrays, swaps and index arithmetic          | 0.91x         | 1.22x              |
| mandelbrot  | tight float loops over a 4000×4000 grid               | 1.00x         | 1.30x              |
| sieve       | a 100M-element bool array, strided writes             | 1.15x         | 1.44x              |
| binarytrees | allocating and freeing many small recursive values    | 0.55x         | 0.89x              |
| wordfreq    | building strings, counting them in a `{str: int}` map | 3.91x         | 5.06x              |
| matmul      | 800×800 float matrix multiply in flat arrays          | 1.16x         | 6.28x              |

Measured on a Ryzen 9 9950X3D, Windows 11, gcc 16.2 (MinGW), Brine 0.4.0, best of three runs. Expect a few percent of noise between runs, more for the shortest programs.

What the numbers say:

* **Release builds run at C’s speed** on numeric code (nbody, spectral, mandelbrot), within a few percent, with every check that remains still running.
* **Brine wins where its memory model helps.** binarytrees runs in about half of C’s time: small blocks come from Brine’s own allocator, and a value with only one reference is freed without an atomic instruction, while the C version calls `malloc` and `free` for every node.
* **Checks that remain cost something.** In sieve, the prover can’t show that `is_prime[j]` is in bounds in the inner loop, so the bounds check runs on every write of the inner loop (`brine check --report` lists it). matmul and nbody have no checks left at all.
* **Strings and maps are the slow spot.** wordfreq takes almost 4x C’s time. The C version writes each word into a reused `char[16]` buffer and looks it up in a bare open-addressing table; the brine version builds a new reference-counted string for every word (`w += letters[...]` makes a one-character string per step), then looks the word up twice (`w in counts`, then `counts[w] += 1`) in a map that also keeps insertion order. The C run is also only 23 ms, so fixed startup costs show more. Faster strings and maps are ongoing work; 0.4.0 already made `s += t` grow strings in place.
* **Dev builds trade run time for build time.** The native backend doesn’t vectorize or optimize loops, which matters most for matmul (6.28x) and nbody (3.42x). For anything you time, use `--release`.

These programs are single-threaded on purpose, to compare code generation. With `par`, Brine programs also use every core and, when it’s faster, the GPU: see [GPUs](/parallelism/gpu/) for those numbers.

### Reproducing the numbers

[Section titled “Reproducing the numbers”](#reproducing-the-numbers)

From a clone of the Brine repository, with Brine and a C compiler on your `PATH`:

```sh
git clone https://github.com/brinelang/brine && cd brine
brine run bench/compare.br
```

`bench/compare.br` builds each program three ways (C with `$CC -O2`, falling back to the first of `cc`, `gcc` and `clang`; Brine `--release`; Brine dev), checks that all three print the same output, and prints the best of three runs:

```text
program         C -O2          release     dev (native)
nbody          0.708s     0.710s 1.00x     2.441s 3.45x
spectral       0.271s     0.274s 1.01x     0.548s 2.02x
fannkuch       1.686s     1.537s 0.91x     2.038s 1.21x
mandelbrot     0.567s     0.597s 1.05x     0.821s 1.45x
sieve          0.253s     0.351s 1.39x     0.508s 2.01x
binarytrees    1.337s     0.747s 0.56x     1.183s 0.88x
wordfreq       0.023s     0.090s 3.90x     0.117s 5.04x
matmul         0.062s     0.068s 1.10x     0.405s 6.53x
```

(That is a second run on the same machine: the short programs, sieve and matmul especially, vary the most.) It tests the `brine` on your `PATH`; to test another build, pass its path first (it must contain a `/` or end in `.exe`): `brine run bench/compare.br -- ./brine2`. Program names after that run only those programs: `brine run bench/compare.br -- nbody sieve`. The executables go to `target/bench/`.

## Related

[Section titled “Related”](#related)

* [The brine command](/tooling/cli/#brine-build)
* [Debugging](/tooling/debugging/)
* [WebAssembly](/tooling/webassembly/)
* [Environment variables](/tooling/environment-variables/)
* [Design and architecture](/reference/design/)
* [Platforms](/reference/platforms/)
