I recently found myself wondering whether it is faster to compile C than C++. On some level, the answer to this is an obvious “yes”: You can’t accidentally summon cthulhu SFINAE-thulhu in your C code, for example.
But my question is a little bit more naive. What if I take code that is valid in both C and C++ and compile it as C once and then once as C++? Clearly, this isn’t possible in general, but we can at least come up with some toy scenarios where it is possible. What difference is that going to make? C is still a “simpler” language, does that buy us anything? Or asked differently: If I already avoid most of C++, but still rely on some C++ features, what kind of tax am I paying for that? – I should also point out that there are plenty of people spending a lot of time working on languages and toolchains that are much faster than anything I am measuring here. I am not interested in establishing some benchmarks for the maximum possible speed you can compile C as; I am merely curious and like to poke around.
To state the obvious: this post is not about providing irrefutable evidence about anything. It’s an exploration, and far from a conclusive argument for or against anything.
For this purpose, I have written a small C# script that supports a handful of code generation scenarios and then compiles them with both MSVC (from VS17.14.7) and Clang 19. The generated code is not representative of “real” code, so take the results with a good grain of salt. There is usually a parameter N
that controls the size of the generated code. For each scenario, I have compiled the code 30 times as C and C++, with /Od
and /O2
. The idea of comparing two different optimization levels is that as a compiler noob, I would expect the difference between C and C++ to mostly manifest in the frontend, and higher optimization levels should lead to more time spent in the backend… so maybe differences would be smaller. Or maybe not! I’m not an expert on this.
You can find the script here. Note that a part of it is Windows specific: we have to setup a compilation environment to invoke the compiler, and doing this for every run is expensive. So I ended up taking the measurements inside of a bash script.
The different scenarios:
Empty
: We compile an empty file.Funcs
: Generating lots of functions that take an integer and just return it immediately, then call them all.FreeFunc
: We generate lots of calls to a function that takes a struct by pointer.CppMember
: Like the previous, but that free function is now a member function. This obviously doesn’t work in C, but I wanted to see how it fares against a free standing function.NoOverload
: DeclareN
types and a corresponding free function that takes the type by pointer.CppOverload
: Like the previous, except that all of the functions now have the same name. This produces unrealistically large overload sets and I mostly just wanted to see how bad this is.ReturnByPointer
: Returns a trivial struct (containing just anint
) by pointer.ReturnByValue
: Returns a trivial struct by value.
The details of the code for the different scenarios are here.
Findings
All times are given in seconds. I don’t aim to perform any sort of deep statistics besides applying an interocular trauma test.
You can find the summary data here in CSV format, if you want to play with it. I have found it helpful to look at the data in a pivot table.
Some general findings: First, when compiling with /Od
, Clang is marginally faster than MSVC (23.26s vs. 24.54s summed total across all medians). With /O2
on the other hand, Clang is much slower than MSVC (51.20s vs. 35.59s total). They also produce different code, of course.
Od | O2 | |
---|---|---|
Clang | 23.26s | 51.20s |
MSVC | 24.54s | 35.39s |
Second, adding a few thousand functions to the same overload set in C++ is a bad idea, regardless of compiler. Who could have guessed! I’ve stopped beyond N=4000
. With 4000 overloads, Clang-O2 already takes 6.3s to compile this. Interestingly, MSVC fares slightly better when handling unrealistically large overload sets. I doubt this has any effect in reality, to be honest. But it’s still interesting to see the non-linear growth (table shows median of 30 runs):
N=1000 | N=2000 | N=4000 | |
---|---|---|---|
Clang Od | 0.38s | 1.52s | 5.93s |
Clang O2 | 0.45s | 1.67s | 6.33s |
MSVC Od | 0.24s | 0.87s | 5.26s |
MSVC O2 | 0.26s | 0.91s | 5.35s |
Third, there is no big difference between CppMember
and FreeFunc
, on either compiler. If anything, CppMember
seems to be ever so slightly faster than FreeFunc
, but not by much. I thought this was an interesting case to include because in C the name of the function is already sufficient to figure out what to call, whereas in C++ you have to know what type you are invoking it on.
C vs C++
Compiling the same code as C is almost always faster than compiling it as C++, and the few cases where it is slower the slowdown is indistinguishable from noise. The difference between C and C++ is much smaller on Clang than on MSVC. The times in the table below are the summed medians across all scenarios (excluding those that only work with C++, of course).
C, Od | C++, Od | C, O2 | C++, O2 | |
---|---|---|---|---|
Clang | 6.54s | 7.24s | 19.15s | 19.80s |
MSVC | 6.98s | 9.62s | 12.11s | 15.40s |
For MSVC, a large driver of the difference between C and C++ compile times is the ReturnByValue
scenario, which is compiled ca. two times faster as C than as C++ for some values of N
. This is not entirely surprising, because value copies are just much simpler in C. For Clang, there are small differences everywhere. I do not think that they are just noise, because they almost always skew towards C. But it’s not exactly clear cut.
I would have loved to end this exploration by looking at a more “real world” example, but as you can imagine it is not exactly simple to find a C project that just happens to also compile as C++. As it stands, this set of experiments has already sufficiently satisfied my curiosity. If you find something else interesting in the data (or something I’m wrong about!) let me know.
Edit!
Some more data points that came up in discussions about this post:
- raddbg move from C-like-C++ to pure C some time ago. Mārtiņš Možeiko shared the numbers below with me. The C++ build times are based on this commit and the C build times are basd on this commit. Thank you, Mārtiņš!
C, Od | C++, Od | C, O2 | C++, O2 | |
---|---|---|---|---|
Clang | 2.60s | 3.29s | 20.09s | 26.13s |
MSVC | 1.28s | 2.11s | 6.83s | 7.85s |
- Arseny Kapoulkine experimented with moving meshoptimizer from C++ to C a few years ago and wrote about the experience on his blog. The post has lots of timings and insight. It’s worth reading outside of interest in compile times, as Arseny also shares a neat trick for “almost” sorting floats that is actually quite a bit cheaper than doing the full work.
Arseny’s post also calls out that including the same header (say math.h
) actually includes different code when compiled as C++ and as C, and may hence pull in arbitrary C++ standard library headers. The worst example of this in my experience is stdatomic.h
, where C++ defines atomic_int
to actually be the non-copyable (!) std::atomic<int>
(see here).