It really boils down to two rules:
1. Don't declare anything in header files that is only used in one compilation unit. Internal structs and functions should be declared and defined in source files, and internal linkage used wherever possible. gcc and clang's -fvisibility=hidden is useful here.
2. The more frequently a header file is included (whether transitively or directly), the more it should be split up. If a "common" or "utility" header file is included in 10000 source files, then any struct, function, etc. that you add to that file will have to be parsed 10000 times by the compiler every time you build from scratch, even if only 10 source files actually use the struct/function that you added. gcc and clang's -H flag is useful here.
[1] https://lore.kernel.org/lkml/YdIfz+LMewetSaEB@gmail.com/
That's what its always was about (to improve build times), better optimization is just a welcome side effect. But header hygiene is hard because the problem will creep back into the code base over time.
> The Linux kernel project was able to net a ~40% reduction in compilation CPU-time
Linux is a C codebase. Header hygiene is much easier in C, because C headers usually only contain interface declarations (usually at most a few hundred lines of function prototypes and struct declarations), while C++ headers often need to include implementation code inside template functions, or are plastered with inline functions (which in turn means more dependencies to include in the header). And even if the user headers are reasonably 'clean', they still often need to include C++ stdlib headers which then indirectly introduce the same problem.
For instance your point (2) only makes sense if this header doesn't need to include any of the C++ stdlib headers, which will add tens of thousands of lines of code to each compilation unit. For such cases you might actually make the problem worse by splitting big headers into many smaller ones.
PS: the most effective, but also most radical and controversial solution is also a very simple one: don't include headers in headers.
But this also reduces the opportunity to parallelize compilation across multiple files because they have been concatenated into fewer build units, and each unit now requires more memory to deal with the non-header parts. For some build systems and repositories, this actually increases build time.
Irrelevant. There is always significant overhead in handling multiple translation units, and unity builds simply eliminate that overhead.
> and each unit now requires more memory to deal with the non-header parts.
And that's perfectly ok. You can control how large unity builds are at the component level.
> For some build systems and repositories, this actually increases build time.
You're creating hypothetical problems where there are none.
In the meantime, you're completely missing the main risk of unity builds: increasing the risk of introducing problems associated with internal linkage.
as in the article, it's best to support both
For instance, just including <vector> in a C++ source file adds nearly 20kloc of code to the compilation unit:
https://www.godbolt.org/z/56ncqEqYs
If your project has 100 source files, each with 100 lines of code but each file includes the <vector> header (assuming this resolves to 20kloc), you will compile around 2mloc overall (100 * 20100 = 2010000).
If the same project code is in a single 10kloc source file which includes <vector>, you're only compiling 30kloc overall (100 * 100 + 20000 = 30000).
In such a case (which isn't even all that theoretical), you are just wasting a lot of energy keeping all your CPU cores busy compiling <vector> a hundred times over, versus compiling <vector> once on a single core ;)
It is a simple thing to do, and the gains are substantial, faster and simpler, less maintenance, especially across different platforms.
For big projects I simply cut them into several libraries.
I've seen some incredulous reactions, mostly from young coders, and I know that makefiles should be faster, but in practice I never found that to be true.
You can lurk but surely you can't lurk into something?
Edit: They appear to be french: http://serge.liyun.free.fr/serge/
About 20 years ago, on UNIX workloads we used to speed the compilation via ClearMake, a kind of distributed version of code cache that would plug into the compilers, however it has part of ClearCase SCM product.
On Windows, with Microsoft and Borland (nowadays Embarcadero), they work quite alright.
Also, modules will fix that, as per VC++ reports, importing the whole standard library (import std, as per C++23) takes a fraction of only including iostream.
Precompiled headers don't play nicely with distributed compilation or shared build caches (which are perhaps the fastest way to build large C++ codebases). So while they can work well for local builds, they exclude the use of (IMO) better build-time optimisations.
They also require maintenance over time- if you precompile a bad set of headers it can make your compile times worse.
Some build systems like cmake already support unity builds, as this is a popular strategy to speed up builds.
Nevertheless, if speed is the main concern them it's preferable to just use a build cache like ccache, and modularize a project appropriately.
So many hacks in compilers to try to work around this. A shame there is no language level fix for this nonsense.
Really wish there could be a C++—- that would improve on C in areas like this, and avoid all the incredible nonsense of C++. And no, not Rust or Go.
Headers only became a massive problem in C++ because of templates and the unfortunate introduction of the inline keyword (which then unfortunately also slipped into C99, truly the biggest blunder of the C committee next to VLAs).
Typical C headers (including the C stdlib headers) are at most a few hundred lines of function prototypes and struct declarations.
Typical C++ headers on the other hand (include the C++ stdlib headers) contain a mix of declarations and implementation code in template and inline functions and will pull in tens of thousands of lines of code into each compilation unit.
This is also the reason why typical C projects compile orders of magnitude faster than typical C++ projects with a comparable line count and number of source files.
C Macros are pretty much considered code smell in C++, right?
But we keep a separate CI job that checks the non-unity build, so developers have to add the right #include statements and can't accidentally reference file-scoped functions from other files. While working on a given library or project, developers often disable the unity build for just that project to reduce incremental build times. It seems to offer the benefits of both approaches.
Precompiled headers don't give nearly the same speedup. We're excited for C++ modules of course, but we're trying to temper any expectations that modules will improve build speed.
All of those things combined make C programming more enjoyable.
What exactly leads you to have multiple declarations in sync, and thus creating the to "keep [multiple] declarations synced"?
And what were the resulting affects on build times?
(HTTP 301 on the old URL would have been appreciated)
Are you working on large compiled software? Any game, rendering engine or large application benefits from compilation units in my experience.
Some of my libraries that I work with take upwards of an hour for a fresh compile. Having sane compilation units cuts down subsequent iteration to minutes instead.
And that was with unified builds enabled.
A brand new AMD Epyc, 64 core machine, will take over an hour to compile. Good times.
I suspect there’s compilation optimizations to be made, but I don’t think it would save more than 30% here and there.