性能优化:并行构建、ccache集成、预编译头文件、Unity构建、增量构建
说实话,很多团队写CMake只关心“能不能编译通过”,从来不管“编译快不快”。我见过一个项目,改一行代码要等三分钟重新链接,整个团队一天下来光等编译就浪费两小时。这其实是个管理问题——不是机器不够快,是构建系统没调好。
这一章,我把CMake里真正能提速的五个手段串起来讲。并行构建、ccache、预编译头文件、Unity构建、增量构建,每个我都踩过坑,也都有实战经验。你按顺序配好,项目编译时间砍掉一半不是梦。
1. 并行构建:让CPU满负荷运转
CMake本身不编译,它生成构建文件后交给底层工具(Make、Ninja)去跑。并行构建说白了就是让这些工具同时编译多个源文件。
我个人习惯在CMake命令行里直接指定并行数:
# 用Make,指定4个并行任务
cmake --build build --parallel 4
# 或者简写
cmake --build build -j 4
# 让CMake自动检测CPU核心数
cmake --build build --parallel
Ninja默认就是并行的,你甚至不用加参数。Make的话,不指定-j默认单线程,慢得你想砸电脑。
你可以在CMakeLists.txt里设置默认并行数,但我不建议这么做——应该让用户自己决定:
# 不推荐:硬编码并行数
set(CMAKE_MAKE_PROGRAM "make -j4")
# 推荐:让用户通过命令行控制
2. ccache集成:缓存编译结果,避免重复劳动
ccache是个神器。它把每次编译的预处理结果、编译选项、目标文件哈希后缓存起来。下次同样的输入,直接跳过编译,从缓存里拿结果。
集成方式极其简单:
# 在CMakeLists.txt最前面加上
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
message(STATUS "ccache found: ${CCACHE_PROGRAM}")
endif()
嗯,这里要注意:RULE_LAUNCH_COMPILE这个属性只在Makefile生成器下生效。Ninja和Visual Studio有各自的机制。
对于Ninja,你需要设置环境变量:
export CCACHE_CPP2=true
cmake -G Ninja -DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache ..
ccache --max-size=50G
另外,如果用了绝对路径的调试信息,换机器编译缓存就失效了。建议用-gmlt代替-g。
3. 预编译头文件:把稳定的头文件提前编译
预编译头文件(PCH)的原理很简单:项目中那些万年不变的头文件(STL、Boost、Qt等),每次编译都要解析一遍,纯属浪费。把它们提前编译成二进制格式,后续编译直接加载,省掉解析时间。
CMake 3.16开始原生支持PCH:
# 指定一个头文件作为预编译头
target_precompile_headers(my_target PRIVATE
"my_common.h"
)
PRIVATE表示只对当前目标生效。如果你想让所有链接的目标都共享,用INTERFACE或PUBLIC。
实际项目中,我建议把最常用的头文件列进去:
target_precompile_headers(my_target PRIVATE
# 系统头文件
4. Unity构建:把多个源文件合并编译
Unity构建也叫“合并构建”或“Jumbo构建”。它的思路很暴力:把多个.cpp文件合并成一个大的.cpp文件,然后只编译这个大文件。
为什么要这么做?因为编译器的启动开销、头文件解析开销被摊薄了。比如你有100个.cpp文件,每个都包含
CMake里开启Unity构建很简单:
# 全局开启
set(CMAKE_UNITY_BUILD ON)
# 或者针对特定目标
set_target_properties(my_target PROPERTIES UNITY_BUILD ON)
你还可以控制每个Unity文件包含多少个源文件:
set_target_properties(my_target PROPERTIES
UNITY_BUILD ON
UNITY_BUILD_BATCH_SIZE 16 # 每16个源文件合并成一个
)
5. 增量构建:只编译改动的文件
增量构建是CMake的默认行为,但很多人用不好。核心在于:CMake通过文件时间戳和依赖关系判断哪些文件需要重新编译。
常见问题:
- 头文件依赖没写对:改了头文件,但CMake不知道哪些.cpp依赖它,导致漏编译。
- 时间戳混乱:git checkout切换分支后,文件时间戳可能比目标文件还旧,导致增量构建失效。
解决方案:
# 确保CMake正确扫描头文件依赖
set(CMAKE_DEPENDS_USE_COMPILER TRUE)
# 或者手动指定依赖
target_include_directories(my_target PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
对于git导致的时间戳问题,我习惯在构建前加一步:
# 在CMakeLists.txt里
add_custom_target(touch_files ALL
COMMAND find ${CMAKE_SOURCE_DIR} -name "*.cpp" -o -name "*.h" | xargs touch
COMMENT "Touching source files to fix timestamps"
)
不过说实话,这招有点粗暴。更好的做法是用ccache,它不看时间戳,只看内容哈希,从根本上避免这个问题。
知识体系总览
下面这张图把这五个优化手段的关系画清楚了。你可以把它们理解成五层加速器,层层叠加:
综合配置示例
下面是一个完整的CMakeLists.txt,把上面五个手段都集成进去。你可以直接拿去用:
cmake_minimum_required(VERSION 3.20)
project(OptimizedProject LANGUAGES CXX)
# ---- 1. ccache 集成 ----
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
message(STATUS "ccache enabled")
endif()
# ---- 2. 全局设置 ----
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ---- 3. 开启 Unity 构建(默认关闭,通过命令行开启) ----
option(ENABLE_UNITY_BUILD "Enable unity build" OFF)
if(ENABLE_UNITY_BUILD)
set(CMAKE_UNITY_BUILD ON)
set(CMAKE_UNITY_BUILD_BATCH_SIZE 16)
endif()
# ---- 4. 定义库目标 ----
add_library(core_lib
src/core/engine.cpp
src/core/manager.cpp
src/core/worker.cpp
src/core/utils.cpp
)
# ---- 5. 预编译头文件 ----
target_precompile_headers(core_lib PRIVATE
"src/core/common.h"
)
# ---- 6. 包含目录 ----
target_include_directories(core_lib PUBLIC include)
# ---- 7. 可执行目标 ----
add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE core_lib)
构建时这样用:
# 首次构建
cmake -B build -G Ninja -DENABLE_UNITY_BUILD=ON
cmake --build build --parallel
# 后续增量构建
cmake --build build --parallel
最后说一句:性能优化不是一锤子买卖。项目规模变了、编译器版本变了、甚至操作系统变了,最优配置都会变。我建议每季度重新跑一次基准测试,看看当前配置是否还合适。毕竟,构建速度就是开发效率,开发效率就是钱。