The Little Things: Speeding up C++ compilation
codingnest.com
codingnest.com
It looks and sounds very good! Congratulations on making it to the release milestone after many years of hard work.
As a developer, I must say that I'm deeply impressed by the simulation complexity and incredible scalability.
I keep telling people that Factorio is the most complex train simulator game that I've ever played. But it isn't a train simulator. It's a factory game. The trains are optional.
That's crazy to me, that a subset of a game can be more complex than entire games that "specialise" in the topic.
After placing a track, click the end of the track and hold shift to use the "placement planner". Helps work out the curves and things.
I'd be interested to see if zapcc could speed up your non-unity build to be close to your unity build, without requiring the same compromises. I'd also be interested to see if zapcc would speed up single-file incremental builds in your unity build.
Ultimately I can't recommend zapcc for general use as it's a fork of clang that's not being regularly rebased anymore, so it's getting out of date. That's why I wish someone would pick it up again.
https://www.youtube.com/watch?v=b_T-eCToX1I
https://github.com/apple/swift-llbuild
I remember watching the talk awhile ago, but haven't kept up with its progress. It's about using LLVM as a library to eliminate duplicate work, as far as I remember.
I think it was for C++ as well as Swift.
This is one of the problems that should be solved by modules in the near future.
compared to what? The worst possible case? Code in weakly coupled modules? Using PIMPL idiom? Unity builds? Using ccache in incremental builds?
Especially ccache is a relevant comparison, because what to a developer is of far more interest than a full build is an incremental build with a few files changed. Using modules requires to scan the source code in order to determine dependencies, so it can be slower than an incremental build using other tools. And in addition to this, C++ compilation with include headers is an embarrassingly parallel problem which means speed advantages from using multi-core systems will probably very relevant in the, say, ten years time in which C++ modules will be somewhat standardized in practice.
Not to say they can't help, but in any other large software project one would need to show that something is useful before adding a big change.
Are modules fast? https://bfgroup.github.io/cpp_tooling_stats/modules/modules_...
One reason why this might not be so simple is that what is often measured as a potential for speed improvement is I/O, as in "lines of code a compiler needs to read to process a translation unit". However what happens is that C++ compilers are usually CPU-bound, not I/O bound. What happens also is that C++, very differently from idiomatic C, Pascal, Modula, and their successors, has no clear separation between declaration and use of an interface, as not functions, but classes form the main interface, and what defines a class is the signature of its member functions and its members, and to know the size of the class, the size of its members need to be known, which has the consequence that the C++ compiler needs to read recursively everything which the class is defined with. This is a big difference to modula etc. Also, of course the reading of data can be cached, but the question is how much the expansion of the read text can be cached - for example some will have different semantics if the DEBUG macro is set, or some WIN_xyz macro. As far as I understand, compilers supporting C++ modules will only cache the latest expansion.
Also, the build system with modules is likely to become more complex, with additional dependencies. I do not know whether this is still actual in every aspect (it could be that what is allowed to go into a module header has been narrowed down a bit), but this post : https://vector-of-bool.github.io/2019/01/27/modules-doa.html explains at least some of the topics involved, and not all of them are trivial.
An almost-should-be-mandatory tool on projects of any decent size is https://ccache.dev/
https://randomascii.wordpress.com/2020/03/30/big-project-bui...
The main takeaway:
> That is, the main source files represent just 0.32% of the lines of code being processed.
Chromium itself has about 12 million lines of code, but the compiler needs to process 3.6 billion lines of code because it needs to parse the same headers over and over again.
One thing I can tell you immediately about this unity build idea that is suboptimal for many codebases: modern compilers do not have much intra translation unit parallelism. If there's more in your project than one CPU core can compile quickly (i.e. most shipping products), it's going to be a serious bottleneck.
I agree with your objection, scaling up to 2-8 compilation units to use several cores is something I also plan to test/benchmark.
And to be honest, I don't completely understand why the classic way of compiling (makefiles + dozens separate files) is so slow but there is clearly something fishy as it is very often unbearable.
Slow compilation time can be very detrimental for productivity as it creates a way for the mind to wander and lose focus.
to give you a data point, in my case, https://github.com/OSSIA/score (fairly mundane C++ project totaling ~360kloc in roughly 1300 .cpp files / 1800 .hpp files, split across ~15 libraries, using boost, Qt and a few other common libs), unity builds (one .cpp per library) divides the time by 5 compared to PCH.
"Unity" builds certainly can speed up project compilation when used judiciously. This is easily shown by imagining an absurd C++ codebase where each method or function is in its own module. This would increase the overhead of parsing .h files. Any two functions that could be combined into one while keeping the h files included the same would almost certainly reduce this overhead. But I doubt your project would compile faster if all of the cc files were combined into one.
That's the musl codebase, in C. It does this in order to be able to remove unused code in a static build.
It compiles ridiculously fast. So the problem does not seem to be the amount of headers.
If only that were true. If you write modern C++, use the STL and popular libraries, even very small projects can take minutes to compile. IIRC, last time I tried this approach, a ~200 LoC throwaway project using the STL, Eigen and spdlog (which includes fmt) took almost a minute to compile an optimized build. I realize we may be using different definitions of "significant" here, but to me that's just unacceptable.
And, as others have already indicated, if your project really is large enough that you need parallelism, just have as many translation units as you have cores. It doesn't impede the idea of the unity build at all.
But the simplicity of the single compilation unit leads to very sizeable gains.
I plan to write an article on this very subject and post a few benchmarks.
If a build spends a lot of time processing code in headers, and source files in the same component are likely to include very similar sets of headers, this kind of configuration gives decent benefits, while not making incremental compilation much more expensive (and possibly cheaper, if you edited a header affecting many files in a single component).
However, C++'s standard library is a productivity blessing. I'm not convinced of the advantage of replacing it with ad-hoc components that may or may not be buggy just because you want to shave off a few seconds of each build.
More often than not, all you have to do to lower a double-digit percentage of your compilation time is to modularized your project, and follow basic principles like not have includes in your interface headers. This alone, along with PImpl, does far more for your compilation time than well-meaning but poorly thought-out tradeoff decisions like not using the STL.
In my opinion the STL is still a big, bloated, unreadable piece of code with a few badly implemented good ideas (containers)
You should read this article: https://zeux.io/2019/01/17/is-c-fast/
I can't say I'm particularly convinced. Yes, C++ debug builds can be slow. For most of us, that's ok. If it's not, make your "debug" build configuration a release build with the optimizer turned down (but not off) and incremental build enabled. Preprocessor definitions, symbol generation, optimization, and incremental build are all independent settings - if all you need from debug is symbols, incremental builds, and the inliner turned down (so stacks are retained) - then configure your compiler that way.
If you genuinely regard the standard library as a productivity blessing I'm sure this looks like an unreasonable tradeoff. I would be very surprised to see someone advocate writing "Orthodox C++" solely for the compile times. Most of us who write code like this do so because we don't find value in the STL and regard templates as an extremely poor tool for their ostensible purpose. The compile times are just a bonus, albeit a very pleasant one.
In truth, the divide isn't between people who prioritize compile times over any other consideration and people who don't. It's between people who see most of the language features of C++ as valuable and people who see them as mostly unhelpful. Personally, I like operator overloading for math and SIMD types, function overloading for less verbosity, namespaces and a bit of constexpr for convenience. The rest I'm quite happy to do without, so there isn't much of a tradeoff to make.
I'd go one step further. Skip the compiler and just write in assembly. Your builds will be lightning fast especially if you use one single source file with no macros and static linking. You'll never ship, but you'll never ship really fast.
Or anywhere else. C++ is such a beast, you just can't expect your coworkers to memorize the whole standard, so it is quite common that teams agree (or the team-leader agrees for them) upon a subset.
In the past that was due to limited availability of experienced C++ programmers and inconsistent support from compilers, these days it's due to the evolving nature of the language. Quick: in which standard was the variant class template introduced?
If you use Eigen but somehow feel that compilation time is your focus instead of actually doing linear algebra or solving systems of linear equations then you have a few best practices that you can follow and techniques at your disposal to lower compilation times.
Unfortunately modules are several years in the future, both due to lack of implementations yet and the fact that few projects are being written in C++20 yet. I also harbour a suspicion that tricks like these will still be useful with modules.
Now, most of the time, the trade off of repeating
class Thing;
is more than offset by faster build times.But if
class Thing;
is actually namespace Bob
{
namespace Dave
{
template <typename T>
class ThingTrait;
template <typename T>
class Thing : BaseClass<T, allocator<T>, trait<ThingTrait<T> >;
}
}
you really want to put that in it's own header, typically "Thing_fwd.h"(apologies for any syntax errors, been a while since I've written C++)
#include "thing.h"
or class Thing;
Either way, as a practical matter, you're saying, "There's something called Thing I'm going to use." C++ forces you to say that before use in each translation unit. It's not really "repeating" yourself to write it, since you haven't "said" it yet in this TU. class Thing;
in every file that uses a Thing. And as I said, that's generally fine.The issue becomes when the declaration of Thing includes namespaces, template params, etc. Rewriting that is tedious and error prone.
Look at something like std::string. That's actually
namespace std {
template< class T > struct allocator;
template<class charT> struct char_traits;
template<> struct char_traits<char>;
template<class CharT, class Traits = char_traits<CharT>,
class Allocator = allocator<CharT>>
class basic_string;
using string = basic_string<char>;
}
No-one is forward declaring that.If you change Thing to an alias to a class template specialization then all of the "class Thing;" forward declarations break.
Note that discc doesn't really solve the header problem, it just throws more compute at it.
Very useful if you are working on very large codebases (I hack on LibreOffice, 10M LOC, from time to time).
Note that this is not required, and most std::string implementations inline small strings anyways.
If your real life benchmark heavily penalises this addition (and not the complementary subtraction when computing a size or a capacity) I ask you to volunteer it.
It does happen that the only thing the point in the article relies on is `sizeof`, for sure.