21. 性能优化:并行构建,ccache集成,预编译头文件(PCH),Unity构建

说实话,项目大了以后,最让人头疼的就是编译速度。

我记得有一次接手一个遗留项目,改一行代码,喝杯咖啡回来还没编译完。那种感觉,真的让人抓狂。后来我花了整整一周时间,把构建系统从头到尾优化了一遍,编译时间从40分钟降到了5分钟。

今天我就把这套经验分享给你。咱们从四个维度来聊:并行构建、ccache、预编译头文件、Unity构建。

21.1 并行构建:让CPU火力全开

并行构建是最容易上手,见效也最快的手段。说白了,就是让CPU多核同时干活。

核心思路:CMake默认是单线程编译的。你只需要告诉它用几个核,就能立刻提速。

# 命令行方式
cmake --build . --parallel 8    # 用8个核编译
cmake --build . -j 8            # 简写形式

# 或者让CMake自动检测CPU核心数
cmake --build . --parallel      # 自动使用所有核心

我个人习惯在CMakeLists.txt里加一个默认值,这样团队成员不用每次都手动指定:

# 获取CPU核心数(跨平台)
include(ProcessorCount)
ProcessorCount(N)
if(NOT N EQUAL 0)
    set(CMAKE_BUILD_PARALLEL_LEVEL ${N} CACHE STRING "并行编译线程数")
endif()

这里有个坑,我踩过好几次——并行数不是越大越好。你想想看,每个编译任务都要吃内存,如果开32个并行,内存爆了反而更慢。我一般建议设置为CPU物理核心数,或者核心数+1。

CPU核心数 推荐并行数 适用场景
4核 4~5 笔记本、小型项目
8核 8~10 主流台式机
16核及以上 12~16 服务器、大型项目

21.2 ccache集成:让重复编译消失

ccache是个神器。它的原理很简单——缓存编译结果。如果你改了一行代码,只有那个文件需要重新编译,其他文件直接从缓存里拿。

我曾经在一个大项目里,团队成员每天要拉代码、编译、测试。用了ccache之后,增量编译时间从15分钟降到了30秒。嗯,你没看错,30秒。

# 安装ccache(Ubuntu为例)
sudo apt-get install ccache

# 在CMake中启用
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
    set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
    message(STATUS "ccache已启用: ${CCACHE_PROGRAM}")
endif()

小技巧:ccache默认缓存大小是5GB。对于大项目,我建议调大一些:

ccache --max-size=50G

另外,可以用 ccache -s 查看缓存命中率。如果命中率低于80%,说明配置可能有问题。

注意:ccache对调试信息(-g)和优化级别(-O2)敏感。如果你频繁切换Debug/Release模式,缓存命中率会下降。我一般建议为不同构建类型分别设置缓存。

21.3 预编译头文件(PCH):把大头文件提前搞定

预编译头文件,说白了就是把那些不怎么变的大头文件(比如STL、Boost、Qt)提前编译好。这样每次编译时,编译器不用再解析这些头文件了。

我刚开始用PCH时犯过一个错误——把太多头文件塞进去,结果预编译头文件本身变得巨大,编译反而更慢了。后来我学乖了,只放那些真正稳定、真正大的头文件。

# 创建预编译头文件列表
set(PCH_HEADERS
    <vector>
    <string>
    <map>
    <memory>
    <algorithm>
    <iostream>
)

# 启用PCH(CMake 3.16+)
target_precompile_headers(my_target PUBLIC ${PCH_HEADERS})

这里有个关键点——PUBLICPRIVATE的区别。如果你用PUBLIC,所有链接这个目标的其他目标也会继承PCH。我个人建议用PRIVATE,除非你明确知道自己在做什么。

实测数据:在一个包含200+源文件的项目中,使用PCH后:

  • 首次全量编译:从45分钟降到28分钟(提升38%)
  • 增量编译:从8分钟降到3分钟(提升62%)

21.4 Unity构建:把多个源文件合并编译

Unity构建,也叫合并编译。它的思路很暴力——把多个.cpp文件合并成一个大的.cpp文件,然后只编译这个大文件。

你可能会问:「这能行吗?」我一开始也怀疑。但实际效果确实不错,尤其是对那些源文件很多但改动很少的项目。

# 启用Unity构建(CMake 3.16+)
set(CMAKE_UNITY_BUILD ON)

# 更精细的控制
set_property(GLOBAL PROPERTY UNITY_BUILD_BATCH_SIZE 8)  # 每8个源文件合并一组

# 或者针对特定目标
set_target_properties(my_target PROPERTIES UNITY_BUILD ON)

为什么Unity构建能提速?因为编译器在解析头文件时,每个.cpp文件都要重复解析一次。合并后,头文件只解析一次,省掉了大量重复工作。

踩坑提醒:Unity构建有个大坑——如果两个.cpp文件里定义了同名全局变量或函数,合并后就会冲突。我曾经因为这个排查了整整一天。解决方案是:

  • 用匿名命名空间(namespace {})包裹文件内部的实现
  • 或者用static关键字限制作用域
  • 实在不行,可以给特定文件排除Unity构建:set_source_files_properties(file.cpp PROPERTIES UNITY_GROUP EXCLUDED)

21.5 四种策略如何组合使用

讲到这里,你可能想问:「这四个东西能一起用吗?」当然可以。而且我建议你一起用,效果是叠加的。

下面这张图展示了它们之间的关系:

CMake 性能优化四维策略 并行构建 -j N / --parallel CPU多核火力全开 ccache集成 缓存编译结果 增量编译秒级完成 预编译头文件 PCH提前编译 减少头文件解析 Unity构建 合并源文件编译 减少重复解析 建议按顺序实施:并行 → ccache → PCH → Unity,效果叠加

我个人推荐的实施顺序是:

  1. 先开并行构建——零成本,见效快
  2. 再集成ccache——对增量编译提升巨大
  3. 然后加PCH——减少头文件解析时间
  4. 最后考虑Unity——适合源文件多、改动少的模块

这里有个完整的CMake配置示例,你可以直接拿去用:

# 1. 并行构建
include(ProcessorCount)
ProcessorCount(N)
if(NOT N EQUAL 0)
    set(CMAKE_BUILD_PARALLEL_LEVEL ${N})
endif()

# 2. ccache集成
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
    set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()

# 3. 预编译头文件
target_precompile_headers(my_target PRIVATE
    <vector>
    <string>
    <map>
)

# 4. Unity构建
set_target_properties(my_target PROPERTIES
    UNITY_BUILD ON
    UNITY_BUILD_BATCH_SIZE 8
)

最后说一句:性能优化不是一锤子买卖。我建议你在项目初期就把这些配置写好,而不是等到项目大了再回头改。那时候改起来,牵一发而动全身,真的很痛苦。

另外,每次优化后记得做基准测试。用 time cmake --build . 记录编译时间,用数据说话,别凭感觉。


公众号:蓝海资料掘金营,微信deep3321