Compile Times of Single-Translation-Unit-Builds
Making a single-translation-unit-build (STUB) is a method of speeding up compile times in C and C++ programs. This article explains how to implement this method, provides examples of regular build times and STUB times, and analyzes the differences.
In a typical C++ build, a translation unit corresponds to a .cpp source file. It is a unit of code to be compiled that contains implementations for functions. Each translation unit is compiled individually, and they are brought together by the linker into a full program. In the early days of C, program source code could be too big to fit in the computer’s memory all at once. Translation units allow you to split up a program into chunks that can be compiled on their own. Traditional wisdom also says that splitting up a program into translation units makes compiling faster: if you make a change to a .cpp file, you only need to recompile that one file, rather than recompiling the entire program.
However, computers have changed a lot since the introduction of C. Fitting an entire program into memory isn’t much of an issue, and CPUs have become unbelievably faster. The traditional wisdom could use some re-evaluation. While changing a source file can be relatively fast, changing a header file requires recompiling every source file that includes it.
Compiling C++ programs today can be incredibly slow, and it’s not getting any better. Visual Studio gets slower with each release (many developers still prefer the 2008 edition), and the language specification only gets more complex with each new version. Complexity matters for build times: code that is more complex takes longer to parse.
Reducing compile times is important in software engineering: your time is valuable, and time spent waiting for compilation means less time for writing code and testing the code. Programmers have a variety of ways to cut down on their build times:
- Changing header files as little as possible is probably the most common strategy. It already works well with the principle of encapsulation (keep your interfaces consistent), but in practice, improving a class can often require a change to some part of a header. Even just changing a comment will tell build systems to recompile all source files that include the header.
- Relatedly, a surefire way to reduce your build times is to #include fewer headers in your source files. The fewer source files you have that include a header, the fewer that have to be recompiled when that header changes. Also, headers are still code, and still have to be parsed just like source files.
- Templates are a huge source of long build times in C++. Cutting down on your use of templated classes is one way to reduce build times.
- Some programmers jumped ship and went back to C. It’s faster to parse since it’s a simpler language, and using C guarantees that no one will use slow language features like templates.
- Jonathan Blow is creating his own language and compiler that compiles 33,000 lines of code in less than half a second (demo on youtube). Unfortunately, the language is not released at time of writing, and making your own language is probably even less productive than waiting for Visual Studio to compile a project built on Boost.
- A method that’s been gaining popularity lately is “single translation unit builds” (STUB) or “unity builds,” where all .cpp files are compiled to a single translation unit. Each header file only has to be parsed once (include guards will stop duplicates when pre-processing) and the linker has much less work to do. The major downside is that this method requires a full rebuild of your program for any change at all. This is the method we’ll be looking at in detail. Are the time savings of a STUB worth a full rebuild every time?
Measuring Build Times
I use a small program called ctime to measure how long it takes Visual Studio to compile. You can get the single-file source on gist. To build it, open a Visual Studio command line and enter
cl ctime.c /link winmm.lib
My 7-year-old workstation laptop has an Intel Core i5-M560 (2.67 GHz, dual core) and reports 3.86 GB of usable RAM. I’m using 64-bit Windows 7 and Visual Studio 2013. It does have an SSD, but file caching probably makes that irrelevant for the size of programs I tested. When building in release mode, the compiler spends a lot of time on optimizing, so I built the programs in debug mode. I did a full rebuild of each configuration five times to get average times.
Not-Even Hello World
First of all, how long does it take Visual Studio to compile the simplest possible C++ program? I’m not talking about hello world, since that requires including stdio.h. I mean this:
On my machine, it takes Visual Studio on average 0.359 seconds to compile this C++ program. That’s about a third of a second to compile pretty much nothing.
Making a STUB for an Existing Project
An older project of mine, Gamepad Key Controller, is in a typical object-oriented style: each class has its own .h file and a matching .cpp file. The source code is about 1,700 lines in total. A full rebuild means compiling 11 .cpp files into their own translation units, then linking them together.
Normal full build times:
Average: 11.984 seconds
To convert this project to a STUB, I made a new build configuration in Visual Studio so that the normal build wouldn’t be changed, and I added a new file called SingleTranslationUnitBuild.cpp. The STUB file simply includes every other source file:
If you compiled now, every function would have multiple definitions, since there are 2 copies of each .cpp being compiled and linked. To fix this, select every .cpp except the STUB in Visual Studio and go to Project > Properties. In Configuration Properties > General, set “Excluded From Build” to “Yes.” Now only the STUB file will be built. In your normal build, just exclude the STUB file.
Average: 2.957 seconds
This is about 2.6 seconds longer than it takes to compile the simplest C++ program. Considering the power of modern computers, 2.957 seconds seems kind of long for such a small program. Will it scale well to larger projects? For comparison, how much time does it take a normal build to recompile main.cpp (which is 40 lines of code) after changing a comment?
Normal build times when changing a comment in a source file:
Average: 2.246 seconds
This is only about 0.7 seconds faster than compiling all of the code in the program. The thing to keep in mind is that source files include headers, which have to be parsed. Parsing main.cpp means parsing Windows headers, SDL headers, and parts of the C++ standard library. The quantity of this code dwarfs my own code and accounts for a lot of the build time. What’s more, the same header files may have to be re-parsed for every translation unit that includes them. Reducing the amount of headers #included in your .cpp files is probably the best way to speed up your build times if you’re not doing a STUB.
You also don’t need to have only one translation unit. If you’re doing a lot of work to a header, you could combine compilation of only the source files that #include that header. You could also partition major components of a project into their own translation units.
Do Static Functions Affect Build Times?
I’ve been writing my 3D game engine with a STUB since beginning the project. Besides a few libraries like Dear Imgui, my code has only one .cpp file, which contains main; everything else is in header files. There’s no need to separate function declarations and definitions since there’s only one translation unit. All of my files together are about 10,000 lines of code, libraries are roughly 20,000 more, and who knows how many lines of standard library and Windows code are included.
The C++ keyword “static” before a function means each translation unit will get its own copy. Since that makes less work for the linker, does it speed up build times at all? To find out, I prefixed most functions in my code with FUNCTION_DEF, which can be #defined to be “static” or just nothing.
STUB times with non-static functions:
Average: 3.357 seconds
STUB times with static functions:
Average: 3.272 seconds
There’s not much of a difference between the two. Maybe in a larger project a difference would be more visible, but it doesn’t look like it’s worth the time investment of adjusting all of your functions.
Pitfalls of STUBS
A STUB isn’t going to work for every code base. If it takes a long time to compile the code itself (such as being heavily templated), the full rebuild required for every change will grow too long. The code might also be written in a way that isn’t compatible with merging all .cpp files into one translation unit. Programmers on a project may have treated static functions in .cpp files as a method of encapsulation. If two files defined the function
static float square(float x), for example, they will conflict when built as a STUB.
I’m currently a student at Boise State University studying computer science. You can find some of my projects and other articles I’ve written on my website, mysterioussoftware.com.