Why does C++ compilation take so long?

Viewed 150829

Compiling a C++ file takes a very long time when compared to C# and Java. It takes significantly longer to compile a C++ file than it would to run a normal size Python script. I'm currently using VC++ but it's the same with any compiler. Why is this?

The two reasons I could think of were loading header files and running the preprocessor, but that doesn't seem like it should explain why it takes so long.

15 Answers

Several reasons

Header files

Every single compilation unit requires hundreds or even thousands of headers to be (1) loaded and (2) compiled. Every one of them typically has to be recompiled for every compilation unit, because the preprocessor ensures that the result of compiling a header might vary between every compilation unit. (A macro may be defined in one compilation unit which changes the content of the header).

This is probably the main reason, as it requires huge amounts of code to be compiled for every compilation unit, and additionally, every header has to be compiled multiple times (once for every compilation unit that includes it).

Linking

Once compiled, all the object files have to be linked together. This is basically a monolithic process that can't very well be parallelized, and has to process your entire project.

Parsing

The syntax is extremely complicated to parse, depends heavily on context, and is very hard to disambiguate. This takes a lot of time.

Templates

In C#, List<T> is the only type that is compiled, no matter how many instantiations of List you have in your program. In C++, vector<int> is a completely separate type from vector<float>, and each one will have to be compiled separately.

Add to this that templates make up a full Turing-complete "sub-language" that the compiler has to interpret, and this can become ridiculously complicated. Even relatively simple template metaprogramming code can define recursive templates that create dozens and dozens of template instantiations. Templates may also result in extremely complex types, with ridiculously long names, adding a lot of extra work to the linker. (It has to compare a lot of symbol names, and if these names can grow into many thousand characters, that can become fairly expensive).

And of course, they exacerbate the problems with header files, because templates generally have to be defined in headers, which means far more code has to be parsed and compiled for every compilation unit. In plain C code, a header typically only contains forward declarations, but very little actual code. In C++, it is not uncommon for almost all the code to reside in header files.

Optimization

C++ allows for some very dramatic optimizations. C# or Java don't allow classes to be completely eliminated (they have to be there for reflection purposes), but even a simple C++ template metaprogram can easily generate dozens or hundreds of classes, all of which are inlined and eliminated again in the optimization phase.

Moreover, a C++ program must be fully optimized by the compiler. A C# program can rely on the JIT compiler to perform additional optimizations at load-time, C++ doesn't get any such "second chances". What the compiler generates is as optimized as it's going to get.

Machine

C++ is compiled to machine code which may be somewhat more complicated than the bytecode Java or .NET use (especially in the case of x86). (This is mentioned out of completeness only because it was mentioned in comments and such. In practice, this step is unlikely to take more than a tiny fraction of the total compilation time).

Conclusion

Most of these factors are shared by C code, which actually compiles fairly efficiently. The parsing step is a lot more complicated in C++, and can take up significantly more time, but the main offender is probably templates. They're useful, and make C++ a far more powerful language, but they also take their toll in terms of compilation speed.

Parsing and code generation are actually rather fast. The real problem is opening and closing files. Remember, even with include guards, the compiler still have open the .H file, and read each line (and then ignore it).

A friend once (while bored at work), took his company's application and put everything -- all source and header files-- into one big file. Compile time dropped from 3 hours to 7 minutes.

The slowdown is not necessarily the same with any compiler.

I haven't used Delphi or Kylix but back in the MS-DOS days, a Turbo Pascal program would compile almost instantaneously, while the equivalent Turbo C++ program would just crawl.

The two main differences were a very strong module system and a syntax that allowed single-pass compilation.

It's certainly possible that compilation speed just hasn't been a priority for C++ compiler developers, but there are also some inherent complications in the C/C++ syntax that make it more difficult to process. (I'm not an expert on C, but Walter Bright is, and after building various commercial C/C++ compilers, he created the D language. One of his changes was to enforce a context-free grammar to make the language easier to parse.)

Also, you'll notice that generally Makefiles are set up so that every file is compiled separately in C, so if 10 source files all use the same include file, that include file is processed 10 times.

C++ is compiled into machine code. So you have the pre-processor, the compiler, the optimizer, and finally the assembler, all of which have to run.

Java and C# are compiled into byte-code/IL, and the Java virtual machine/.NET Framework execute (or JIT compile into machine code) prior to execution.

Python is an interpreted language that is also compiled into byte-code.

I'm sure there are other reasons for this as well, but in general, not having to compile to native machine language saves time.

Another reason is the use of the C pre-processor for locating declarations. Even with header guards, .h still have to be parsed over and over, every time they're included. Some compilers support pre-compiled headers that can help with this, but they are not always used.

See also: C++ Frequently Questioned Answers

The biggest issues are:

1) The infinite header reparsing. Already mentioned. Mitigations (like #pragma once) usually only work per compilation unit, not per build.

2) The fact that the toolchain is often separated into multiple binaries (make, preprocessor, compiler, assembler, archiver, impdef, linker, and dlltool in extreme cases) that all have to reinitialize and reload all state all the time for each invocation (compiler, assembler) or every couple of files (archiver, linker, and dlltool).

See also this discussion on comp.compilers: http://compilers.iecc.com/comparch/article/03-11-078 specially this one:

http://compilers.iecc.com/comparch/article/02-07-128

Note that John, the moderator of comp.compilers seems to agree, and that this means it should be possible to achieve similar speeds for C too, if one integrates the toolchain fully and implements precompiled headers. Many commercial C compilers do this to some degree.

