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})
这里有个关键点——PUBLIC和PRIVATE的区别。如果你用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 四种策略如何组合使用
讲到这里,你可能想问:「这四个东西能一起用吗?」当然可以。而且我建议你一起用,效果是叠加的。
下面这张图展示了它们之间的关系:
我个人推荐的实施顺序是:
- 先开并行构建——零成本,见效快
- 再集成ccache——对增量编译提升巨大
- 然后加PCH——减少头文件解析时间
- 最后考虑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