28、性能优化:并行构建、Unity构建(Jumbo)、预编译头文件、编译器缓存(ccache/sccache)、链接时优化(LTO)
说实话,项目规模一大,编译时间就成了团队效率的杀手。我见过不少团队,每天等编译等得抓狂,喝杯咖啡回来还没跑完。其实CMake给了我们一套完整的加速工具箱,关键是你得知道怎么用。
今天咱们就把这五个加速手段聊透:并行构建、Unity构建、预编译头文件、编译器缓存、还有链接时优化。每个都有它的适用场景,也有它的坑。
并行构建:最基础的加速手段
这个最简单,说白了就是让CPU多核干活。CMake本身不直接控制并行度,它把这事儿交给了底层的构建工具。
# 用Makefile时
make -j$(nproc)
# 用Ninja时(Ninja默认就并行)
ninja
# 在CMake中指定
cmake --build . --parallel 8
我个人习惯在CMakePresets.json里配好并行数,省得每次敲命令。
Unity构建(Jumbo):把多个源文件合并编译
这个思路很有意思——把多个.cpp文件合并成一个大的.cpp文件来编译。为什么要这么做?因为编译器启动是有开销的,头文件解析也是重复劳动。合并后,这些开销只产生一次。
# 启用Unity构建
set(CMAKE_UNITY_BUILD ON)
# 设置合并批次大小(每个Unity文件包含的源文件数)
set(CMAKE_UNITY_BUILD_BATCH_SIZE 16)
# 针对特定目标启用
target_sources(my_target PRIVATE
FILE_SET HEADERS
BASE_DIRS src
FILES src/utils.h src/core.h
)
set_target_properties(my_target PROPERTIES
UNITY_BUILD ON
UNITY_BUILD_BATCH_SIZE 8
)
我在一个大型游戏引擎项目里用过这个,编译时间从45分钟降到了12分钟。效果确实惊人。但要注意,Unity构建有个大坑——如果两个源文件里定义了同名全局变量或函数,合并后就会冲突。
预编译头文件(PCH):把稳定的头文件缓存起来
预编译头文件,说白了就是把那些不怎么变的标准库头文件(像<vector>、<string>)提前编译好,存成二进制格式。后面每个.cpp文件编译时,直接加载这个缓存,省去重复解析的时间。
# 创建预编译头文件
target_precompile_headers(my_target PRIVATE
<vector>
<string>
<memory>
"my_project_common.h"
)
# 也可以指定为公共接口
target_precompile_headers(my_target INTERFACE
<vector>
<string>
)
嗯,这里要注意:预编译头文件里的内容越稳定越好。我曾经见过有人把业务代码也塞进PCH,结果每次改业务逻辑都要重新编译整个PCH,反而更慢了。
编译器缓存:ccache和sccache
这个我特别喜欢。原理很简单:编译器缓存会记录每次编译的输入(源文件内容、编译选项、头文件依赖),如果输入没变,就直接从缓存里拿结果,跳过真正的编译过程。
# 安装ccache
# apt install ccache # Ubuntu
# brew install ccache # macOS
# 在CMake中启用
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
endif()
# 或者更现代的方式
cmake -DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache ..
sccache是Mozilla出的,支持分布式缓存,适合CI环境。我在团队CI里配过sccache,配合S3做远端缓存,全团队共享编译结果,效果非常好。
链接时优化(LTO):编译阶段的最后一道优化
LTO跟前面几个不太一样,它不是为了加速编译,而是为了优化生成的可执行文件。原理是让链接器能看到所有编译单元的中间表示,从而做跨模块的优化——比如内联函数、消除死代码。
# 全局启用LTO
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
# 针对特定目标启用
set_target_properties(my_target PROPERTIES
INTERPROCEDURAL_OPTIMIZATION ON
)
# 指定LTO模式(ThinLTO是LLVM的轻量版)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_THINLTO ON)
不过LTO有个代价——链接时间会变长。我记得在一个金融交易系统项目里,启用LTO后链接时间从30秒涨到了5分钟。但生成的二进制性能提升了15%,对于高频交易来说,这15%值了。
知识体系总览
下面这张图把五种加速手段的关系和适用场景梳理清楚了:
组合使用的最佳实践
这些手段不是互斥的,可以组合使用。我一般这样配:
- 日常开发:并行构建 + ccache + 预编译头文件。编译速度最快,改动后增量编译也快。
- CI流水线:并行构建 + sccache(远端缓存)+ Unity构建。CI环境通常从零编译,Unity构建的优势最大。
- 发布版本:并行构建 + LTO + 预编译头文件。发布版追求性能,LTO的编译时间增加可以接受。
好了,这五种手段讲完了。你想想看,你的项目现在卡在哪一步?是编译太慢,还是链接太久?对症下药,效果才最好。