[关闭]
@lniwn 2017-08-23T21:53:30.000000Z 字数 5778 阅读 5457

加快C++编译速度之一(预编译头)

C++ 翻译


系列文章

原文地址
http://itscompiling.eu/2017/01/12/precompiled-headers-cpp-compilation/

如何使用PCH(precompiled headers)加快C++编译速度

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

  1. cmake_minimum_required(VERSION 3.1)
  2. set_property(GLOBAL PROPERTY USE_FOLDERS ON)
  3. set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS true) # 使用简版if else
  4. set(CMAKE_CXX_FLAGS_DEBUG "/MDd /Zi /Ob0 /Od /RTC1 /D_DEBUG")
  5. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FC") # 输出信息使用全路径
  6. set(CMAKE_CXX_STANDARD 11) # 3.1以上版本支持的属性,使用C++11
  7. add_definitions(-DUNICODE -D_UNICODE)
  8. aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} DIR_SRCS)
  9. macro(use_precompiled_header TARGET HEADER_FILE SRC_FILE)
  10. get_filename_component(HEADER ${HEADER_FILE} NAME)
  11. # Use MSVC_IDE to exclude NMake from using PCHs
  12. if (MSVC AND NOT NMAKE AND NOT OGRE_UNITY_BUILD)
  13. set_target_properties(${TARGET} PROPERTIES COMPILE_FLAGS /Yu"${HEADER}")
  14. set_source_files_properties(${SRC_FILE}
  15. PPROPERTIES COMPILE_FLAGS /Yc"${HEADER}"
  16. )
  17. elseif (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_CLANGXX)
  18. endif ()
  19. endmacro(use_precompiled_header)
  20. project(main-pch)
  21. add_executable(${PROJECT_NAME} WIN32 ${DIR_SRCS})
  22. use_precompiled_header(${PROJECT_NAME} "${CMAKE_CURRENT_SOURCE_DIR}/header.h"
  23. "${CMAKE_CURRENT_SOURCE_DIR}/stdafx.cpp")
  24. if(WIN32)
  25. set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:CONSOLE")
  26. endif(WIN32)
  27. project(main)
  28. add_executable(${PROJECT_NAME} WIN32 ${DIR_SRCS})
  29. if(WIN32)
  30. set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:CONSOLE")
  31. endif(WIN32)

header.h

  1. #ifndef HEADER_H
  2. #define HEADER_H
  3. #include <algorithm>
  4. #include <deque>
  5. #include <iostream>
  6. #include <map>
  7. #include <memory>
  8. #include <set>
  9. #include <thread>
  10. #include <utility>
  11. #include <vector>
  12. #ifdef WIN32
  13. #include <windows.h>
  14. #endif
  15. #endif

stdafx.cpp

  1. #include "header.h"

main.cpp

  1. #include "header.h"
  2. int main() {
  3. return 0;
  4. }

通过CMake生成VS工程,main工程是不带pch的,main-pch带pch的。
1.启用VS的编译时间统计功能image_1bm25kqlc16fee8t18me17l8i7p9.png-44.1kB
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.hPCH版本,然后,在编译main.cpp时,GCC使用预编译头替代解析原始头文件。效果就是编译时间缩短了5倍!这个例子有点夸张,因为main.cpp基本没做任何事情,header.h包含了标准库里的几个臃肿的头文件。一个cpp文件有100行,并且包含20几个文件,这种情况频率有多高?我觉得,太常见了。
此外,如果你的工程中使用了C++标准库,则可能在90%的源文件中都用到了,每个头文件中有数以千计的高度模板化C++代码。即使你不使用标准库,也至少有一个为其他工程提供基础工具的库,它很可能也包含复杂的模板代码,这样的库能完美体现预编译功能的魔力。

MSVC and stdafx.h

微软的C++编译器使用stdafx.h[3]头文件约定已经有一段时间了,主要是为了包含“系统头文件和使用很频繁但是很少改动的项目头文件”[4],这是预编译头文件的一种常见用法。如果使用频繁,改动很少,就值得预编译这些昂贵的头文件,然后在后续编译中直接使用。
MSVC model for precompiled headers
上图中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 and Clang

可以使用GCC/Clang编译器来模拟MSVC模块,对于跨平台代码,可以直接使用stdafx.h,但是这里我们改个名叫precompile.h,下面是最简单的Makefile:

  1. CXX = g++
  2. CXXFLAGS = -std=c++11
  3. OBJ = main.o a.o b.o
  4. # file containing headers for precompilation
  5. PCH_SRC = precompile.h
  6. # project headers that are going to get precompiled (pch dependencies)
  7. PCH_HEADERS = header.h
  8. # pch output filename
  9. PCH_OUT = precompile.h.gch
  10. main: $(OBJ)
  11. $(CXX) $(CXXFLAGS) -o $@ $^
  12. $(PCH_OUT): $(PCH_SRC) $(PCH_HEADERS)
  13. $(CXX) $(CXXFLAGS) -o $@ $<
  14. %.o: %.cpp $(PCH_OUT)
  15. $(CXX) $(CXXFLAGS) -include $(PCH_SRC) -c -o $@ $<

如果你不熟悉make语法也没关系,因为我也知之甚少,这里主要为了演示。假设所有必须文件都准备妥当,执行make命令会有如下输出:

  1. $ make
  2. g++ -std=c++11 -o precompile.h.gch precompile.h
  3. g++ -std=c++11 -include precompile.h -c -o main.o main.cpp
  4. g++ -std=c++11 -include precompile.h -c -o a.o a.cpp
  5. g++ -std=c++11 -include precompile.h -c -o b.o b.cpp
  6. 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。不幸的是,这不太现实,因为还有许多限制。

Limitations and drawbacks

就我而言,PCHs最大的限制是每个TU只有一个PCH。因此,使用它们的唯一合理方法是把最少改动的头文件(一般是C++标准库头文件+系统头文件)放入预编译头中。再回到上面的Makefile,PCH依赖header.h,所有源文件依赖PCH,所以,修改header.h的代价会非常大:

  1. $ make
  2. make: 'main' is up to date.
  3. $ touch header.h
  4. $ make
  5. g++ -std=c++11 -o precompile.h.gch precompile.h
  6. g++ -std=c++11 -include precompile.h -c -o main.o main.cpp
  7. g++ -std=c++11 -include precompile.h -c -o a.o a.cpp
  8. g++ -std=c++11 -include precompile.h -c -o b.o b.cpp
  9. g++ -std=c++11 -o main main.o a.o b.o

修改PCH中的任何一个头文件就意味完全重新编译,即使每个月更改一次的头文件也是如此,这个问题与团队和代码库体积成正比,我们需要模块化来解决它。
其他常见的PCH限制:
- PCH头必须是TU中的第一行(预处理器指令除外)。
- PCH必须使用相同的编译器选项编译,想修改选项?完全重编。
- 用于编译的预处理器指令必须与用于PCH的一致(或不影响编译内容预编译内容的差异)。
每个编译器对PCH的实现不尽相同,所以,请查阅编译器文档。[7]

Summary

预编译头可以帮助大大减少编译时间。 如今,大多数C++编译器都以某种方式支持PCH,并且在大多数项目中几乎不使用它们。 虽然它们的使用有限制,但是在模块化进入C++标准之前,这是最好的方式。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注