Xo 0.27 release notes
Downloads: Xo 0.27.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.
Getting started shows how to install
one.
Xo against Go
Each release is measured against Go on the same Mac (bench/results/release-vs-go-v0.27.0.md, lines 11 to 24):
- Run time: Xo is faster on 14 of 20 workloads, within noise on 2, and slower on 4 (conc rwmutex 3.58 times Go’s wall time, llvm nbody 1.57, conc spawn 1.20, conc pmap 1.07).
- Peak memory of the run: lower than Go’s on all 20 (0.38 to 0.99).
- Binary size: 1.85 to 1.94 MB against Go’s 2.39 to 2.43 MB.
- Compile: cold builds 1.8 to 2.3 s against Go’s 1.6 to 1.9 s; a warm build of an unchanged program, 0.27 to 0.34 s against 0.04 s (0.42 to 0.64 s in 0.26.0), is still the largest gap, and xo’s build now peaks at 0.49 to 0.50 GB cold (0.76 to 0.89 GB in 0.26.0).
Breaking changes
- A third-party
use goneeds a pin (decision 0356). Ause goof a package outside Go’s standard library needsrequire go <module> <version>in xo.mod; without it the use is error XO1319. Before, the build followed the module’s latest version and recorded it only in the git-ignored.xo/, so two machines could build different code from the same commit.xo fixadds the line with the version the binding resolved, andxo mod tidyadds missing pins. xo.lock now records each pinned Go module’s go.sum hashes (go <module> <version> h1:...): a changed module is XO1304, a missing line XO1305 (runxo mod tidy). Go’s standard library packages need nothing. - std’s errors print plain messages (decision 0358). Display of
std’s error enums is a sentence, as Go’s error strings are:
PATH: no such file or directory,connection refused,no rows in result set, instead of nested Debug output such asConn(msg: "Io(msg: \"tls: ...\")"). This covers FsErr, NetErr, IoErr, EnvErr, ProcErr, ClockErr, SqlErr, HttpErr, ClientErr, UrlErr, CookieErr, TlsErr, SmtpErr and TemplateErr.${e:?}(Debug) is unchanged. A program or test that compares the printed text changes with it.
New
--releaseis a preset (decision 0354). Each setting it changes has its own flag in any build (xo build,run,test), which wins over the preset whatever the order:--debug-contracts,--dbg,--error-args,--debug-info=on|off|split,--profile-support,--trace-support(on|off, a bare flag is on) and--opt=O0..O3(LLVM).--error-args=offno longer needs--release.--debug-info=split(decision 0355) keeps the debug information beside the program:PROG.dSYMon macOS,PROG.debugon Linux, found by tracebacks, lldb, gdb and addr2line.- Build information in every binary.
xo version -m BINARYprints what built it, asgo version -mdoes: the xo, backend, target, every effective setting, the stamps and the sources’ hashes. A native crash report names the build on its second line. - Deterministic scheduling on the native backends (decision 0350):
xo run --seed=N,xo build --seed=N, andxo test --seed=N/--explore=Nrun every task on one thread with a seeded choice at each scheduling point, so a seed reproduces an interleaving.--exploreprints the command that reproduces a failure. XO_PROF_HEAP_AT=TEXT(std/debug) writes the heap in use when a labeled point is reached.
Faster
xo checkis about 2.5 times faster on a warm run (xo itself 1.8 s to 0.67 s, a small server 0.59 s to 0.26 s) and peaks a third lower in memory: an unchanged module is skipped and its diagnostics replayed, and the native runtime’s bodies are checked as a build checks them.xo check --supportis 2.3 to 2.6 times faster (3.7 s to 1.6 s on xo itself). The output is unchanged;XO_CHK_CACHE=offchecks everything.- Warm builds: the module check cache and a shared cache pack are on
by default (a warm build of xo itself 0.885 of the wall time; a new
program’s first build 0.831), front end cache keys that are never
stored are no longer looked up, and std and runtime function bodies
are parsed only when needed (a small program’s warm build peaks about
40% lower in memory).
XO_CHK_CACHE=off,XO_SHARED_PACK=offandXO_LAZY_PARSE=offturn them off. - Runtime: channels of one-word values (integers, Float, Bool, enums
without payloads) send the word instead of a one-element list (ping
pong 0.835 of the wall time);
List.sortsorts runs of 16 and merges between two buffers (1M Ints 0.82 of the wall time, now level with Go); derived JSON decoders find every field in one pass; the macOS monotonic clock is read as Go reads it. - Code generation on x86_64: a better register ranking for long hot ranges, cold-path saves, and a larger inlining budget for a function called once.
- std/http and std/url: a URL is parsed once, a plain query is read in place, and router path parameters skip a range check.
Fixes
Most of these come from field testing of 0.26.0 on an Intel Mac and on Windows. Thank you to the testers: every report below was reproduced, fixed with a test, and checked again.
- x86_64 miscompile: a function whose entry block runs without its frame (shrink wrapping) read parameters passed on the stack from the wrong offset, giving wrong results on linux/amd64.
- macOS on Intel: programs with
use cor--ld=cclink again;--racebinaries load ThreadSanitizer’s runtime beside them, and--backend=llvm --raceuses xo’s own runtime instead of Apple clang’s, which stopped at SIGILL. - Cross builds link or fail with a message naming the target, instead
of exiting 0 with only the assembly written;
--targets=allskips a target the program cannot support (a network program and wasip1/wasm) with a note;--targets=wasip1/wasmalone builds, and wasm code seestarget.osas Wasi. - Windows paths:
xo initandxo testno longer hang at a drive root, andC:/...paths are absolute everywhere in the CLI;XO_TRACEBACK=localssays once that locals are not available on Windows. xo checkchecks by the native backends’ rules, as a build does (use cpasses;--backend=gofor the Go backend’s), and the native support check is--support.- Language and diagnostics:
r ?? e => bodybuilds with a plain variable; a generic struct’s field default that mentions its type parameter builds natively;"${r?:?}"parses; a parse error in a parameter list no longer cascades into unrelated errors;xo fixremoves a?after an expression that cannot fail; a hole inside a string has typeStr. - std/sql: Postgres numeric, json and date columns read as the spec
says, and a wrong parameter count is
Param. - Tools:
xo dumplists live tasks only;XO_TRACEBACK=localsshows a list’s elements and leaves out locals not set yet;xo query type --dirand the MCP query tool find a file under the directory;xo serveexplains a socket path that is too long;xo fmt ./...no longer rewrites files undertestdata/, and everyDIR/...walk skipstestdata,vendorand names starting with.or_; a test’sopt?failure names its location. - Very large programs: three quadratic compile costs in very long
functions are gone, and a
matchof tens of thousands of arms assembles on arm64.
Known gaps
- Spawning many short tasks is slower than Go: 1.20 times Go’s wall time in this release’s spawn workload, and the field report measured 100,000 spawns at 1.25 times Go.
- Sorting strings:
List.sortof Strs takes 1.18 times Go’sslices.Sort(Ints and Floats are now level with Go). - Floating point: the n-body workload takes 1.57 times Go’s wall time.
- A read-write lock contended by many tasks is several times slower than Go’s (3.58 times in this release’s comparison): Xo spreads lock-bound tasks over all its workers, where Go keeps them near one.
The full Xo against Go comparison for this release is in bench/results/release-vs-go-v0.27.0.md.