@lniwn
2017-08-23T21:53:30.000000Z
字数 5778
阅读 5457
C++
翻译
系列文章
原文地址
http://itscompiling.eu/2017/01/12/precompiled-headers-cpp-compilation/
如何使用PCH(precompiled headers)加快C++编译速度
作为C++开发者,意味着陷入这样一个循环:写代码、编译、运行(测试)、结果确认,一个专业的码农应该致力于缩短每一步的时间。虽然设计/编写代码的时间占用了整个工作时间的大多数,但是编译时间很多时候也是一个重要的考量因素。总的说来,你可能会很惊讶,即使修改了少数文件中的一小部分,也会导致大部分代码库的重新编译,这主要是因为C++对头文件的处理方式继承自C,当一个头文件改变时,所有包含它的源文件都要重新编译。
一个C++编译器工作的基本单位叫做translation unit(TU),它是一个术语(来自C++标准),指的是一个C++源文件和它所包含的文件,以及被包含的文件中包含的文件[1]。这意味着一个头文件每次被包含时,编译器会强制在每个TU
中进行预处理和解析,显然,这里有改进的余地。模块化
[2]旨在建立一个更好的模式,用以缓解这种问题。C++委员会极度希望模块化出现在C++17,但是我们知道已经无法实现了。
目前,在没有modules
的情况下,解决这个问题的最常见方法是使用一种称为“预编译头”的技术,现在大多数C++编译器都已经支持了。本文中,我的重点是MSVC, GCC和Clang。
译者注:原作者使用的g++来进行测试,译者在windows平台使用CMake + Visual Studio 2015来进行测试
目录结构如下
|--project
|-- CMakeLists.txt
|-- header.h
|-- stdafx.cpp
|-- main.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.1)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS true) # 使用简版if else
set(CMAKE_CXX_FLAGS_DEBUG "/MDd /Zi /Ob0 /Od /RTC1 /D_DEBUG")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FC") # 输出信息使用全路径
set(CMAKE_CXX_STANDARD 11) # 3.1以上版本支持的属性,使用C++11
add_definitions(-DUNICODE -D_UNICODE)
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} DIR_SRCS)
macro(use_precompiled_header TARGET HEADER_FILE SRC_FILE)
get_filename_component(HEADER ${HEADER_FILE} NAME)
# Use MSVC_IDE to exclude NMake from using PCHs
if (MSVC AND NOT NMAKE AND NOT OGRE_UNITY_BUILD)
set_target_properties(${TARGET} PROPERTIES COMPILE_FLAGS /Yu"${HEADER}")
set_source_files_properties(${SRC_FILE}
PPROPERTIES COMPILE_FLAGS /Yc"${HEADER}"
)
elseif (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_CLANGXX)
endif ()
endmacro(use_precompiled_header)
project(main-pch)
add_executable(${PROJECT_NAME} WIN32 ${DIR_SRCS})
use_precompiled_header(${PROJECT_NAME} "${CMAKE_CURRENT_SOURCE_DIR}/header.h"
"${CMAKE_CURRENT_SOURCE_DIR}/stdafx.cpp")
if(WIN32)
set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:CONSOLE")
endif(WIN32)
project(main)
add_executable(${PROJECT_NAME} WIN32 ${DIR_SRCS})
if(WIN32)
set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:CONSOLE")
endif(WIN32)
header.h
#ifndef HEADER_H
#define HEADER_H
#include <algorithm>
#include <deque>
#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <thread>
#include <utility>
#include <vector>
#ifdef WIN32
#include <windows.h>
#endif
#endif
stdafx.cpp
#include "header.h"
main.cpp
#include "header.h"
int main() {
return 0;
}
通过CMake生成VS工程,main
工程是不带pch的,main-pch
带pch的。
1.启用VS的编译时间统计功能
2.编译BUILD_ALL工程
3>Project Performance Summary:
3> 16 ms F:\work\project\vs2015\speed-up-cxx-compilation\build\ZERO_CHECK.vcxproj 4 calls
3> 0 ms GetTargetPath 1 calls
3> 0 ms GetNativeManifest 1 calls
3> 0 ms GetResolvedLinkLibs 1 calls
3> 16 ms GetCopyToOutputDirectoryItems 1 calls
3> 2027 ms F:\work\project\vs2015\speed-up-cxx-compilation\build\main-pch.vcxproj 1 calls
3> 2027 ms Build 1 calls
2>Project Performance Summary:
2> 0 ms F:\work\project\vs2015\speed-up-cxx-compilation\build\ZERO_CHECK.vcxproj 4 calls
2> 0 ms GetTargetPath 1 calls
2> 0 ms GetNativeManifest 1 calls
2> 0 ms GetResolvedLinkLibs 1 calls
2> 0 ms GetCopyToOutputDirectoryItems 1 calls
2> 2867 ms F:\work\project\vs2015\speed-up-cxx-compilation\build\main.vcxproj 1 calls
2> 2867 ms Build 1 calls
可以看到带预编译的工程耗时2027ms,不带预编译的工程耗时2867ms。
GCC生成的header.h.gch
文件是header.h
的PCH
版本,然后,在编译main.cpp
时,GCC使用预编译头替代解析原始头文件。效果就是编译时间缩短了5倍!这个例子有点夸张,因为main.cpp
基本没做任何事情,header.h
包含了标准库里的几个臃肿的头文件。一个cpp文件有100行,并且包含20几个文件,这种情况频率有多高?我觉得,太常见了。
此外,如果你的工程中使用了C++标准库,则可能在90%的源文件中都用到了,每个头文件中有数以千计的高度模板化C++代码。即使你不使用标准库,也至少有一个为其他工程提供基础工具的库,它很可能也包含复杂的模板代码,这样的库能完美体现预编译功能的魔力。
微软的C++编译器使用stdafx.h
[3]头文件约定已经有一段时间了,主要是为了包含“系统头文件和使用很频繁但是很少改动的项目头文件”
[4],这是预编译头文件的一种常见用法。如果使用频繁,改动很少,就值得预编译这些昂贵的头文件,然后在后续编译中直接使用。
上图中stdafx.h
包含C++标准库头文件和windows.h
文件,其他源文件包含stdafx.h
并从预编译头中受益。stdafx.h
必须在每个
源文件中包含,虽然可以手动来包含预编译头,但是比较冗余,并且不具备很好的跨平台性。幸运的是,MSVC附带了Forced Included File
选项用以强制包含stdafx.h
而不用修改单个源文件。存在stdafx.cpp
是因为至少要有一个源文件使用"/Yc"(create precompiled header)
选项,编译这个文件的结果就是包含预编译头文件。
我不会详细介绍怎样设置MSVC来使用预编译头文件,网上已经有很多教程,比如[5]。如果你使用CMake要保证至少有一个使用MSVC/GCC/Clang编译器的模块可以无缝添加PCH[6],对于其他编译系统,也应该比较容易设置PCH,毕竟它这么流行。
可以使用GCC/Clang编译器来模拟MSVC模块,对于跨平台代码,可以直接使用stdafx.h
,但是这里我们改个名叫precompile.h
,下面是最简单的Makefile:
CXX = g++
CXXFLAGS = -std=c++11
OBJ = main.o a.o b.o
# file containing headers for precompilation
PCH_SRC = precompile.h
# project headers that are going to get precompiled (pch dependencies)
PCH_HEADERS = header.h
# pch output filename
PCH_OUT = precompile.h.gch
main: $(OBJ)
$(CXX) $(CXXFLAGS) -o $@ $^
$(PCH_OUT): $(PCH_SRC) $(PCH_HEADERS)
$(CXX) $(CXXFLAGS) -o $@ $<
%.o: %.cpp $(PCH_OUT)
$(CXX) $(CXXFLAGS) -include $(PCH_SRC) -c -o $@ $<
如果你不熟悉make语法也没关系,因为我也知之甚少,这里主要为了演示。假设所有必须文件都准备妥当,执行make
命令会有如下输出:
$ make
g++ -std=c++11 -o precompile.h.gch precompile.h
g++ -std=c++11 -include precompile.h -c -o main.o main.cpp
g++ -std=c++11 -include precompile.h -c -o a.o a.cpp
g++ -std=c++11 -include precompile.h -c -o b.o b.cpp
g++ -std=c++11 -o main main.o a.o b.o
首先,预编译头文件precompile.h
。接着,使用-include precompile.h
选项编译所有cpp文件,这个标识强制编译器把precompile.h
作为源文件中第一个包含的文件。因此,对于每个使用此标识的文件,编译器会在搜索路径中寻找precompile.h
之前寻找precompile.h.gch
。最后,编译器把中间文件打包进可执行文件。
GCC和Clang有一点小小的区别,Clang只有存在-include
标识时才会查找预编译头,而GCC对于每条#include
指令都会查找。GCC的行为可以用于单独预编译项目头,换句话说,在一个TU中使用多个PCHs。不幸的是,这不太现实,因为还有许多限制。
就我而言,PCHs最大的限制是每个TU只有一个PCH。因此,使用它们的唯一合理方法是把最少改动的头文件(一般是C++标准库头文件+系统头文件)放入预编译头中。再回到上面的Makefile,PCH依赖header.h
,所有源文件依赖PCH,所以,修改header.h
的代价会非常大:
$ make
make: 'main' is up to date.
$ touch header.h
$ make
g++ -std=c++11 -o precompile.h.gch precompile.h
g++ -std=c++11 -include precompile.h -c -o main.o main.cpp
g++ -std=c++11 -include precompile.h -c -o a.o a.cpp
g++ -std=c++11 -include precompile.h -c -o b.o b.cpp
g++ -std=c++11 -o main main.o a.o b.o
修改PCH中的任何一个头文件就意味完全重新编译,即使每个月更改一次的头文件也是如此,这个问题与团队和代码库体积成正比,我们需要模块化来解决它。
其他常见的PCH限制:
- PCH头必须是TU中的第一行(预处理器指令除外)。
- PCH必须使用相同的编译器选项编译,想修改选项?完全重编。
- 用于编译的预处理器指令必须与用于PCH的一致(或不影响编译内容预编译内容的差异)。
每个编译器对PCH的实现不尽相同,所以,请查阅编译器文档。[7]
预编译头可以帮助大大减少编译时间。 如今,大多数C++编译器都以某种方式支持PCH,并且在大多数项目中几乎不使用它们。 虽然它们的使用有限制,但是在模块化进入C++标准之前,这是最好的方式。