Hacker News new | ask | show | jobs
by MauranKilom 884 days ago
I am well aware of the techniques you mention, and intimitely familiar with inspecting include trees under the light of "does it make sense for this code to be recompiled if this code changes?", particularly across module boundaries. To clarify, I'm talking about cases of multiplicative object code growth from layering templated abstractions (as per my reply) causing a single object file with a clear singular purpose to grow beyond linker limits. Not because it does too many things or needlessly compiles in a whole bunch of mistakenly-in-interface-headers templates from other modules all over the place; those would be easy to fix. But this case is not solved by "move code into submodules" (there is nothing to further sub-modularize) and "move templates out of headers" (that translation unit uses and instantiates precisely the templates that are reasonable to involve). Yes, those are the easiest fat to trim from compilation times, I know and agree, but I'm trying to point out to you that not every template in a header is a mistake, and that object code size can grow beyond expectations even if those basic techniques have been exhausted. (And to be clear, we of course use incremental builds, but that's not even relevant to object size limits).

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&>>>>>;