28、性能优化:减少重复配置,使用ccache,并行编译,Unity Build(合并编译单元)
说实话,做CMake项目最让人头疼的不是写代码,而是等编译。尤其是大项目,改一行代码,喝杯咖啡回来还没编译完。我早期带团队的时候,就遇到过这种尴尬——大家每天浪费在等待编译上的时间,比写代码还多。
所以这一章,咱们聊聊怎么让CMake项目跑得更快。说白了,就是四个方向:减少重复劳动、缓存中间结果、多核并行、合并编译单元。每个方向都有对应的CMake技巧,咱们一个一个来。
核心思路:编译优化的本质是「能不编译就不编译,能一起编译就一起编译」。别小看这几招,大项目里能把编译时间从半小时压到五分钟。
28.1 减少重复配置:缓存与预设
每次执行 cmake .. 的时候,CMake都会重新扫描工具链、查找依赖库、生成构建规则。如果项目大、依赖多,这个过程本身就要花几十秒甚至几分钟。
我的做法:用 CMakePresets.json 把常用配置固化下来。这样团队成员不用每次都敲一长串 -D 参数,也避免了「我忘了加这个flag」的尴尬。
// CMakePresets.json
{
"version": 3,
"configurePresets": [
{
"name": "default",
"displayName": "默认配置",
"binaryDir": "${sourceDir}/build/default",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_CXX_FLAGS": "-O2 -march=native"
}
},
{
"name": "debug",
"displayName": "调试模式",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_FLAGS": "-g -O0"
}
}
]
}
用的时候只需要:
cmake --preset=debug
cmake --build --preset=debug
嗯,这里要注意:CMakePresets.json 要放在项目根目录,并且建议纳入版本控制。这样新人拉下来就能直接编译,不用问东问西。
28.2 使用ccache:缓存编译结果
ccache 是个好东西。它的原理很简单:把每次编译的中间结果(.o文件)缓存起来。下次编译时,如果源文件没变、编译选项没变,就直接从缓存里拿结果。
我曾在某个嵌入式项目里用过它。那个项目每次全量编译要40分钟,加上ccache之后,增量编译基本秒过。团队里有个同事还开玩笑说「编译太快了,我都没时间刷手机了」。
配置方式:
# 在 CMakeLists.txt 开头加入
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
message(STATUS "ccache 已启用")
endif()
或者更现代的方式,用 CMAKE_CXX_COMPILER_LAUNCHER:
cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ..
小技巧:ccache 默认缓存大小是5GB。如果项目大,建议调大:ccache --max-size=20G。另外,可以用 ccache -s 查看缓存命中率,如果命中率低于80%,说明配置可能有问题。
28.3 并行编译:让CPU满负荷工作
现在的CPU动不动就8核16线程,但默认情况下,CMake只用一个线程编译。这不是浪费吗?
解决方案:用 -j 参数指定并行数。
cmake --build . -j $(nproc) # Linux/macOS
cmake --build . -j 8 # Windows 手动指定
我个人习惯在 CMakePresets.json 里也加上这个:
"buildPresets": [
{
"name": "default",
"configurePreset": "default",
"jobs": 8
}
]
为什么会这样?因为CMake生成的构建系统(比如Makefile或Ninja)本身就支持并行。你只是没告诉它「你可以用更多资源」。说白了,就是没把CPU的潜力榨干。
注意:并行数不是越大越好。如果内存不够,开太多并行会导致内存溢出,反而更慢。我一般建议设置为「CPU核心数+1」或者「核心数×2」。具体可以试一下,找到自己机器的甜点值。
28.4 Unity Build:合并编译单元
这个技巧可能有些朋友不太熟悉。Unity Build 的核心思想是:把多个 .cpp 文件合并成一个大的 .cpp 文件来编译。
为什么要这么做?因为每个 .cpp 文件编译时,都要重复解析头文件。如果100个文件都 #include 了同一个头文件,那这个头文件就被解析了100次。合并之后,只解析一次。
CMake 里的实现方式:
# 开启 Unity Build(CMake 3.16+)
set(CMAKE_UNITY_BUILD ON)
# 或者针对特定目标开启
set_target_properties(my_target PROPERTIES
UNITY_BUILD ON
UNITY_BUILD_BATCH_SIZE 8 # 每8个源文件合并为一个单元
)
我曾经在一个游戏引擎项目里试过这个。原本全量编译需要12分钟,开启Unity Build后降到4分钟。当然,代价是增量编译变慢了——因为改一个文件,整个合并单元都要重新编译。所以我的建议是:Release构建用Unity Build,Debug构建不用。
最佳实践:
- Release/生产构建:开启 Unity Build + ccache + 并行编译
- Debug/开发构建:关闭 Unity Build,只开 ccache 和并行编译
- CI/CD环境:全开,但注意控制并行数,避免把服务器搞崩
28.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到,四个优化方向分别作用于编译流程的不同阶段:
28.6 综合配置示例
最后,给一个完整的 CMakeLists.txt 片段,把上面所有技巧整合在一起:
cmake_minimum_required(VERSION 3.20)
project(MyOptimizedProject LANGUAGES CXX)
# ---- 1. ccache ----
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
message(STATUS "ccache 已启用")
endif()
# ---- 2. Unity Build(仅 Release) ----
if(CMAKE_BUILD_TYPE STREQUAL "Release")
set(CMAKE_UNITY_BUILD ON)
set(CMAKE_UNITY_BUILD_BATCH_SIZE 8)
message(STATUS "Unity Build 已启用(Release模式)")
endif()
# ---- 3. 并行编译(通过预设或命令行指定) ----
# 命令行: cmake --build . -j $(nproc)
# ---- 4. 目标定义 ----
add_executable(my_app src/main.cpp src/renderer.cpp src/input.cpp)
target_compile_features(my_app PRIVATE cxx_std_17)
嗯,到这里,性能优化的四个核心技巧就讲完了。你想想看,如果每个项目都按这个配置来,团队每天能省下多少等待时间?我个人觉得,这可能是CMake里「投入产出比」最高的几个设置之一了。