Xo is experimental. The source is now public on GitHub, or try Xo in your browser.

Xo 0.34 release notes

released 2026-10-29 · tag v0.34.0 · all releases

Downloads: Xo 0.34.0 on GitHub, with archives for macOS (Apple silicon and Intel, signed and notarized), Linux (arm64 and x86-64), and Windows (x86-64, Authenticode signed), and a SHA256SUMS file.

Xo 0.34.0 makes the edit loop fast: xo build and xo run now compile only your own code and take the standard library prebuilt, so an edit to a small program rebuilds in about 70 ms, faster than go build of the same edit. It also brings a round of standard library speed work against Go’s packages, a faster sort, the fixes from 0.33.1, and the field reports from 0.33.0’s testers. Archives for darwin/arm64 and darwin/amd64 (Developer ID signed and notarized), linux/arm64, linux/amd64 and windows/amd64 (Authenticode signed), each xo stamped with the commit it was built from (xo version -v).

One change breaks source: uuid.Uuid is now a value of two words, not its text. Read the migration below before upgrading a program that uses std/uuid.

Breaking change: uuid.Uuid

uuid.Uuid is now its 128 bits as two words instead of its canonical text (decision 0387; deprecated in 0.33.0 with vet’s XO0912). Migration: u.0 becomes u.display() (or "${u}"); uuid.Uuid(s) becomes uuid.parse(s)?; a pattern uuid.Uuid(t) becomes a binding and t = u.display(); u.0 < v.0 becomes u < v (same order). The checker names display() when it sees u.0. Against google/uuid: parse 1.18, string 1.02 and bytes 1.75 times Go’s time, from 1.23, 0.35 and 4.24 (bytes is now 0.965 with the stack change above).

The edit loop: separate compilation

  • On by default on darwin and linux, arm64 and amd64, and on windows/amd64 (decision 0388). A xo build or xo run compiles and links only the program’s own code; the standard library’s functions come from prebuilt variants in the build cache.
  • Edit rebuilds against 0.33.0 (darwin/arm64, medians of 20 interleaved): a one-function edit to the llvm-fib program takes 70.2 ms instead of 230.8 (go build of the same edit 87.7), and to the HTTP server 156.0 ms instead of 570.5 (go build 245.5).
  • The check skips std’s sources: a separately compiled build reads std’s declarations from a record instead of parsing std, so fib’s check takes 17.4 ms instead of 30.5 with 0.33.0, and the HTTP server’s 25.1 instead of 54.9.
  • Linux links faster for separately compiled builds: the DWARF sections are written whole and the relocation readers shift instead of looping (a Linux HTTP edit’s link 86 to 61 ms, then 93 to 87 ms, in Docker); a Linux HTTP edit takes 454 ms against 804 whole, fib 197 against 350.
  • windows/amd64 compiles separately by default too: llvm-fib’s edit 295 to 143 ms, the HTTP server’s 619 to 280. Windows links and PDBs are also faster on their own: the HTTP server’s PDB 105 to 66 ms of an edit, and a separately compiled link keeps only the debug types its live functions reach (PDB 2.38 to 2.25 MB, 4600 to 2127 type records; pe.pdb 15.5 to 13.4 ms, then 13.2 to 11.0 ms).
  • No runtime cost: separately compiled programs run as fast as whole-program builds (every release-vs-go workload within noise; the HTTP server 144,895 against 145,268 requests a second).
  • macOS links hash the image once: the binary’s UUID now comes from the code signature’s page hashes (decision 0400), one pass over the image instead of two: an HTTP server’s separately compiled link 0.898 of its time, fib’s 0.964.
  • When it does not apply: --release, --race, a cc link, programs that use c, and flags or variables that change std’s code build the whole program as before. The first build after std changes (a new xo) builds the whole program and builds std’s variants in the background. XO_SEPCOMP=0 turns it off for one build, XO_SEPCOMP_DEFAULT=0 for all.

