Faster Than Ninja

elasticdog2 pts0 comments

Faster Than Ninja

Faster Than Ninja

Posted on 5 Aug 2026 by<br>Boris Kolpackov

Some months ago I read an article about a tool for<br>snooping on slow build systems that can be used to find build<br>bottlenecks. While the whole article is quite illuminating, this quote stood<br>out to me:

Ninja is not a 100% fair comparison to other tools, because it<br>benefits from some "baked in" build logic by the tool that created the ninja<br>file, but I think it's a reasonable "speed of light" performance benchmark<br>for build systems.

To clarify, by "baked in" the author means that nobody writes Ninja build<br>files by hand. Rather, a second tool, such as CMake, is invoked to generate<br>them and some steps performed during this generation phase (examples below)<br>should ideally be part of the build phase. Also, Ninja is notoriously<br>minimalist, providing only the bare minimum of functionality, especially on<br>the change tracking side of things (again, examples below). A modern build<br>system would be expected to provide more.

Still, it would be interesting to see how close a modern, native (that<br>is, without the generation step) build system can approach the "speed of<br>light".

Let's take a look at how build2 measures up. While there<br>is a number of substantial projects (such as Boost and Qt) that can be built<br>with both build systems, finding a project of substance that would result in<br>an apples-to-apples comparison is difficult because when we package more<br>complex projects for build2, we invariably have to untangle the<br>"ball of intra-dependencies" structure into something more orderly (for a<br>good example, take a look at the upstream qtbase module versus<br>build2 packages). And this usually<br>results in a slightly different set of intermediate build artifacts, like<br>bootstrap and utility libraries.

So we will have to make do with something simpler, where we can make sure<br>the same set of object files and binaries is produced with more or less<br>identical compile and link options. In the end I've picked Xerces-C++, an<br>XML parser/serializer for C++. It has quite a few features (like XML Schema<br>validation) so it's not exactly tiny, measuring 299 C++ translation units<br>that are linked into a shared library.

We are going to test a full, from-scratch build, the same as in the<br>quoted article. Ninja completes this build on my machine (see Benchmark Details below) in 3.4s:

Time (mean ± σ): 3.429 s ± 0.029 s [User: 48.536s, System: 5.033s]<br>Range (min … max): 3.383 s … 3.464 s 10 runs

Before we measure build2, let's at least acknowledge the<br>elephant in the room: while Ninja builds the project in 3.4s, CMake takes<br>15.6s to generate the Ninja build files. So if you had Xerces-C++ as a<br>dependency of your project and it was being built from scratch, you would<br>wait 19 seconds, not 3.4, for this build.

With the matching configuration (same C++ compiler, C++ standard, debug<br>build, etc) build2 takes 3.8s, or about 11% slower:

Time (mean ± σ): 3.808 s ± 0.046 s [User: 58.037s, System: 8.012s]<br>Range (min … max): 3.746 s … 3.886 s 10 runs

Pretty close, but not at the speed of light. Let's see if we can get<br>there. Maybe building in vacuum will help?

To try to get closer to Ninja's time we are going to make the comparison<br>more accurately apples-to-apples. As discussed above, Ninja is notoriously<br>minimalist with build2 providing a lot of functionality that<br>Ninja does not. And some of this functionality has measurable cost,<br>performance-wise. So we are going to disable a few features to closer match<br>the amount of work done by Ninja.

The first feature that we will disable is the more precise change<br>tracking for C and C++ source files. Ninja simply checks whether the file's<br>modification time has changed and if so, recompiles it. build2,<br>in contrast, performs an extra step in this case: it tokenizes the<br>(partially-preprocessed) source file and computes the checksum of the<br>resulting tokens. If this checksum hasn't changed since the last time the<br>file was compiled, then it skips recompiling it. This ignores<br>whitespace-only changes (as long as they do not alter the column numbers of<br>the tokens) and is very useful during development (and is critical in some<br>case, like if you want to change your project's version with every commit).<br>But tokenizing all the 299 translation units in the from-scratch build has<br>an upfront cost, even if it may pay off during further incremental<br>builds.

The way to disable this ignorable change detection is to tell<br>build2 that the project is read-only (which is done<br>automatically by the package manager for external dependencies). In this<br>case build2 will fall back to using just the modification time,<br>the same as Ninja. With this change our build time goes down to 3.4s, pretty<br>much the same as Ninja's:

Time (mean ± σ): 3.433 s ± 0.055 s [User: 51.093s, System: 6.701s]<br>Range (min … max): 3.383 s … 3.551 s 10 runs

Let's see if we can go even faster. Next, we disable compression in the<br>file cache. We will discuss the file cache in more detail a bit...

ninja build build2 time file change

Related Articles