Note that the Unix model of factoring everything out to a separate binary is a kind of the worst case model for Windows (with its slow process creation). It is very noticable when comparing GCC build times between Windows and *nix, especially if the make/configure system also calls some programs just to obtain information.

Some reasons are:

1) C++ grammar is more complex than C# or Java and takes more time to parse.

2) (More important) C++ compiler produces machine code and does all optimizations during compilation. C# and Java go just half way and leave these steps to JIT.

A compiled language is always going to require a bigger initial overhead than an interpreted language. In addition, perhaps you didn't structure your C++ code very well. For example:

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

Compiles a lot slower than:

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

The trade off you are getting is that the program runs a wee bit faster. That may be a cold comfort to you during development, but it could matter a great deal once development is complete, and the program is just being run by users.

Most answers are being a bit unclear in mentioning that C# will always run slower due to the cost of performing actions that in C++ are performed only once at compile time, this performance cost is also impacted due runtime dependencies (more things to load to be able to run), not to mention that C# programs will always have higher memory footprint, all resulting in performance being more closely related to the capability of hardware available. The same is true to other languages that are interpreted or depend on a VM.

To answer this question simply, C++ is a much more complex language than other languages available on the market. It has a legacy inclusion model that parses code multiple times, and its templated libraries are not optimized for compilation speed.

Grammar and ADL

Let's have a look at the grammatical complexity of C++ by considering a very simple example:

x*y;

While you’d be likely to say that the above is an expression with multiplication, this is not necessarily the case in C++. If x is a type, then the statement is, in fact, a pointer declaration. This means that C++ grammar is context-sensitive.

Here’s another example:

foo<x> a;

Again, you might think this is a declaration of the variable "a" of type foo, but it could also be interpreted as:

(foo < x) > a;

which would make it a comparison expression.

C++ has a feature called Argument Dependent Lookup (ADL). ADL establishes the rules that govern how the compiler looks up a name. Consider the following example:

namespace A{
  struct Aa{}; 
  void foo(Aa arg);
}
namespace B{
  struct Bb{};
  void foo(A::Aa arg, Bb arg2);
}
namespace C{ 
  struct Cc{}; 
  void foo(A::Aa arg, B::Bb arg2, C::Cc arg3);
}

foo(A::Aa{}, B::Bb{}, C::Cc{});

ADL rules state that we will be looking for the name "foo" considering all arguments of the function call. In this case, all of the functions named “foo” will be considered for overload resolution. This process might take time, especially if there are lots of function overloads. In a templated context, ADL rules become even more complicated.

#include

This command is something that might significantly influence compilation times. Depending on the type of file you include, the preprocessor might copy only a couple of lines of code, or it might copy thousands.

Furthermore, this command cannot be optimized by the compiler. You can copy different pieces of code that can be modified just before inclusion if the header file depends on macros.

There are some solutions to these issues. You can use precompiled headers, which are the compiler's internal representation of what was parsed in the header. This can’t be done without the user's effort, however, because precompiled headers assume that headers are not macro dependent.

The modules feature provides a language-level solution to this problem. It’s available from the C++20 release onward.

Templates

The compilation speed for templates is challenging. Each translation unit that uses templates needs to have them included, and the definitions of these templates need to be available. Some instantiations of templates end up in instantiations of other templates. In some extreme cases, template instantiation can consume lots of resources. A library that uses templates and that was not designed for compilation speed can become troublesome, as you can see in a comparison of metaprogramming libraries provided at this link: http://metaben.ch/. Their differences in compilation speed are significant.

If you want to understand why some metaprogramming libraries are better for compilation times than others, check out this video about the Rule of Chiel.

Conclusion

C++ is a slowly compiled language because compilation performance was not the highest priority when the language was initially developed. As a result, C++ ended up with features that might be effective during runtime, but are not necessarily effective during compile time.

P.S – I work at Incredibuild, a software development acceleration company specializing in accelerating C++ compilations, you are welcome to try it for free.

In large object-oriented projects, the significant reason is that C++ makes it hard to confine dependencies.

Private functions need to be listed in their respective class' public header, which makes dependencies more transitive (contagious) than they need to be:

// Ugly private dependencies
#include <map>
#include <list>
#include <chrono>
#include <stdio.h>
#include <Internal/SecretArea.h>
#include <ThirdParty/GodObjectFactory.h>

class ICantHelpButShowMyPrivatePartsSorry
{
public:
    int facade(int);

private:
    std::map<int, int> implementation_detail_1(std::list<int>);
    std::chrono::years implementation_detail_2(FILE*);
    Intern::SecretArea implementation_detail_3(const GodObjectFactory&);
};

If this pattern is blissfully repeated into dependency trees of headers, this tends to create a few "god headers" that indirectly include large portions of all headers in a project. They are as all-knowing as god objects, except that this is not apparent until you draw their inclusion trees.

This adds to compilation time in 2 ways:

  1. The amount of code they add to each compilation unit (.cpp file) that includes them is easily many times more than the cpp files themselves. To put this in perspective, catch2.hpp is 18000 lines, whereas most people (even IDEs) start to struggle editing files larger than 1000-10000 lines.
  2. The number of files that have to be recompiled when a header is edited is not contained to the true set of files that depend on it.

Yes, there are mitigations, like forward declaration, which has perceived downsides, or the pimpl idiom, which is a nonzero cost abstraction. Even though C++ is limitless in what you can do, your peers will be wondering what you have been smoking if you go too far from how it's meant to be.

The worst part: If you think about it, the need to declare private functions in their public header is not even necessary: The moral equivalent of member functions can be, and commonly is, mimicked in C, which does not recreate this problem.

Related