Compiler and runtime

  • The compiler builds itself with 4.4% fewer instructions (reference counting insertion does the same work with fewer copies).
  • Interpreters and other match dispatch loops (a match of 8 or more arms in a loop) get each arm its own jump-table branch, so their speed no longer depends on where one branch lands in memory: the interp benchmark ran 204.5M to 281.4M cycles across 16 code placements, now 158.4M to 169.1M (decision 0397).
  • Intel CPUs with the JCC erratum (Skylake through Comet Lake): Xo’s x86-64 assembler pads jumps off 32-byte boundaries, so hot loops do not fall back to the slow decoder. sort1M on an i7-9750H ran 73 to 90 ms depending on code placement; now 69 to 72 ms (Go 67). interp on the same laptop: 160 to 128 ms (decision 0398).
  • Small strings and bytes a function only reads live in its stack frame on arm64 (decision 0395): uuid.bytes() takes about half its time (0.504) and now matches Go.
  • Struct fields in loops: a loop that stores into a List field of a struct (h.table[i] = x) checks the list once on the way in and keeps its length in a register, as a local list already did.
  • List[Int].sort is Go’s pdqsort: 1M integers in 54.7 ms on arm64 (Go 56.5; the old merge sort 59.0), and 82 to 84 ms on an Intel i7-9750H (Go 67 to 69; the merge sort 114 to 117). Other element types keep the stable merge.
  • Channel hand-offs switch once: a task that parks hands its worker straight to the task its wake queued (decision 0394), one stack switch instead of two: a 2M-message ping-pong in 0.698 of its time, 0.864 of Go’s (was 1.244); the other concurrency rows within noise. XO_DIRECT=0 turns it off.
  • Thread-local reads inline on macOS arm64 (decision 0396): dyld’s fast path runs in 12 instructions with no call; mutex 0.926 and fanout 0.935 of their time.
  • Timers under busy channel traffic (from 0.33.1): the scheduler checks timers and the network once a time slice, so sleeps and socket timeouts are no longer held up by tasks handing work to each other.
  • Fs.copy clones files on macOS (APFS copy on write) and copies in the kernel on Linux: 40 copies of a 7 MB file 79.5 ms to 7.0 ms on macOS.
  • Faster startup: a program binds only the imports its kept code uses, and builds without --race carry no ThreadSanitizer hooks, so an empty program’s symbol bindings at launch drop from 123 to 53 separately compiled and 64 to 53 whole; against the build before the change, startup takes 0.757 and 0.941 of the instructions and 0.942 and 0.977 of the wall time.

Standard library speed

