Skip to content

Using precompiled headers in Visual Studio 6

I only learned how to use precompiled headers recently — even though I remember reading this paper about how to do it back in the late 90’s. It seemed like too much effort, then, and my then-blazing fast Windows NT 4.0 machine didn’t seem to need the help.

Somehow, ten years later with a dual-core CPU that’s ten times as fast, ten times as much RAM, and an effectively infinite amount of disk (1 TB on two disks), it became important enough to do. And my build times went down by a factor of three or more.

Setting up precompiled headers for the first time

  1. Decide whether this project is going to use precompiled headers with C files or C++ files. You can’t do both, so pick the file type that there is more of.
  2. For now let’s assume you are doing precompiled headers for C++ files.
  3. Organize the project’s source files directory into three folders: CPP Files, C Files, and Precompiled Header. You can have subfolders under those if you want, but you must have those three folders at top level, and no C or C++ source files that are not underneath them. All C++ files that will use precompiled headers go into CPP Files. The file stdafx.cpp (the precompiled header file) goes into Precompiled Header. All other source files, typically only .c files, go into the C Files folder.
  4. Select ‘All Configurations’.
  5. Go to project settings, select the entire CPP Files folder, and on the C/C++ tab select “Precompiled Headers” from the Category: dropdown. Choose “Use precompiled header file (.pch)” and fill in “stdafx.h” in the “Through Header:” box.
  6. Select the entire Precompiled Header folder, and without changing tab or category, choose “Create precompiled header file (.pch)” and fill in “stdafx.h” in the “Through Header:” box
  7. Select the entire C Files folder, again without changing the tab or category, choose “Not using precompiled headers”
  8. Close project settings.
  9. Open the file stdafx.cpp and ensure that it only contains one line: #include "stdafx.h"
  10. Open each file in the CPP files directory and ensure that the first non-comment line in that file is #include "stdafx.h". Anything before the inclusion of the precompiled header will be cheerfully ignored by the compiler. I just verified this by including a syntax error at file scope before and after the precompiled header. This failed:
    
    #include "stdafx.h"
    Microsoft is cool
    

    while this succeeded:

    
    M1cr0s0ft suxx
    #include "stdafx.h"
    
  11. Now you can proceed to move headers from the .cpp files into the precompiled header. As a rule of thumb, put all system headers (angle brackets) into the precompiled header file. Put non-system headers into the precompiled header if they are used by more than about 25% of the files in the project. Or you can obsessively time and retime rebuilds until you find the optimal distribution of headers.
  12. Now do Build | Batch Build and choose Rebuild All. The build will appear to stall on the first C++ file, because it is precompiling the headers. Then the build of each successive file will be very quick.

Maintaining precompiled headers

When you add a new C++ file to your project, Visual Studio will helpfully select the other option from the precompiled header area — “Automatic use of precompiled headers”, also known as the evil option. As far as I can tell, this causes Visual Studio to randomly trash the precompiled header file.

This is more than merely irritating; it can introduce a circular dependency that breaks the build and makes it impossible to run your program in the debugger. If file A.cpp depends on the precompiled header in the correct way, but file B.cpp is added and depends on (and rebuilds) the precompiled header in the evil way, Visual Studio will do the following on a Rebuild All:


 compile precompiled header as per A.cpp (correctly)
 compile A.cpp
 compile precompiled header as per B.cpp (evilly and brokenly)
 compile B.cpp
 link

Then if you try to run, it notices that a dependency of A.obj is newer than A.obj, notably the .pch file. If you have incremental linking turned on, you’ll just get the “One or more files are out of date or do not exist.” dialog but the project will run.

If you later modify A.cpp and try to build to run in the debugger, then you get the message

c:\users\client\project-work\file.cpp(1) : fatal error C1852: 'DebugU/project.pch' is not a valid precompiled header file

The only cure for that is to do another Rebuild All.

There is a way to avoid this problem. It is the path of righteousness: when you add a file to a project that uses precompiled headers, you must ensure that it gets the “Use precompiled header file” option.

Deciding what to put in your precompiled header

Using precompiled headers is a sort of a devil’s bargain, really, because in order to get the speedup from using them, you wind up including the header in many more translation units. That’s not actually too painful in terms of speed, and it works great as long as you only include system headers in your precompiled header source file (stdafx.h).

Once you start including project header files, however, a change in that project file will cause the precompiled header to be invalidated, which will cause all the object files that depend on it to be invalidated, which forces you to effectively rebuild your whole project.

That’s fine if half your project depended on that file anyway. But it turns out not to be too painful even when it doesn’t because precompiled headers are such a big win. On one client project (40k loc, C++), a full rebuild with precompiled headers takes 20s, of which building the precompiled header is 4.5s. The full rebuild without precompiled headers takes over four minutes. Here’s a graph:

5 seconds of precompiling is worth 220s of compiling

So even though we spend up to 5 seconds precompiling the header, it pays off dramatically, cutting the overall compile time by a factor of 10.

That’s not the fairest possible comparison. I haven’t spent any time tuning the header includes of the non-pch build, so I’m certainly including some headers unnecessarily into translation units that don’t need any of the things declared in those headers. I could probably spend a good half hour hunting them down, trying to make the build as tight as possible. Or I could turn on precompiled headers, get a nearly free tenfold increase in build times, and spend the half hour hacking.