28、性能优化:并行构建、Unity构建(Jumbo)、预编译头文件、编译器缓存(ccache/sccache)、链接时优化(LTO)

说实话,项目规模一大,编译时间就成了团队效率的杀手。我见过不少团队,每天等编译等得抓狂,喝杯咖啡回来还没跑完。其实CMake给了我们一套完整的加速工具箱,关键是你得知道怎么用。

今天咱们就把这五个加速手段聊透:并行构建、Unity构建、预编译头文件、编译器缓存、还有链接时优化。每个都有它的适用场景,也有它的坑。

并行构建:最基础的加速手段

这个最简单,说白了就是让CPU多核干活。CMake本身不直接控制并行度,它把这事儿交给了底层的构建工具。

# 用Makefile时
make -j$(nproc)

# 用Ninja时(Ninja默认就并行)
ninja

# 在CMake中指定
cmake --build . --parallel 8

我个人习惯在CMakePresets.json里配好并行数,省得每次敲命令。

小技巧:并行数一般设为CPU物理核心数的1.5到2倍。设太多反而会因为上下文切换降低效率。

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构建有个大坑——如果两个源文件里定义了同名全局变量或函数,合并后就会冲突。

注意: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,反而更慢了。

最佳实践: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做远端缓存,全团队共享编译结果,效果非常好。

经验之谈:ccache的命中率很关键。如果命中率低于80%,说明你的编译环境有问题——可能是头文件依赖没配好,或者编译选项总在变。

链接时优化(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%值了。

注意:LTO可能会暴露一些平时隐藏的bug。比如,有些代码在普通编译下因为没被内联而正常工作,LTO一优化就出问题。建议在测试环境充分验证后再上线。

知识体系总览

下面这张图把五种加速手段的关系和适用场景梳理清楚了:

CMake性能优化五大利器 编译加速策略 并行构建 make -j / ninja 多核并行编译 Unity构建 合并源文件 减少编译开销 预编译头文件 缓存稳定头文件 减少重复解析 编译器缓存 ccache/sccache 复用编译结果 链接时优化 跨模块优化 提升运行性能 各手段对比 手段 加速编译 优化产物 风险等级 并行构建 ✅ 显著 ❌ 无 Unity构建 ✅ 显著 ❌ 无 预编译头文件 ✅ 中等 ❌ 无 编译器缓存 ✅ 显著 ❌ 无 LTO ❌ 减慢 ✅ 显著

组合使用的最佳实践

这些手段不是互斥的,可以组合使用。我一般这样配:

  1. 日常开发:并行构建 + ccache + 预编译头文件。编译速度最快,改动后增量编译也快。
  2. CI流水线:并行构建 + sccache(远端缓存)+ Unity构建。CI环境通常从零编译,Unity构建的优势最大。
  3. 发布版本:并行构建 + LTO + 预编译头文件。发布版追求性能,LTO的编译时间增加可以接受。
核心原则:加速编译的手段(并行、Unity、PCH、缓存)优先用在开发阶段。优化产物的手段(LTO)用在发布阶段。别搞反了。

好了,这五种手段讲完了。你想想看,你的项目现在卡在哪一步?是编译太慢,还是链接太久?对症下药,效果才最好。