Each change below has a timing row against Go’s package (bench/perf/*/twin.go.txt). Times are wall-clock against the previous build on darwin/arm64; the release’s own table against Go is under Xo against Go below. Where a line gives a ratio to Go, it is that change’s estimate in std/changes (its ratio to the parent times the previous ratio to Go), not a new measurement.

  • compress, archive, gzip: CRC-32 with arm64’s CRC32 instructions, back-references copied in bulk, Huffman tables built in place, the deflater’s hash tables at Go’s size. unzip 0.893, 0.834 and more of its previous time per step; unzip against Go about 1.04, gunzip about 1.01.
  • crypto: SHA-3 and SHAKE with arm64’s SHA3 instructions on Apple CPUs: SHA3-256 of 1 MiB in 0.577 of its time, 0.948 of Go’s (was 1.66); SHAKE and ML-KEM’s hashing take the same path.
  • math/big: decimal text 0.907, 0.945, 0.958 per step; long division (quorem) 0.898 then 0.779; multiplication 0.906.
  • regexp: compile 0.726 of its time (1.68 to 1.21 times Go’s). A further compile step (the ASCII bitmap built a word at a time, 1.15 times Go) is not in 0.34; it lands after the cut.
  • template: render 0.789, 0.859, 0.920, 0.934 per step (about 1.60 to 1.08 times Go); parse 0.948.
  • toml: decode 0.873, 0.937, 0.942, 0.954 per step (1.45 to about 1.13 times Go).
  • xml and derive Xml: encode writes as it goes with no element tree: 0.548, then 0.687 and 0.801 for the escapes and the tag stack.
  • mail: address lists 0.468 and addresses 0.527 of their time.
  • websocket: masking a word at a time: big frames 0.507.
  • path: clean and dir with no list for up to 8 elements (parts 0.865 then 0.927); still about 1.2 times Go, an allocation cost.
  • mime, uuid: format_media_type 0.961; uuid.bytes() 0.504 (above).
  • New: BytesBuilder.put_le and put_be overwrite 1, 2, 4 or 8 bytes in place (decision 0399), and xml.escape_to appends escaped text to a builder.

Fixes

The fixes of 0.33.1 are all in 0.34.0: timers under busy channels, the cold-build lz4 crash, gunzip and unzip on WebAssembly, and the use go fixes below.

  • use go with a newer Go (decision 0393): a release carries the binding helper’s Go source and builds it with your Go once, so Go 1.27 no longer breaks every binding.
  • use go on Windows finds the shipped bin/xo-gobind.exe, and xo mod tidy no longer doubles C:/ paths. use go and --backend=go also work from PowerShell and cmd: xo started the Go build (and std’s background variant build) through an env program that a Windows host has only under Git Bash. Also on Windows: xo started from Git Bash finds itself without the .exe, and XO_BACKEND_CACHE_DIR given as C:\x is taken as absolute.
  • use go values: == on a Go value handed out by value (netip.Addr, language.Tag) compares by value; one Xo type per Go type across bindings (decision 0392); modules with a patch-level go line build; xo mod tidy with a require go pin and no binding says to run xo check first (XO1320); a helper from another xo is XO1101 naming both formats; xo check --no-gc reports one XO1201 per Go package and file. Bindings regenerate automatically (format 7).
  • x86 Linux with GNU as: a build that assembles through cc (XO_NOLIBC=0) failed with “unknown pseudo-op: .arch_extension”; the arm64 directives are now emitted for arm64 only.
  • The build cache no longer rebuilds std’s cached entries twice when an edit comes more than a minute after the previous one.
  • xo bench --ab judges a loaded round against the machine’s own idle load (plus 3, at least 6) instead of a fixed 6, which rejected most rounds of multi-threaded workloads on a busy idle machine.
  • xo vet --cost notes a Str or Bytes slice in a loop read only by index, length or equality (XO0913) and a small List made and only read every iteration (XO0914).

Xo against Go

The release comparison runs the same programs in Go and in Xo on the release’s own build (the v0.34.0 tag’s xo, Go 1.26.1, darwin/arm64 on an Apple M4 Pro, xo bench --ab, 10 interleaved rounds, quiet machine). Ratios are Xo’s wall time over Go’s; under 1.0 means Xo took less.

  • Compute: interpreter 0.291 (was 0.500 in 0.33.0), strings 0.458, sort 0.815, sieve 0.924, collatz 0.965, fib 0.966; n-body 1.040, still slightly slower than Go (was 1.054).
  • Collections: branch 0.025, iter 0.264, putget 0.579, get 0.599, update 0.601, copymut 0.675, push 0.735.
  • Concurrency: mutex 0.271, spawn 0.428, fanout 0.667, ping-pong 0.897 (was 1.196, the direct hand-off above), rwmutex 0.897; parallel map 1.010 (was 1.086).
  • Against 0.33.0: 18 of the 20 workloads are faster than Go (n-body and the parallel map the two that are not, both closer than before); one wall ratio is worse than 0.33.0’s by more than 3%, rwmutex 0.869 to 0.897 (+3.2%, inside its spread). Peak memory against Go rose on sort, sieve and fanout, but Xo’s own peak is unchanged: a direct comparison of the 0.33.0 and 0.34.0 builds of sort gives 34,592 KB for both, and the ratio moved with Go’s side (its sort about 83 MB in the 0.33.0 run, 76 MB in this one).
  • Builds: an edit rebuilds in 60 to 70 ms (the collections program 130) against go build’s 100 to 110, and a cold build takes 850 to 1030 ms against Go’s 1440.

Platform notes

  • darwin/arm64 is the measured platform throughout; the Intel numbers above are from an i7-9750H laptop.
  • SHA-3’s and CRC-32’s instruction paths are arm64 only; amd64 and wasm keep the portable code (Linux arm64 keeps scalar SHA-3, as Go does).
  • Separate compilation needs the backend cache; with it off, builds are whole-program as before.

Known issues

  • --backend=wasm: a program that uses tasks (scope, spawn, Chan) does not build. Fixed in 0.35.