Faster Than Ninja
Faster Than Ninja
Faster Than Ninja Posted on 5 Aug 2026 by Boris Kolpackov 比 Ninja 更快,发布于 2026 年 8 月 5 日,作者:Boris Kolpackov
Some months ago I read an article about a tool for snooping on slow build systems that can be used to find build bottlenecks. While the whole article is quite illuminating, this quote stood out to me: Ninja is not a 100% fair comparison to other tools, because it benefits from some “baked in” build logic by the tool that created the ninja file, but I think it’s a reasonable “speed of light” performance benchmark for build systems. 几个月前,我读到一篇关于探测缓慢构建系统工具的文章,该工具可用于发现构建瓶颈。虽然整篇文章都很有启发性,但这段引文引起了我的注意:“Ninja 与其他工具的比较并非 100% 公平,因为它受益于创建 ninja 文件的工具所‘内置’的构建逻辑,但我认为它是构建系统性能的一个合理的‘光速’基准。”
To clarify, by “baked in” the author means that nobody writes Ninja build files by hand. Rather, a second tool, such as CMake, is invoked to generate them and some steps performed during this generation phase (examples below) should ideally be part of the build phase. Also, Ninja is notoriously minimalist, providing only the bare minimum of functionality, especially on the change tracking side of things (again, examples below). A modern build system would be expected to provide more. 澄清一下,作者所说的“内置”是指没有人会手动编写 Ninja 构建文件。相反,通常会调用第二个工具(如 CMake)来生成这些文件,而生成阶段执行的一些步骤(见下文示例)理想情况下应该属于构建阶段的一部分。此外,Ninja 以极简主义著称,仅提供最基本的功能,特别是在变更追踪方面(同样见下文示例)。而现代构建系统理应提供更多功能。
Still, it would be interesting to see how close a modern, native (that is, without the generation step) build system can approach the “speed of light”. Let’s take a look at how build2 measures up. 尽管如此,看看一个现代的、原生的(即没有生成步骤的)构建系统能多接近“光速”还是很有趣的。让我们来看看 build2 的表现如何。
While there is a number of substantial projects (such as Boost and Qt) that can be built with both build systems, finding a project of substance that would result in an apples-to-apples comparison is difficult because when we package more complex projects for build2, we invariably have to untangle the “ball of intra-dependencies” structure into something more orderly (for a good example, take a look at the upstream qtbase module versus build2 packages). And this usually results in a slightly different set of intermediate build artifacts, like bootstrap and utility libraries. 虽然有许多大型项目(如 Boost 和 Qt)可以使用这两种构建系统进行构建,但要找到一个能够进行“苹果对苹果”公平比较的实质性项目却很困难。因为当我们为 build2 打包更复杂的项目时,总是不得不将那种“内部依赖纠缠”的结构梳理得更有序(一个很好的例子是对比上游的 qtbase 模块与 build2 软件包)。这通常会导致中间构建产物(如引导程序和实用库)的集合略有不同。
So we will have to make do with something simpler, where we can make sure the same set of object files and binaries is produced with more or less identical compile and link options. In the end I’ve picked Xerces-C++, an XML parser/serializer for C++. It has quite a few features (like XML Schema validation) so it’s not exactly tiny, measuring 299 C++ translation units that are linked into a shared library. 因此,我们必须退而求其次,选择一个更简单的项目,确保在基本相同的编译和链接选项下,能够产生相同的一组目标文件和二进制文件。最终我选择了 Xerces-C++,这是一个用于 C++ 的 XML 解析器/序列化器。它具有相当多的功能(如 XML Schema 验证),因此并不算小,包含 299 个被链接到一个共享库中的 C++ 翻译单元。
We are going to test a full, from-scratch build, the same as in the quoted article. Ninja completes this build on my machine (see Benchmark Details below) in 3.4s: 我们将测试一次完整的、从零开始的构建,与引用的文章中一样。在我的机器上(见下文基准测试详情),Ninja 完成此构建耗时 3.4 秒:
Time (mean ± σ): 3.429 s ± 0.029 s [User: 48.536s, System: 5.033s] Range (min … max): 3.383 s … 3.464 s 10 runs 时间(平均值 ± σ):3.429 秒 ± 0.029 秒 [用户:48.536 秒,系统:5.033 秒] 范围(最小值 … 最大值):3.383 秒 … 3.464 秒,运行 10 次
Before we measure build2, let’s at least acknowledge the elephant in the room: while Ninja builds the project in 3.4s, CMake takes 15.6s to generate the Ninja build files. So if you had Xerces-C++ as a dependency of your project and it was being built from scratch, you would wait 19 seconds, not 3.4, for this build. 在测量 build2 之前,我们至少要承认一个显而易见的事实:虽然 Ninja 构建该项目只需 3.4 秒,但 CMake 生成 Ninja 构建文件却需要 15.6 秒。因此,如果你的项目依赖 Xerces-C++ 且需要从零开始构建,你等待的时间将是 19 秒,而不是 3.4 秒。
With the matching configuration (same C++ compiler, C++ standard, debug build, etc) build2 takes 3.8s, or about 11% slower: 在匹配的配置下(相同的 C++ 编译器、C++ 标准、调试构建等),build2 耗时 3.8 秒,慢了约 11%:
Time (mean ± σ): 3.808 s ± 0.046 s [User: 58.037s, System: 8.012s] Range (min … max): 3.746 s … 3.886 s 10 runs 时间(平均值 ± σ):3.808 秒 ± 0.046 秒 [用户:58.037 秒,系统:8.012 秒] 范围(最小值 … 最大值):3.746 秒 … 3.886 秒,运行 10 次
Pretty close, but not at the speed of light. Let’s see if we can get there. Maybe building in vacuum will help? To try to get closer to Ninja’s time we are going to make the comparison more accurately apples-to-apples. As discussed above, Ninja is notoriously minimalist with build2 providing a lot of functionality that Ninja does not. And some of this functionality has measurable cost, performance-wise. So we are going to disable a few features to closer match the amount of work done by Ninja. 非常接近,但还没达到“光速”。让我们看看能否达到。也许在“真空”环境下构建会有帮助?为了更接近 Ninja 的时间,我们将使比较更加“苹果对苹果”。如上所述,Ninja 以极简著称,而 build2 提供了许多 Ninja 所没有的功能。其中一些功能在性能上是有可衡量的成本的。因此,我们将禁用一些功能,以更接近 Ninja 所做的工作量。
The first feature that we will disable is the more precise change tracking for C and C++ source files. Ninja simply checks whether the file’s modification time has changed and if so, recompiles it. build2, in contrast, performs an extra step in this case: it tokenizes the (partially-preprocessed) source file and computes the checksum of the resulting tokens. If this checksum hasn’t changed since the last time the file was compiled, then it skips recompiling it. 我们要禁用的第一个功能是针对 C 和 C++ 源文件的更精确的变更追踪。Ninja 只是检查文件的修改时间是否改变,如果改变了就重新编译。相比之下,build2 在这种情况下会执行一个额外的步骤:它会对(部分预处理后的)源文件进行词法分析,并计算所得标记(tokens)的校验和。如果该校验和自上次编译以来没有改变,它就会跳过重新编译。
This ignores whitespace-only changes (as long as they do not alter the column numbers of the tokens) and is very useful during development (and is critical in some case, like if you want to change your project’s version with every commit). But tokenizing all the 299 translation units in the from-scratch build has an upfront cost, even if it may pay off during further incremental builds. 这会忽略仅包含空格的更改(只要它们不改变标记的列号),这在开发过程中非常有用(在某些情况下至关重要,例如如果你想在每次提交时更改项目版本)。但是,在从零开始的构建中对所有 299 个翻译单元进行词法分析是有前期成本的,即使它在后续的增量构建中可能会带来回报。
The way to disable this ignorable change detection is to tell build2 that the project is read-only (which is done automatically by the package manager for external dependencies). In this case build2 will fall back to using just the modification time, the same as Ninja. With this change our build time goes down to 3.4s, pretty much the same as Ninja’s: 禁用这种可忽略变更检测的方法是告诉 build2 该项目是只读的(包管理器对于外部依赖项会自动执行此操作)。在这种情况下,build2 将回退到仅使用修改时间,与 Ninja 一样。通过此更改,我们的构建时间降至 3.4 秒,与 Ninja 基本相同:
Time (mean ± σ): 3.433 s ± 0.055 s [User: 51.093s, System: 6.701s] Range (min … max): 3.383 s … 3.551 s 10 runs 时间(平均值 ± σ):3.433 秒 ± 0.055 秒 [用户:51.093 秒,系统:6.701 秒] 范围(最小值 … 最大值):3.383 秒 … 3.551 秒,运行 10 次
Let’s see if we can go even faster. Next, we disable compression in the file cache. We will discuss the file cache in more detail a bit later but for now let’s just say that by disabling compression we trade temporary disk space usage for speed: 让我们看看能否更快。接下来,我们禁用文件缓存中的压缩功能。稍后我们将更详细地讨论文件缓存,但现在只需说,通过禁用压缩,我们用临时的磁盘空间占用换取了速度:
Time (mean ± σ): 3.355 s ± 0.067 s [User: 49.987s, System: 6.153s] Range (min … max): 3.281 s … 3.471 s 10 runs 时间(平均值 ± σ):3.355 秒 ± 0.067 秒 [用户:49.987 秒,系统:6.153 秒] 范围(最小值 … 最大值):3.281 秒 … 3.471 秒,运行 10 次
And now we are 2.2% faster than Ninja! While this may not seem like much, it becomes more impressive considering build2 still does a lot more than Ninja. Some of this work is done by CMake and some is just not done at all. For example, build2 generates the XercesVersion.hpp header from XercesVersion.hpp.in as part of the build while Ninja leaves this to CMake. build2 also makes sure this file is properly change-tracked (while Ninja expects you to re-run CMake manually). This header is included in pretty much every translation unit in Xerces-C++, meaning that no compilation can start until it is generated. As an experiment, I hacked the build2 build file to pretend XercesVersion.hpp is static. That increased the gap to 2.6%. build2 also has to extract a lot more information from the compiler, something that in… 现在我们比 Ninja 快了 2.2%!虽然这看起来不多,但考虑到 build2 仍然比 Ninja 做了更多的工作,这就令人印象深刻了。其中一些工作是由 CMake 完成的,而有些工作 Ninja 根本不做。例如,build2 在构建过程中从 XercesVersion.hpp.in 生成 XercesVersion.hpp 头文件,而 Ninja 则将其留给 CMake。build2 还确保该文件得到正确的变更追踪(而 Ninja 则期望你手动重新运行 CMake)。这个头文件几乎被 Xerces-C++ 中的每个翻译单元包含,这意味着在它生成之前,任何编译都无法开始。作为实验,我修改了 build2 的构建文件,假装 XercesVersion.hpp 是静态的。这使得差距扩大到了 2.6%。build2 还必须从编译器中提取更多信息,这在……