使用预编译头提高编译速度

什么是预编译头

在介绍预编译头之前,有必要了解一下C/C++的编译方式。C/C++的编译单元是源文件(带有.c、.cc、.cpp等扩展名的文件),在编译一个源文件之前,预处理器会把这个源文件中所有通过#include指令包含进来的头文件递归地展开,也就是把所有直接或间接包含的头文件原封不动地插入进来。当这个过程结束之后,才开始编译。

这种编译方式的缺点是会使头文件被重复编译。假如有一百个源文件都包含了Windows.h,那么这个头文件会在一百个源文件中展开,它里面的代码会被重复编译了一百次,尽管每次编译的结果都相同。对于具有成千上万个源文件的大型项目来说,重复编译是难以接受的,会浪费大量的编译时间。

为了解决这个问题,预编译头应运而生。顾名思义,预编译头就是预先把头文件编译好,在编译源文件的时候直接取用这些编译结果,避免对头文件重复编译。这项技术能大幅提高C++的编译速度。

Visual C++生成的扩展名为.pch的文件即是预编译头生成的结果。

如何使用预编译头

Visual C++对预编译头的设置并不直观,容易造成误解。在这里详细介绍一下如何启用预编译头。以下操作是在Visual Studio 2013上进行的,不过Visual Studio各版本之间的差异不大,更早或更新的版本也适用。

首先要做的,是在项目中添加一个头文件以及源文件,这两个文件是给预编译头这个机制使用的。它们的名称并没有限制,这里把它们命名为precompiled.h和precompiled.cpp(Visual C++使用的默认名字是StdAfx.h和StdAfx.cpp)。

precompiled.h将被指定成预编译头文件,所有在这个头文件中的代码都会被预编译。可以把任意代码添加到这个文件,一般的做法是把常用的头文件包含进来,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once

#include <Windows.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <list>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>
#include <boost/format.hpp>

precompiled.cpp则为precompiled.h提供了编译的载体,因为C/C++只能对源文件编译,而不能对头文件编译。precompiled.cpp只需要包含precompiled.h即可(由于示例项目的需要,包含语句中包含了相对路径):

1
#include "win/precompiled.h"

接下来,指定通过precompiled.cpp来生成预编译结果。在“解决方案资源管理器中”,右击precompiled.cpp文件,在弹出的菜单中点击“属性”,打开该文件的属性页窗口。

在属性页窗口中,打开“预编译头”配置页,设置“预编译头”选项的值为“创建(/Yc)”,设置“预编译头文件”选项的值为“win/precompiled.h”。如下图所示:

点击确定完成设置。如此一来,在编译precompiled.cpp的时候就会生成预编译结果,也就是.pch文件。

设置了生成预编译结果之后,还需要设置使用预编译结果。在“解决方案资源管理器中”,右击项目节点,在弹出的菜单中点击“属性”,打开项目的属性页窗口。

同样地,在属性页窗口中打开“预编译头”配置页,设置“预编译头”选项的值为“使用(/Yu)”,设置“预编译头文件”选项的值为“win/precompiled.h”。要注意“预编译头”选项的值跟之前的不同。如下图所示:

最后,需要在所有的源文件中包含预编译头文件,并且该文件必须是第一个包含的。这是使用预编译头的硬性规定,假如不遵守这个规定,编译会失败。重复地在所有源文件中添加预编译头文件很繁琐,所幸的是Visual C++提供了强制在所有源文件中包含指定头文件的选项。同样在项目的属性页窗口中,打开“C/C++”分类下的“高级”配置页,在“强制包含文件”的选项中,添加“win/precompiled.h”即可,如下图所示:

至此,预编译头的设置就完成了。注意,在预编译头文件之后再重复包含该文件内已包含的头文件并不会有问题,所以不必特意去掉那些重复的包含语句。

预编译头性能实测

预编译头对编译性能有多大的提升呢?这里用一个实际的项目进行测试。该项目共有1006个头文件和源文件,分别在关闭和打开预编译头的情况下重新生成整个项目两次,记录下生成的开始时间和结束时间。最终得到的数据如下:

可以看到,使用了预编译头之后,编译性能有了30%以上的提升,节省了不少时间。当然,这里的数据只是一个参考值,实际的提升程度得视项目的具体情况而定。一般来说,预编译头文件中包含的头文件被使用得越多,性能提升越明显。

使用预编译头的注意事项

既然预编译头有这样的好处,那么是不是加入预编译的头文件越多越好呢?答案是否定的。上文已经提到,使用预编译头的时候必须在所有源文件中包含预编译头文件,由此造成的影响是,一旦其中的头文件发生了变化,不论这个变化有多细微,整个项目都要重新编译。把一个会被频繁修改的头文件包含到预编译头文件中是非常不明智的做法,因此,理想的选择是下列几乎不会修改的头文件:

  • 操作系统API头文件,例如Windows.h。
  • C/C++标准库头文件,例如string。
  • 第三方库头文件,例如boost/filesystem.hpp。

另外一个要注意的是,C++的预编译头是不能用在C上的,反之亦然。也就是说,假如预编译头是通过.cpp源文件生成的,那么在.c源文件中使用了这个预编译头就会导致编译出错。有方法可以分别为这两种语言生成和使用不同的预编译头,不过这样做稍显复杂,最简单的做法是把源文件的扩展名改成.cpp,统一使用C++即可,这在大部分情况下都是可行的。