Home Articles Books Controls FAQs Tips

Improving C++Builder Build Times With Pre-Compiled Headers.

C++Builder is one of the fastest C++ compilers around, and probably the fastest Win32 C++ compiler. Despite the speed advantage that C++Builder holds over other C++ compilers, many Delphi programmers contort in agony while waiting for a C++Builder project to compile. Anyone that has seen Delphi knows that it is blazing fast in comparison to any C++ compiler. Delphi can compile small example projects in less than a second, and large projects can be built in less than five.

So why does Delphi hold such a speed advantage over C++Builder? Furthermore, is there anything that can be done to improve the compilation speed of C++Builder? This article explains why C++ compilers are inherently slow, and demonstrates a simple tactic to boost compile times in C++Builder.


Why C++ Compilers are Slow

In C++, you cannot call 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 to 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 the function. The compiler must know if 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 a #include statement. 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 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 classes and functions of the VCL are listed in header files that come with C++Builder. You can't create a very useful Windows program without including header files provided by Microsoft or Inprise.

Header files help implement C++ type checking in a manner that is easy to manage for the programmer. However, this benefit comes at a huge cost. When the compiler runs across a #include directive, it literally opens the included file and inserts it into the current file. The compiler then parses the included file as if it was part of the file that it was already compiling. So what happens if the first header file includes yet another file? The compiler will suck in that file and start parsing it. 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 all of the VCL header files.

To demonstrate how the compiler branches off and translates included files, consider the following code example. This is a simple console mode program that I built using the Console Wizard from the Object Repository. In order to test this code, select Options-Project-Compiler and turn off pre-compiled headers.


    // include some standard header files

    #include <stdio.h>

    #include <string.h>

    #include <iostream.h>

    #include <windows.h>



    #pragma hdrstop

    #include <condefs.h>



    //-----------------------------------------------

    #pragma argsused

    int main(int argc, char **argv)

    {

        printf("Hello from printf.\n");

        cout << "Hello from cout" << endl;

        MessageBeep(0);

        return 0;

    }

When I build this project with C++Builder, the build progress dialog reports that the project contains 130,000 lines of code. 130 thousand lines! How can that be? The source file only contains about four lines of 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.

Now let's investigate what happens when you have multiple CPP files in a project. Building off of the existing project, let's add a unit to the console program that we already have. Add a simple function to this second unit. Then alter the first CPP file so it will call the new function.


    //-------------------------------------------------------

    // UNIT1.CPP

    #include <stdio.h>

    #include <string.h>

    #include <iostream.h>

    #include <windows.h>

    #include "Unit1.h"         // prototype A() in unit1.h



    #pragma hdrstop



    void A()

    {

        printf("Hello from function A.\n");

    }

    //-------------------------------------------------------



    //-------------------------------------------------------

    // PROJECT1.cpp

    #include <stdio.h>

    #include <string.h>

    #include <iostream.h>

    #include <windows.h>

    #include "Unit1.h"



    #pragma hdrstop

    #include <condefs.h>



    //-------------------------------------------------------

    USEUNIT("Unit1.cpp");

    //-------------------------------------------------------

    #pragma argsused

    int main(int argc, char **argv)

    {

        printf("Hello from printf.\n");

        cout << "Hello from cout" << endl;

        A();

        MessageBeep(0);

        return 0;

    }

    //-------------------------------------------------------



Now build the project. If you turned off pre-compiled headers before building the project, you will see that the compiler progress dialog reports 260,000 lines of code when the build finishes. Notice that the compiler had to translate the same set of header files in two different CPP files. We know from the previous example that these header files place a burden of 130,000 lines on the compiler. The second CPP file places the same 130,000 line burden on the compiler, for a grand total of 260,000 lines of header files. Imagine how this line count would rapidly grow in a large project. The burden of processing the same group of header files over and over can greatly increase compile times.


How C++Builder Uses Pre-Compiled Headers To Reduce 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 the build. To achieve this goal, Borland C++ 3.0 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 one 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 processing the header files a second time.

Let's modify our console mode program to see how pre-compiled headers can impact build times. The code itself should already be fine. We just need to turn the project's pre-compiled headers option back on. Select Options-Project-Compiler and check the Use pre-compiled headers option or the Cache pre-compiled headers option. Enter PCH.CSM in the pre-compiled header filename box. Do a full rebuild of the project once you change the settings.

During the build, pay special attention to the compiler progress dialog. You should see that the compiler processes 130,000 lines of code when it compiles PROJECT1.CPP, but when it compiles UNIT1.CPP, it only process 20 lines of code. The compiler generates a pre-compiled image when it parses the first source file, and that pre-compiled image is used to speed up compilation of the second source file. Imagine the performance boost that you would attain if the project contain 50 source files instead of 2.


Explanation of Pre-Compiled Headers in a VCL GUI Project

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, which you can see by inspecting the top of any form's source file:


    #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 get pre-compiled when VCL.H is pre-compiled? If you look at VCL.H, you will see that it includes another file called VCL0.H (assuming you have BCB3). If you don't alter the default settings of C++Builder, VCL0.H will include a small set of VCL header files. They are:


    // Core (minimal) VCL headers

    //

    #include <sysdefs.h>

    #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. You can #define a variable called INC_VCLDB_HEADERS to pre-compile the VCL database header files. Likewise, you can define 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 SDK COM header files. These defines should be placed before the #include statement for VCL.H.


    #define  INC_VCLDB_HEADERS

    #define  INC_VCLEXT_HEADERS

    #include <vcl.h>

    #pragma hdrstop

