Pre-compiled header tips

by Harold Howe

There are several things to keep in mind when using pre-compiled headers. This article will explain what to do—and what not to do—when using pre-compiled headers. The tips in this article apply primarily to the second technique described in the article, “Improving build times with pre-compiled headers.”

Don’t pre-compile constant variables

The compiler cannot pre-compile a header file if it contains a constant variable that is assigned a value. For example, placing the following line in a header file can interfere with the creation of a pre-compiled header image:

const AnsiString strError = "Error!!";

If you want to place const variables in a header file, create a separate header that contains the constants, and don’t pre-compile that file. Try to reduce the burden on the compiler by not allowing this header file to include other files. Similarly, don’t include this file from other header files if you can help it. If this seems like a difficult task, you can use the extern keyword. Create a header file that contains extern prototypes for your constants. Then create a CPP file that defines the constants (i.e. gives them a value).

Note that the problem of const variables only occurs when you pre-compile your own source file headers. If you don’t pre-compile a header file, then you can add constants to it without any problems. Also, #defines do not present a problem, only const variables (although I don’t recommend that you use #defines simply to eliminate this problem).

Don’t pre-compile template headers

This suggestion is based on empirical evidence. I have a template class with several inline functions. The entire template class resides in a header file. The compiler was able to pre-compile this header file, but I noticed that the pre-compiled image was always re-generated during an incremental make. This apparently has to do with the way some templates are handled by the compiler. Some template headers pre-compile just fine, while others do not. I suggest that you go ahead and try to compile template headers, but pay close attention to the compiler progress dialog. You may need to stop pre-compiling certain template headers if they cause problems.

Keep an eye on the compiler progress dialog

The compiler progress dialog tells you how well your pre-compiled headers are working. After implementing pre-compiled headers, you should see that the compiler takes a long time to compile the first C++ source file in your project, and that subsequent files compile much faster. The compiler generates the pre-compiled header image during compilation of the first file in the project. When the first file is compiled, you will see the line count on the compiler progress dialog reach a huge number (100,000-500,000 lines). The line count for other files in the project should only be between 20 and 1,000 lines if you have defined the PRECOMPILE_ALL symbol. If you don’t define this symbol, though, the line count should still stay under 15,000 lines or so. Once the compiler finishes translating the first file in the project, subsequent files should only take a second or two to compile.

If the compiler gets bogged down on one C++ file for more than a few seconds, you probably have a source file whose pre-compiled image doesn’t match the image created by the common header file. The line count is another indicator. If you see the line count sail up above 50,000 lines for a particular source file, it’s a good indication that the compiler was unable to apply the existing pre-compiled image to that source file.

Don’t pre-compile header files that change frequently

When using pre-compiled headers, realize that any small change to a header file will force the compiler to regenerate the pre-compiled image. Based on my tests, this could take from 20 seconds to a minute. During the early stages of development, your project header files may change frequently. In that case you may want to pre-compile only system and VCL header files. This is the purpose of the PRECOMPILE_ALL symbol. It allows you to easily include or remove your header files from the pre-compiled image. For example:

// PCH.H: Common header file
#ifndef PCH_H
#define PCH_H

// include every VCL header that we use
#include <Buttons.hpp>
#include <Classes.hpp>

// include the C RTL headers that we use
#include <string.h>
#include <iostream.h>
#include <stdio.h>

// project headers are pre-compiled
// only if PRECOMPILE_ALL is defined
#ifdef PRECOMPILE_ALL
#include "About.h"
#include "mainform.h"
// remainder of user-created header files
#endif

#endif

To add your own header files to the pre-compiled header, add PRECOMPILE_ALL to the Conditional defines field in the Project Options dialog (Directories/Conditional page). If your header files change frequently, then don’t add this conditional define to the project. When you don’t pre-compile your own header files, a full build of your project will take a little longer. However, when you make a change to one of your header files, an incremental make will be faster because the compiler won’t waste 20-60 seconds rebuilding the pre-compiled image.

I do not define PRECOMPILE_ALL for a project during the early stages of development because I am changing my header files frequently. I found that incremental makes of my project were taking more than 2 minutes. I the amount of time a full build took when the PRECOMPILE_ALL symbol was defined. Then I made a small change to the header file for my main form and performed an incremental make. Next, I repeated this process with the PRECOMPILE_ALL value not defined. Table A shows are the results.

Table A: Effects of defining PRECOMPILE_ALL

Setting                  Full Build                                              Incremental Make

Not defined     195 seconds, 408,887 lines       28 seconds, 27,059 lines

 

Defined            179 seconds,  255,689 lines     179 seconds,  255,689 lines

 Notice that a full build is 16 seconds faster when I pre-compile my own header files, but look what happens when I do an incremental make after changing a header file. The incremental make takes just as long as a full build. When you pre-compile your own header files, the compiler rebuilds the pre-compiled image every time you change a header file. Additionally, when the pre-compiled image changes, every file that depends on that image will be re-compiled as well. When you alter a header file, the entire project essentially gets rebuilt. When I do not pre-compile my own header files, the pre-compiled image never gets rebuilt. This keeps the incremental make time down.

Since you probably perform an incremental make much more often than you do a full build, it seems wise to keep the incremental make time down, even if it means that a full build will be 10% slower.

Don’t remove existing include statements

Creating a common header file does not mean that you should remove from your header files the includes that the IDE automatically adds. Leave those include statements where they are. There are several reasons why you should leave existing include statements as they are. First, if you remove include statements from your header files, C++Builder will simply add them back again. Second, you may want to stop pre-compiling certain files, which would force to you add the include statements back into your source files. Lastly, by leaving include statements intact, you preserve the necessary inclusion order between header files. If you remove include statements, you will need to worry about the order of the include statements in your common header.

The include files have guards to prevent multiple inclusion, so there’s no need to worry about including the same file twice. The point is that even though you may use a common include file, you don’t need to worry about removing the include statements that C++Builder generates. When I create a new source file, I add include statements so that file compiles without relying on the common header. Once the new source file compiles, I insert an #include statement for the common header to keep compile times down.

Observe case sensitivity for include statements

It is important to understand that the compiler observes case sensitivity when matching pre-compiled images. If you include the common header with varying case in different units, the compiler will regenerate the pre-compiled image for each one. Take this code, for example.

// MAINFORM.CPP
#include <VCL.H>
#include "pch.h"
#pragma hdrstop 

// SPLASH.CPP
#include <VCL.H>
#include "PCH.H" // mismatched case
#pragma hdrstop 

In this case the compiler will generate and use a pre-compiled image for MAINFORM.CPP, but SPLASH.CPP will generate its own pre-compiled image. This will, obviously, slow down the compile time. The rule of thumb is this: every include file listed above the #pragma hdrstop directive should use the same case that other files use. Include statements below the #pragma hdrstop directive don’t have to match case, because they are not pre-compiled.

Consider adding VCL.H to the common header

The common header shown in the previous article does not include the file VCL.H. Each CPP source file includes both VCL.H and PCH.H, like this:

#include <VCL.H> 
#include "pch.h"   
#pragma hdrstop

You may prefer to include VCL.H in the common header. If you do, then each CPP file can simply include the common header.

#include "pch.h"   
#pragma hdrstop

This is cleaner, and less prone to error because you don’t have to worry about which file should be listed first. However, it violates the previous tip that suggested that you don’t remove existing #include statements. If you ever need to yank out the common header file, you will need to add VCL.H back into every CPP file in your project.