C++Builder is one of the fastest C++ compilers around, and probably the fastest Win32 C++ compiler in terms of compilation speed. Despite the speed advantage that C++Builder holds over other C++ compilers, the C++ language is complex and requires a lot of processing time to compile. This article explains why C++ compilers are inherently slow, and demonstrates techniques to boost compile times in C++Builder. The focus of this article is pre-compiled headers and how to use them properly in your projects.
In C++, you cannot call a function from a source file unless that function has been previously defined or declared. So what does this mean? Consider a simple example where function A() calls function B(). A() cannot call B() unless a prototype for B(), or the function body for B(), resides somewhere above the function body for A(). The code below illustrates this point.:
// declaration or prototype for B
void B();
void A()
{
B();
}
// definition, or function body of B
void B()
{
cout << "hello";
}
The code will not compile without the prototype for B(), unless the function body for B() is moved up above A().
Function prototypes serve a crucial role for the compiler. Every time you execute a routine, the compiler must insert proper code to call the routine. The compiler must know how many parameters to pass to the function, and whether the function expects its parameters on the stack or in registers. In short, the compiler needs to know how to generate the correct code to call the function, and it can only do this if it has seen a previous declaration or definition for the function that is being called.
To simplify the prototyping of functions and classes, C++ supports the #include directive. The #include directive allows a source file to read function prototypes from a header file prior to the location in code where the prototyped functions are called. The #include directive plays an important role in Win32 C++ development. Function prototypes for C runtime library (RTL) functions are provided in a standard set of header files, the Win32 API is prototyped in a set of header files provided by Microsoft, and the VCL classes and functions are prototyped in the VCL headers. You can’t create a useful Windows program without including header files provided by Microsoft and Borland.
Header files help implement C++ type checking in a manner that is easy to manage for the programmer. However, this benefit comes at a cost. When the compiler runs across an #include directive, it literally opens the included file and inserts it into the file it is compiling. The compiler then parses the included file as part of the file currently compiling. So what happens if the first header file includes yet another file? The compiler will suck in that file and start parsing it as well. Imagine what happens when 10, 20, or even 100 files are included? While this number of include files may sound large, it isn’t unrealistic when you start adding up the Windows SDK header files and the VCL header files.
To demonstrate how the compiler branches off and translates included files, I created a simple console mode program. Here is that program:
// include some standard header files
#include <stdio.h>
#include <string.h>
#include <iostream.h>
#include <windows.h>
#pragma hdrstop
#include <condefs.h>
int main()
{
printf("Hello from printf.\n");
cout << "Hello from cout" << endl;
MessageBeep(0);
return 0;
}
To prove a point, I turned off pre-compiled headers in the Project Options dialog. When I built this project, the build progress dialog reported that the project contains 130,000 lines of code. 130 thousand lines! How can that be? The source file contains only four lines of actual code. The 130,000 lines were contained in STDIO.H, STRING.H, IOSTREAM.H, WINDOWS.H, and all of the other header files that are included by these four header files. In this example, the compiler spent the vast majority of its time processing header files.
Next I added a new unit with a single simple function, and added code to the console application that calls this function:
#include <stdio.h>
#include <string.h>
#include <iostream.h>
#include <windows.h>
// prototype for A() is in unit1.h
#include "Unit1.h"
#pragma hdrstop
void A()
{
printf("Hello from function A.\n");
}
The console application now looked like this:
#include <stdio.h>
#include <string.h>
#include <iostream.h>
#include <windows.h>
#include "Unit1.h"
#pragma hdrstop
#include <condefs.h>
USEUNIT("Unit1.cpp");
int main()
{
printf("Hello from printf.\n");
cout << "Hello from cout" << endl;
A();
MessageBeep(0);
return 0;
}
When I built the project again, the compiler progress dialog reported 260,000 lines of code compiled. This is because the compiler had to translate the same set of header files twice, once for each of the CPP files in the project. Imagine how this line count would grow in a large project. The burden of processing the same group of header files over and over greatly increases compile times.
The engineers at Borland realized that they could decrease build times by designing a compiler that did not process the same header files over and over during a build. To achieve this goal, Borland C++ 3 (Borland C++, not C++Builder) introduced the concept of pre-compiled headers.
The idea behind pre-compiled headers is relatively simple. When the compiler processes a set of header files for a particular source file, it saves the compiled image of the header files on the hard drive. When that set of header files is required by another source file, the compiler loads the compiled image instead of compiling the header files a second time.
Next, I turned pre-compiled headers on for the console mode program and entered PCH.CSM for the pre-compiled header filename (in the Project Options dialog). When I built the project again, the compiler processed 130,000 lines of code when it compiled PROJECT1.CPP, but only 20 lines of code when it compiled UNIT1.CPP. The compiler generated a pre-compiled image when it parsed the first source file, and that pre-compiled image was loaded and reused for the second source file. Imagine the performance boost that you would attain if the project contained 50 source files instead of only two.
The use of pre-compiled headers in the previous example reduced the build time of the project by almost 50%. But that was a simple console mode program that didn’t do much. You probably want to know how you can take advantage of pre-compiled headers in a full-blown VCL GUI program. By default, C++Builder automatically turns on pre-compiled headers for you. However, C++Builder does not pre-compile every header file that is used by your program. It only pre-compiles the file VCL.H. Inspect the top of any form’s source file and you will find these lines:
#include <vcl.h>
#pragma hdrstop
The #pragma hdrstop directive tells the compiler to stop generating the pre-compiled image. Any #include statement located before the hdrstop directive will be pre-compiled, while any #include below the directive will not be pre-compiled.
So how many header files are pre-compiled when VCL.H is compiled? If you look at VCL.H, you will see that it includes another file called VCL0.H. If you don’t alter the default settings of C++Builder, VCL0.H will include a small set of VCL header files. For C++Builder 4 and 5, those files are:
#include <system.hpp>
#include <windows.hpp>
#include <messages.hpp>
#include <sysutils.hpp>
#include <classes.hpp>
#include <graphics.hpp>
#include <controls.hpp>
#include <forms.hpp>
#include <dialogs.hpp>
#include <stdctrls.hpp>
#include <extctrls.hpp>
This is a small cross section of header files, and it probably represents only a subset of the header files that are used in a moderate to large sized project. VCL0.H does allow you to pre-compile more header files through the use of conditional defines. For example, you can define a variable called INC_VCLDB_HEADERS to pre-compile the VCL database header files, or INC_VCLEXT_HEADERS to pre-compile header files for the extra controls that come with C++Builder. If you define a variable called INC_OLE_HEADERS, C++Builder will pre-compile some of the Win32 COM header files. If you use these defines, place them before the #include statement for VCL.H:
#define INC_VCLDB_HEADERS
#define INC_VCLEXT_HEADERS
#include <VCL.H>
#pragma hdrstop
If you decide to try this technique, make sure you add to the two defines to every CPP file in your project, even if they don’t use database classes or extra controls. The reasoning for this will be explained shortly.
The default pre-compiled header settings do reduce the time it takes to build a project. You can prove this fact by timing a full build of a large project when use of pre-compiled headers are enabled, and by timing the build when pre-compiled headers are disabled. The goal of this article is to tweak the way C++Builder pre-compiles files in order to reduce build times even more. In the next section I will outline two techniques for improving build times.
Before we discuss those techniques, it is important to realize how C++Builder determines whether it can use an existing pre-compiled image when compiling a source file. C++Builder generates a unique pre-compiled image for every source file in your project. These pre-compiled images are saved in a file on your hard drive. The compiler will reuse an existing pre-compiled image when two source files require the same pre-compiled image. This is an important distinction. Two source files will require the same pre-compiled image if they include exactly the same files. Furthermore, they must include the files in the same order. Simply put, the source files must be identical up to the #pragma hdrstop directive. Table A shows a few examples of pre-compiled images that don’t match, and Table B shows examples of pre-compiled images that do match.
Table A: Mismatched header includes.
|
Example |
UNIT1.CPP |
UNIT2.CPP |
|
1 |
#include
<stdio.h> |
#include
<iostream.h> |
|
|
#pragma
hdrstop |
#pragma
hdrstop |
|
|
|
|
|
2 |
#include <stdio.h> |
#include <stdio.h> |
|
|
#include
<iostream.h> |
#pragma
hdrstop |
|
|
#pragma
hdrstop |
|
|
|
|
|
|
3 |
#include <stdio.h> |
#pragma hdrstop |
|
|
#pragma hdrstop |
#include <stdio.h> |
Table B: Properly matched header includes.
|
Example |
UNIT1.CPP |
UNIT2.CPP |
|
1 |
#include
<stdio.h> |
#include
<stdio.h> |
|
|
#include
<string.h> |
#include
<string.h> |
|
|
#include
<iostream.h> |
#include
<iostream.h> |
|
|
#include
<windows.h> |
#include
<windows.h> |
|
|
#include
“unit1.h” |
#include
“unit1.h” |
|
|
#pragma
hdrstop |
#pragma
hdrstop |
|
|
|
|
|
2 |
#define INC_VCLDB_HEADERS |
#define INC_VCLDB_HEADERS |
|
|
#define INC_VCLEXT_HEADERS |
#define INC_VCLEXT_HEADERS |
|
|
#include
<vcl.h> |
#include
<vcl.h> |
|
|
#pragma
hdrstop |
#pragma
hdrstop |
|
|
#include
“unit1.h” |
#include
“unit2.h” |
When the compiler processes a source file with a pre-compiled image that does not match an existing image, it will produce a completely new image from scratch. Look at Example 2 in Table A. Even though STDIO.H is compiled along with UNIT1.CPP, the compiler will translate STDIO.H again when it compiles UNIT2.CPP because the second unit’s include list does not match that of the first unit. Pre-compiled headers reduce compile times only when the compiler can reuse an existing pre-compiled image across multiple source files.
This is the foundation for the techniques that I explain in the next section. Pre-compile as many header files as you can, and make sure that you use the same pre-compiled image in every source file.
I have discovered two different techniques for optimizing pre-compiled headers. I will explain those techniques in the following sections.
The first technique involves boosting the number of files that VCL.H includes. This is accomplished by adding two conditional defines to every source file. To implement this technique, open every CPP file in your project (including the project source file) and modify the first two lines so they look like this:
#define INC_VCLDB_HEADERS
#define INC_VCLEXT_HEADERS
#include <VCL.H>
#pragma hdrstop
If you don’t like the idea of adding these defines to every source file, you can accomplish the same thing by adding INC_VCLDB_HEADERS and INC_VCLEXT_HEADERS to the Conditional defines field of the Project Options dialog (found on the Directories/Conditional page).
You might want to throw in some of the RTL header files that you commonly use, along with WINDOWS.H. Make sure you add the lines before the #pragma hdrstop line, and make sure that you list them in the same order in every C++ source file:
#define INC_VCLDB_HEADERS
#define INC_VCLEXT_HEADERS
#include <VCL.H>
#include <windows.h>
#include <stdio.h>
#pragma hdrstop
This technique works fairly well, but it isn’t very flexible. If you decide to add a new header file to the list of files that get pre-compiled, you need to modify every C++ source file in your project. Furthermore, this technique is prone to error. If you mess up the order of your includes, you can actually make your compile times worse, not better.
The second technique addresses some of the downfalls described for the first technique. The strategy for this technique is to create one huge header file that includes every header file that is required by your project. This single file will include the VCL headers, the Windows SDK headers, and the RTL headers. It can also include all of the header files for forms and units that you have created, but you don’t want to pre-compile header files that are likely to change (see the companion article “Pre-compiled header tips” for further explanation).
Here is an example of how the common header file (which I have named PCH.H) might look:
// PCH.H: Common header file
#ifndef PCH_H
#define PCH_H
// include every VCL header that we use
// could include VCL.H instead
#include <Buttons.hpp>
#include <Classes.hpp>
#include <ComCtrls.hpp>
#include <Controls.hpp>
#include <ExtCtrls.hpp>
#include <Forms.hpp>
#include <Graphics.hpp>
#include <ToolWin.hpp>
// include the C RTL headers that we use
#include <string.h>
#include <iostream.h>
#include <stdio.h>
// headers for the 3rd party controls
// TurboPower System
#include "StBase.hpp"
#include "StVInfo.hpp"
// Our custom controls
#include "DBDatePicker.h"
#include "DBRuleCombo.h"
#include "DBPhonePanel.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
Note the section that checks to see if PRECOMPILE_ALL has been defined. This is a symbol that I created to indicate whether the individual headers for my project’s units should be pre-compiled. See the following article for more information regarding the effects of pre-compiling your own headers.
Once you have created the gigantic common header file, you must modify every source file so it includes only this file. For example:
#include <VCL.H>
#include "pch.h"
#pragma hdrstop
I have chosen to leave the original include statement for VCL.H intact, but you might want to move VCL.H to the common header file.
After you have added the include for PCH.H to every C++ source file, don’t insert any more include files prior to the #pragma hdrstop. Doing so will cause those C++ files to require a pre-compiled image that does not match the pre-compiled image from other files.
I am currently employing Technique 2 without defining PRECOMPILE_ALL on my current project. The project is a medium sized client/server database program that consists of 113 C++ source files, most of which are forms or data modules. Using this technique, a full build of the project takes only 195 seconds. Of those 195 seconds, 40 seconds are spent generating the pre-compiled header, and about 40 seconds are spent linking. In the remaining time, the compiler translates 113 C++ source files. That’s an average of one file per second. By way of comparison, the project takes more than 30 minutes to build when no pre-compiled headers are used, and the project takes 18 minutes to build when pre-compiled headers are used but this technique is not utilized.
Incremental makes with Technique 2 are lightning fast when no header files have changed. The compiler does not bother to regenerate the pre-compiled image on disk if no header files have changed. When this condition is met, an incremental make takes only one or two seconds, because only those C++ source files that have changed need to be compiled. The compiler spends all of its time compiling those files, instead of compiling system header files. When a header file does change, the speed of an incremental make depends on whether or not the PRECOMPILE_ALL flag was defined.
Proper use of pre-compiled headers can make a huge difference in the amount of time required to compile a project. The first step to properly using pre-compiled headers is in understanding how they work. The second step is putting that knowledge to work. By implementing the techniques explained in this article you can drastically reduce your project build times.