Faster Than Ninja
Posted by elasticdog 1 day ago
Comments
Comment by evmar 1 day ago
As they observe, Ninja gets to be fast mostly by cheating: it avoids a lot of work by saying many things are just out of scope for Ninja to do, and that means it is a useful a target to race against. (Funny thing: when I wrote Ninja I was misremembering how fast an earlier build system was so I kept trying to make it faster. So don't treat it as a lower bound, I just made it up!)
I comment here to say I find the explanation for 'why' in this post unsatisfying. They mention three design decisions.
The first one is a criticism of CMake, not Ninja (?), so I don't think it can be why. I might have misunderstood?
The second reason given is doing some work like header dependencies in multiple threads. This is the most plausible reason to me but it still feels unlikely. It's a very small amount of work: the post mentions 300 compiles, so maybe parsing 300 small text files?
The third is that they run the compiler up front an additional time to gather headers, which is strictly more work than Ninja. There is some hand waving about file access patterns but I am skeptical; if the end-to-end build time is 3 seconds then the project is small enough to all fit in kernel caches. They also mention doing other things like invoking the compiler to get version information. This seems like it would dwarf any performance gain from number 2.
Maybe it's just my own curiosity, I think this post would be better if it had a better explanation for the reason. I'm not disputing the result, I just think the result should make you suspicious that something else is going on, and you might learn something from that! You could for example explore whether it's the header dependency thing by profiling the Ninja invocation and seeing if it's waiting for CPU or waiting for tasks to execute.
(If I had to guess without looking at any of the involved code, I would predict it's something about how CMake generates the build, like it introduces serialization in a place where build2 is parallel, or it adds some extra build steps like gathering the current git hash into a header file or something.)
Comment by boris 1 day ago
> The first one is a criticism of CMake, not Ninja (?), so I don't think it can be why.
Fair enough. The point I was making is that if you want to compete with Ninja, you cannot leave any potential performance gains on the table.
> The second reason given is doing some work like header dependencies in multiple threads. This is the most plausible reason to me but it still feels unlikely.
We are talking about ~2% performance difference here. Parallelizing even a small amount of work across 24 threads rather that doing it serially saving a percent or two feels plausible to me.
> There is some hand waving about file access patterns but I am skeptical; if the end-to-end build time is 3 seconds then the project is small enough to all fit in kernel caches.
It fits into the system's file cache unless there is memory pressure, like one would expect from having 24 C++ compiler jobs running in parallel. We actually measured this in isolation (with more detailed results in the linked article) and it has a measurable effect.
> They also mention doing other things like invoking the compiler to get version information. This seems like it would dwarf any performance gain from number 2.
I measured this, it costs 70ms or ~2% of the overall time.
Comment by menaerus 22 hours ago
300 TUs is not much. If they build in 3 seconds then they are trivial (small). If the machine is 24-thread (I assume some sort of heterogeneous 12-core), how little RAM does the machine have for the kernel to start evicting page cache during the build?
Comment by vlovich123 1 day ago
Couldn’t you amortize this to 0 by just caching the result and only changing it if the binary timestamp changed?
Comment by boris 1 day ago
Yes, that would be nice, but the tricky question is can any of this information change without the compiler binary mtime changing? First off, GCC's gcc/g++ binaries are drivers and are not what does the actual compilation, it's private cc1/cc1plus binaries that do the job. Can one of these change but not the driver? I think it's plausible (some package manager optimization where the file is not touched if it hasn't changed). So at a minimum we would need to discover where those are located (probably by invoking gcc/g++) and checking them as well. Could there be something else? Who knows. We value speed very much but we value correctness even more.
I think a more fruitful direction to explore is to improve GCC itself to dump all this information in a single invocation and in a machine-readable format (JSON). I think if we go from 70ms to 14ms (and perhaps even lower because this special GCC mode could conceivably do things faster than how we do it now), it would be good enough.
Comment by Orphis 1 day ago
And if they are in their own targets, they are not really an issue (they would serialize everything that depends on them as you'd expect), but if you have them in a library grouped with other files to compile, then the whole library compilation is serialized.
And obviously worse if you also have to build the generator for the generated files, but that's not a big surprise, you can observe that in full builds of Chromium or its libraries too waiting for protoc if you crank the parallelization a lot.
Comment by evmar 1 day ago
Comment by Alecazam 1 day ago
Would be nice to see build2 go this route.
Comment by evmar 1 day ago
Comment by yaris 1 day ago
Comment by Orphis 1 day ago
Nowadays, I think a build tool that doesn't natively support distributed caching (and possibly remote execution) is a weird choice. And I don't think that spawning layers of processes allows for good parallelization as you don't know if an action is going to be network bound or compute bound and the job slot is then spent. So you either oversubscribe or undersubscribe.
Comment by oso2k 14 hours ago
Part of the motivation to use processes is because their structure helps to keep the job generic, uni
Comment by Alecazam 11 hours ago
Comment by majoe 23 hours ago
They seem to create a dynamic nix derivation per compilation unit, which would be very similar to what you describe as manifests in your post as it also creates a hash of all inputs.
Would be interesting to here your opinion on that approach
Comment by zX41ZdbW 1 day ago
The speed (reading the build.ninja file) was never a concern for us. If I could share a wish-list, it will be:
- Fix the possibility of a segmentation fault when the build file is damaged;
- Use a better order of execution: https://github.com/ninja-build/ninja/issues/2157
Comment by bellowsgulch 1 day ago
Comment by evmar 1 day ago
Comment by feelamee 1 day ago
Comment by bellowsgulch 1 day ago
Comment by eska 1 day ago
Since they had to rewrite the build file for their program I also assume that something is missing. Didn’t see any mention of verifying that.
I also really didn’t like their denigrating tone. It totally turns me off trying build2, because it seems they don’t understand the point of separating build stages like environment setup (getting dependencies), configure, native build, cross build, packaging. I am a very happy ninja user instead of a batteries-included solution because it does its one job well and can be used very flexibly. I personally detest cmake, so I use nix + own configure script + ninja.
The blog is also wrong about ninja being unable to call configure, but I intentionally don’t want that (I want the build stages to communicate in one direction for sanity).
Comment by Orphis 1 day ago
That's easy to do if you control the whole pipeline and can integrate all the features together, not so much with the CMake model unfortunately. I think it would be nice if CMake had Ninja integrated as a library, it could lead to some nice optimizations later.
Comment by bluGill 1 day ago
Comment by Orphis 1 day ago
I recently (a few weeks ago) made a project to batch those type of commands together and parallelize tests as much as possible. You can check a merge request for CMake upstream here: https://gitlab.kitware.com/cmake/cmake/-/merge_requests/1226... and the associated issue for more details.
In general, CMake is fast enough, the only slow bit on the critical path is detecting the toolchain. The rest is quick, but if your build scripts are using slow serialized functionality, you'll be paying that price. But only once as the slow results are cached, so next incremental run should be pretty fast!
Comment by vient 1 day ago
* Because in CMake there are several target properties that may be affected by the "parent" (another target which added this target as dependency), meaning that these properties need to be re-evaluated in the context of each root target dependent on this one. If I remember correctly, exact mechanism is that some properties can have generator expressions, and these expressions can reference "parent" target. Now we imagine an app which has some std lib with 1k targets, and 100 top-level executables dependent on this std lib - suddenly we have 100k target evaluations in generation step.
Comment by bluGill 16 hours ago
Comment by vient 16 hours ago
Comment by Meneth 1 day ago
Comment by actionfromafar 1 day ago
Comment by zamalek 1 day ago
Something is wrong here. Which compression algorithm is being used here and how much has it been tuned? A core hypothesis of the likes of zram is that disk access is so slow (even NVME), that you can often beat it with the bit-rate of decompression.
1. Is something slow like gzip being used?
2. Is the compression effort over-tuned for size? Do some space benchmarks and make sure that you aren't saving a few dozen MB on GBs of data.
zstd, with 1-3 effort (you may even find negative is a overall win), and a trained dictionary (your data does all look identical) is probably a good start.
Comment by boris 1 day ago
Comment by shevy-java 1 day ago
Comment by Orphis 1 day ago
In practice, it does show the limitations of the CMake model though. You should not be paying the price to generate the build files for bits you don't care about. You should be able to just load the CMake files and build a target, ignoring the rest.
This is how tools like Bazel function and that allows them to scale to very large monorepos. Build files are only processed when a target from that folder is built directly or used. CMake has to load everything as it does not know about the build tree and the maintainer has to manage exclusion lists or inclusion lists.
Comment by wpollock 1 day ago
Everyone's workflow is different. Your's obviously works for you and many others.
But I like local man pages. They get updated at the same time as the software is updated. Sometimes I have an older version of software and the online version is for a newer version, or vice-versa. Local man pages are always available and generally quicker than accessing them online.
If the machines in question are mult user, you should always install local documentation regardless of your personal preferences.
Comment by sramsay 1 day ago
They do care that the code can compile on their (possibly old) machine. They do care about the ability to include some features and not others (including their ability to not have to make decisions on this if they don't want to). And they certainly want documentation that helps them to achieve their goal (which, again, is to build the software).
The newer tools make substantial inroads in making the developer's life easier. If your software is not of the sort where people download it and try to build it (because it only ships as a closed binary, is entirely internal, etc.) then maybe that makes sense. But until all these fancy gadgets start seriously caring about the use case above, I'm going to stick with autotools.
Comment by bluGill 1 day ago
In my experience is it doesn't work. Sure it can be made to work and autotools asks all the right questions. However in the real world I have yet to see an autotools project that works out of the box in my custom environment.
Meanwhile cmake just worked every time when I plugged in the toolchain file that I created for the very first project I needed to build for my environment.
Again, it isn't that autotools cannot do what is claims. However nobody gets the details right and so something doesn't work. That something is different for every project. In the end what works is either what someone takes the time to make work and those things that are "very close". My project is cross compiling linux, which cmake does well (autotools also messed something up, often failing to use the sysroot paramater where needed)
Comment by quotemstr 1 day ago
When, from first principles, something shouldn't be faster, yet it is, you dig and dig until you understand. You don't just say your shit rules and the other thing sux0rz. If you do, you'll regret it: if you don't understand how you got a win, you don't understand how to keep it.
Comment by ladams 1 day ago
This claim isn't supported by the author's measurements?
Comment by karen_arutyunov 1 day ago
no?
Comment by boris 1 day ago