Optimizing the unoptimizable: a journey to faster C++ compile times
vitaut.net
vitaut.net
EDIT:
This compiles in 1 second on an i7 laptop.
import std;
int main()
{
std::cout << "Hello World!\n";
}Also Microsof Office team has been migrating to modules, with great build improvements.
https://devblogs.microsoft.com/cppblog/integrating-c-header-...
Key takeway
"Fortunately, we were able to show a build performance improvement great enough that the team agreed to adopt header units into the Office production build system alongside msvc 17.6.6!"
Unfortunely the performance findings blogpost is still WIP.
I don't agree. The real solution for fast compilation times is to not have to recompile things. This means onboarding tools like ccache, organize your project around independent subprojects which eliminate/minimize compile-time dependencies, and leverage incremental builds.
There's a C++ book somewhere that describes how the subprojects approach helps lower build times to a residual length which if my memory doesn't fail me was dubbed horizontal architecture. It consists of designing every single subprojects to be stand-alone and leave any integration to the linking stage. I've used it in the past on a legacy C++ project that took slightly over 1h to pull off an end-to-end build, and peeling out 4 or 5 subprojects following the horizontal architecture approach allowed me to cut down full end-to-end build times to slightly over 8 minutes and incremental builds down to less than a minute, without bothering with compiler caches. I'm sure that if I bothered onboarding ccache I could drive incremental build times to just a few seconds.
If you know what you are doing, you can do a lot without begging for magic.
C++ does not "need" more than that. You personally might find it more convenient if you don't have to think through your software architecture, but the truth of the matter is that you only need to spend a few minutes looking at your project to speed everything up, which is exactly what everyone does the very moment they feel they have a problem.
It makes no sense at all to demand a whole tech stack to change around you when you can't even spend a few minutes looking for an answer to the problem you have.
> There's some ridiculous template heavy code out there where the majority of time is spent linking.
That is not a problem. Templates are only special in a build because compilers spend time generating code. Again, you can work around that without any problem at all with the tools available to you for the past two or three decades.
Properly modularizing your app with explicit template instantiation already drives down the build time of any naive project structure to a fraction of the time, and basic stuff like onboarding a compiler cache tool and moving template code out of interface headers is enough to get the linking stage to be the most expensive step of a build.
> You can't even do a debug build without optimizations on Windows with these programs because the coff file format can't handle it, even when compiling with "/bigobj".
I'm sorry but this is simply not true. I suggest you start to look at your projects to break it down to subprojects and see where the critical path of your build is. There is absolutely no project in the world whose build would not take less than a minute (or even a few seconds) with incremental builds, even if it's code that uses template metaprogramming extensively.
However, I will still challenge you on your build time/object size claim. Take a codebase structured around lazy tasks (i.e. every function is a coroutine), factor in runtime parallelism/SIMD dispatch, multiply with loop abstractions (think std::ranges) and you get frighteningly close to linker limits in no time (which, of course, also implies minutes of compilation per source file). Each of those pieces is templates through and through, and there's no explicit template instantiation way out of any of them.
Also, I have to (sadly) point out that explicit template instantiation declarations only take you so far. Yes, they reduce compiler time spent in codegen, but also cause the compiler to create all required transitive (!) declarations in every TU, which will quickly offset those gains when talking about template types that have lots of small member functions.
Nothing in your example suggests long compilation times are a hard requirement. Just move your components into submodules and remove template code from interfaces. Your build only needs to recompile what you tell it to recompile.
> Also, I have to (sadly) point out that explicit template instantiation declarations only take you so far.
It takes you as far as you need to go. You instantiate what you need, you move your template code into submodules and out of interface headers, and you're set. This is not sourcery. It's common sense.
I recommend you read "large scale C++,vol1 - process and architecture" to get acquainted with basic C++ techniques to structure your project so that builds don't waste time chewing through code they don't need to touch.
Again, I am not objecting to the general advice you give, but maybe you can take a step back and appreciate that not every code base and C++ use case falls into the range of what you have seen and interacted with so far. There is no doubt that a vast majority of C++ code bases would benefit immensely from improving compile-time encapsulation, but that is not the be-all-end-all of solving long compilation times, and it does not give you grounds for dismissing concerns of people who have done that and still face different problems.
> You instantiate what you need, you move your template code into submodules and out of interface headers, and you're set.
You seem to have completely missed my point. I implore you to consider for a second that I might be familiar with what you are trying to tell me, and that there are indeed complications beyond the basic techniques you advocate for. Let me illustrate with an example.
https://godbolt.org/z/5j43WrM68
Here we have a very simple class that uses a few std library types. It is a template, but say that we know that it is only valid to use it with the shown basic arithmetic types (note 1). The class implementation and according explicit template instantiation definitions have therefore been moved to a separate TU (not shown) and we only have the shown code in the header. This is a simple application of those "basic C++ techniques" you mention.
What happens to the compilation time if we enable the explicit template instantiation declarations in the header? Measuring on godbolt is noisy, but by repeatedly changing the source file (e.g. just adding spaces) you can quickly get a bunch of measurements. The lowest value of 10-20 measurements is going to be a reasonable proxy for real-world compilation performance. And if you perform these measurements with and without the ETIDeclarations enabled, you will notice that they cause this short piece of code to take 100-200 ms longer to compile. Mind you, these are explicit template instantiation declarations - they tell the compiler that it does not have to generate code for these template instantiations. However, they do still force the compiler to instantiate all the declarations of all involved transitive templates. Explicit-template-instantiation-declarating X<int> requires the compiler to (internally, in some abstract way) note down the existence of every last std::vector<int>::const_reverse_iterator::operator-=(size_t) and so on (note 2). That's a lot of nested templates that it needs to (abstractly) "declare", even if it never has to actually generate code for them, which explains why the mere presence of the ETIDeclarations slows down compilation measurably - in every single TU that includes this header!
Note 1: It is, in my experience, relatively rare that the set of valid template arguments is closed and not open - in a way, an open set of permissible types is the whole point of writing templates. And such a templates intentionally being provided across a module boundary is also all but rare for lower level libraries in a bigger code base.
Note 2: It's easy to underestimate the amount of template nesting in e.g. the standard library too. Here's a single typedef in std::vector (with two further levels of typedefs expanded):
using const_reverse_iterator = _STD reverse_iterator<_Vector_const_iterator<_Vector_val<conditional_t<_Is_simple_alloc_v<_Alty>, _Simple_types<_Ty>,
_Vec_iter_types<_Ty, size_type, difference_type, pointer, const_pointer, _Ty&, const _Ty&>>>>>;