# From five minutes to under a minute: fast self builds, and seeds instead of a chain


Every change to Xo's compiler ends with the compiler building itself,
and every fresh machine starts by building one. Both got much faster
this week. This post is about where the time went, what changed, and
what that saves. As usual, AI agents did the work and the project owner
set the direction (decisions 0216 and 0220).

## Where the time went

We measured before changing anything. On the development Mac, the
compiler building itself took 337.28 s, and almost all of it was the
back end: 178.80 s writing arm64 assembly, then 112.41 s for the system
assembler and linker to read the 437 MB of assembly that produced. The
whole front end (loading, parsing, and checking every module) took
1.17 s. On a 32 vCPU arm64 cloud machine (r8g.8xlarge) the same build
took 496.82 s.

## Stage 1: use the cores

The compiler was doing all of that on one core. Stage 1 split the work:
the code generator emits functions in parallel, the assembly is written
and assembled in parts, and the reference counting pass and the
verifier run as tasks. The output is byte identical whatever the number
of jobs.

- Mac: 337.28 s to 59.00 s.
- r8g.8xlarge, 16 jobs: 496.82 s to 49.83 s.

## Round 2: write less code

Stage 1 made the same work faster. Round 2 cut the work itself. When a
value of a struct type died, the compiler released each heap field
separately, and the emitter loaded every field and called its release.
That pattern was 38 percent of the lines of the compiler's own
assembly. Now a struct is released with one call, and large structs go
through one generated helper per type. On r8g.8xlarge:

| | Before | After |
|---|---|---|
| self build | 49.83 s | 34.15 s |
| peak memory | 19.86 GB | 7.57 GB |
| assembly | 463.6 MB | 391.6 MB |
| calls | 4.64 million | 1.27 million |
| compiler binary | 88.4 MB | 73.1 MB |

The rebuild still reproduces itself byte for byte, and the test
programs pass on both native backends.

## x86: stop optimizing development builds

On x86-64 the compiler is built through LLVM, and most of the time
went to clang optimizing a very large program. A development build of the
compiler now uses `-O0`; builds for distribution stay at `-O2`. The
trade-off is real: a `-O0` compiler is about 1.5 times slower at
compiling programs. But it builds itself in a fraction of the time, and
with round 2 the LLVM IR it produces shrank from 1.116 GB to 312.7 MB.

Put together, the x86 development self build went from 2,510.80 s, at
`-O2` before these changes, to 83.10 s.

## How that compares

For scale, Go's own `make.bash`, which builds the whole Go toolchain
three times over, took 46.40 s from a cold cache on the same class of
machine. Xo's compiler building itself in 34.15 s is now in the same
range. It is not the same task, and Go's build is mature and cached in
ways Xo's is not yet; per-module caching is the next stage of the plan.

## Seeds: no more chain

With the Go compiler deleted, the only thing that can build `xo` is an
earlier `xo`. Until now, a fresh machine started from the last compiler
written in Go and replayed a chain of intermediate compilers up to
today's. On a fresh cloud machine that took 1,491 to 1,936 s before the
real build could even start.

Now each version of `xo` is built by the previous version, and that
previous version is kept prebuilt for every platform, in private
storage the project's own machines can read. They are not public
downloads. A fresh machine downloads one seed and does one build.
The chain from the Go compiler still exists, but only when someone asks
for it: to bootstrap a new platform, or to rebuild everything from
source so no old binary has to be trusted.

The newest seed was cut on 2026-10-14 right after the round 2 changes,
for all five platforms. The Mac's own (darwin/arm64) was built natively
with full optimization after its fixed point passed; the other four
(Linux arm64 and x86-64, Windows, Intel Mac) were cross built on the
same Mac by that compiler, through the Go backend, in 16 to 24 s each.
Each is a 14 to 16 MB archive (35 MB for the Mac's). With the first
seed, on 2026-10-13 and before the fast builds, a Mac with empty caches
had a working `xo` in 325 s, against 1,491 to 1,936 s for the chain on
a fresh cloud machine.

Seeds also decide when old runtime code can go. The C version of the
random number functions had moved to Xo, but deleting the C broke the
build: the previous seed's compiler still called it. Once the new seed
existed, nothing called it, and the C was deleted the same day. That
is the rule working as designed: a runtime change the compiler itself
depends on waits for the next seed.

## What it saves

The savings below are per build, measured; we have not added them up
into totals.

- **Every compiler change on the Mac:** a self build of 59.00 s instead
  of 337.28 s, about four and a half minutes saved each time an agent
  rebuilds the compiler.
- **Every fixed-point check in the cloud:** the self build on
  r8g.8xlarge is 34.15 s instead of 496.82 s, and the fixed point
  builds the compiler more than once.
- **Every x86 development build:** 83.10 s instead of 2,510.80 s,
  about 40 minutes saved each time.
- **Every fresh machine:** one seed and one build, instead of 25 to 32
  minutes of chain first.

What it will save is the same arithmetic, applied to every future
change. A self build is the step every compiler change, every merge,
and every fixed-point check waits on, so it is the one worth making
fast.

## What is next

Stage 2 of the plan caches work per module, so a change to one module
does not rebuild the rest; stage 3 compiles modules separately. Each
stage starts only after the measurement of the one before.