Note: If you decide to try this technique, make sure you add to the two defines to every CPP file, even if they don't use DB classes or extra controls. The reasoning for this will be explained shortly.


Optimizing C++Builder's Use of Pre-Compiled Headers

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 pre-compiled headers are on and by timing the build when pre-compiled headers are off. The goal of this article is to tweak the way C++Builder pre-compiles files to reduce build times even more. In this section, I have outlined two techniques for improving build times.

Before we discuss the techniques, it important to realize how C++Builder decides that 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 re-use 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 until the #pragma hdrstop directive. Here are some examples:


    Example 1: Pre-compiled images don't match

    //--------------------                  //--------------------

    // UNIT1.CPP                            // UNIT2.CPP

    #include <stdio.h>                      #include <iostream.h>

    #pragma hdrstop                         #pragma hdrstop





    Example 2: Pre-compiled images don't match

    //--------------------                  //--------------------

    // UNIT1.CPP                            // UNIT2.CPP

    #include <stdio.h>                      #include <stdio.h>

    #include <iostream.h>                   #pragma hdrstop

    #pragma hdrstop





    Example 3: Pre-compiled images don't match

    //--------------------                  //--------------------

    // UNIT1.CPP                            // UNIT2.CPP

    #include <stdio.h>                      #pragma hdrstop

    #pragma hdrstop                         #include <stdio.h>





    Example 4: Pre-compiled images match

    //--------------------                  //--------------------

    // UNIT1.CPP                            // UNIT2.CPP

    #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



    Example 5: Pre-compiled images match

    //--------------------                  //--------------------

    // UNIT1.CPP                            // UNIT2.CPP

    #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"





    Example 6: Pre-compiled images don't match

    //--------------------                  //--------------------

    // UNIT1.CPP                            // UNIT2.CPP

    #define  INC_VCLDB_HEADERS              #include <vcl.h>

    #define  INC_VCLEXT_HEADERS             #pragma hdrstop

    #include <vcl.h>

    #pragma hdrstop

When the compiler processes a source file with a pre-compiled image that does not match an existing image, the compiler will produce a completely new image from scratch. Look at Example 2 above. Even though STDIO.H is compiled along with UNIT1.CPP, the compiler will translate STDIO.H again when it compiles UNIT2.CPP. Pre-compiled headers reduce compile times only when the compiler can re-use an existing pre-compiled image across multiple source files.

This is the foundation for both of the techniques that I list here. Pre-compile as many header files as you can, and make sure that you use the same pre-compiled image in every source file.

Technique 1:

The first technique is to simply boost the number of files that VCL.H includes by adding two conditional defines to every source file. Open every CPP file in the project, including the project source file, and change the first two lines of the file so they look like:


    #define  INC_VCLDB_HEADERS

    #define  INC_VCLEXT_HEADERS

    #include <vcl.h>

    #pragma hdrstop

You might want to throw in some of the C RTL header files that you commonly use, along with WINDOWS.H. Make sure that you add the lines before the hdrstop pragma, 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

Technique 2:

Technique 1 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, Technique 1 is prone to error. Lastly, Technique 1 does not pre-compile any of the header files that you create.

Technique 2 addresses some of the downfalls of Technique 1. The strategy here is to create one huge header file that includes every header file that is used in your project. This single file will include the VCL files, windows SDK header files, and RTL header files. It will also include all of the header files for forms and units that you have created. Here is an example of what this file will look like:


    //---------------------------------------------------------

    // 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>



    // include 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"



    // Object Repository header files

    #include "BaseData.h"

    #include "BASEDLG.h"



    // project include files

    #include "About.h"

    #include "mainform.h"

    ...

    ... // about 60 more files

    ...

    #include "validate.h"



    #endif

Once you have the gigantic common header file ready, change every source file so it includes this file. I have chosen to leave the original include statement for VCL.H intact. You might want to move VCL.H to the common header file.


    //-----------------------------------------------

    #include <vcl.h>

    #include "pch.h"

    #pragma hdrstop

Note: After you add 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.

Results:

I am currently employing Technique 2 on my current project. The project is a medium sized client/server database program that consists of 75 C++ source files, most of which are forms or datamodules. Using Technique 2, a full build of the project takes only 68 seconds. Of that 68 seconds, 32 seconds were spent generating the pre-compiled header. In the remaining 36 seconds, the compiler translated 74 C++ source files. That's an average of two files 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 12 minutes to build when pre-compiled headers are used but Technique 2 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 1 or 2 seconds.

Notes:

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 = "An Error Occurred!!!!!";

If you want to place const variables in a header file, create a separate header file to contain the constant. Try to reduce the burden on the compiler by not allowing this header file to include other files.

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. I think this has to do with the way templates are handled by the compiler.

Keep an eye on the compiler progress dialog: The compiler progress dialog tells you how well you're pre-compiled headers are working. When you employ Technique 2, you should see that the compiler takes a long time to compile the first C++ source file in your project. The compiler generates the pre-compiled header image during compilation of the first file in the project. During this time, you should see the line count on the compiler progress dialog reach a huge number (100,000-500,000). Once the compiler moves on to other C++ files, the line count should probably be between 20 and 1000 lines for each source file. 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 4 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 one source file, it's a good indication that the compiler was unable to apply the existing pre-compiled image to that source file.

Avoid changes to header files: When using Technique 2, realize that any small change to a header file will force the compiler to regenerate the pre-compiled image. Based on the test results, this could take from 20 seconds to a minute.



Copyright © 1997-1998 by Harold Howe.
All rights reserved.