Bun’s Zig fork got 4x faster compilation times

AI is entirely besides the point here. The changes in this Zig fork are not desirable to upstream for several reasons.

Parallel semantic analysis has been an explicitly planned feature of the Zig compiler for a long time, and it has heavily influenced the design of the self-hosted Zig compiler. However, implementing this feature correctly has implications not only for the compiler implementation, but for the Zig language itself! Therefore, to implement this feature without an avalanche of bugs and inconsistencies, we need to make language changes.

An example of this is the changes to type resolution which happened in the 0.16.0 release cycle—these didn’t affect users too much, but had big implications for the compiler implementation. Before those changes, the compiler’s behavior was often highly dependent on the order in which types and declarations were semantically analyzed by the compiler. Some orders might result in successful compilation, while others give compile errors. Single-threaded semantic analysis prevented these bugs from causing user-facing non-determinism. The rewritten type resolution semantics were designed to avoid these issues, but Bun’s Zig fork does not incorporate the changes (and has not otherwise solved the design problems), which means their parallelized semantic analysis implementation will exhibit non-deterministic behavior. That’s pretty much a non-starter for most serious developers: you don’t want your compilation to randomly fail with a nonsense error 30% of the time.

Put more simply, we are going to make these enhancements, but hacking them in for a flashy headline isn’t a good outcome for our users. Instead we’re approaching the problem with the care it deserves, so that when we ultimately ship it, we don’t cause regressions.

The other enhancement they discussed is splitting the LLVM backend’s output into multiple modules. There’s nothing wrong with doing this for Debug builds in theory, but it’s simply not the right place for us to focus our efforts. The Tweet showing off this feature showed a compile time of 20 seconds for Bun. However, we view 20 seconds as an entirely unacceptable amount of time to wait for every single rebuild. This change to the LLVM backend has a hard performance ceiling, which Bun’s implementation has probably hit, because there’s no getting around the final bottleneck of LLVM’s compilation speed.

So instead of wasting time writing a more robust implementation of this LLVM module splitting logic for a relatively minor improvement, we have instead put that effort towards features like self-hosted backends and incremental compilation, which can improve compilation speed by orders of magnitude. We can compare the time taken for clean builds of the Zig compiler when using the LLVM backend vs our self-hosted x86_64 backend:

$ zig build --summary new -Dno-lib -Duse-llvm
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
   └─ compile exe zig Debug native success 2m MaxRSS:4G

$ zig build --summary new -Dno-lib
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
   └─ compile exe zig Debug native success 31s MaxRSS:1G

There’s the 4x speedup claimed by the Bun team, already available on Zig 0.16.0! (The self-hosted aarch64 backend is unfortunately not stable enough to use yet, but if you’re on aarch64-macos, you can just build for x86_64-macos and run the binaries through Rosetta.)

We can also try using incremental compilation—here’s what it looks like when I run an incremental build of the Zig compiler and then make some random changes to it:

$ zig build --summary new -Dno-lib -fincremental --watch
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
   └─ compile exe zig Debug native success 40s

Build Summary: 4/4 steps succeeded
install success
└─ install zig success
   └─ compile exe zig Debug native success 361ms

Build Summary: 4/4 steps succeeded
install success
└─ install zig success
   └─ compile exe zig Debug native success 368ms

Build Summary: 4/4 steps succeeded
install success
└─ install zig success
   └─ compile exe zig Debug native success 371ms

Build Summary: 4/4 steps succeeded
install success
└─ install zig success
   └─ compile exe zig Debug native success 172ms

Build Summary: 4/4 steps succeeded
install success
└─ install zig success
   └─ compile exe zig Debug native success 308ms

watching 121 directories, 1 processes

Again, this feature is available in Zig 0.16.0—you can use it!

Each update is taking less than 0.4s, compared to the 120+ seconds taken to rebuild with LLVM. In other words, incremental updates are over 300 times faster on this codebase than fresh LLVM builds are. In comparison, an enhancement capped at a 4x improvement is pretty abysmal. To be clear: these numbers are entirely realistic for a big project. I didn’t do things which I knew would be fast, I just made some vaguely-realistic changes. (If you try this on your project and a seemingly-simple incremental update takes more than, like, 0.5s, open an issue! I’m hungry for incremental compilation bugs to fix…)

Put simply, if you’re struggling with compilation speed, please take advantage of the solutions we’ve been working on! The self-hosted x86_64 backend has been the default since 0.15.x, so by upgrading from 0.14.x you automatically get the 4x performance improvement claimed in the Bun team’s post; and incremental compilation can be a night-and-day difference to development loops, particularly when fixing a large number of compilation errors, where you can get an updated list of errors almost instantly.

90 Likes