28、性能优化与调试:--trace与--debug-output选项、cmake --build的并行构建、ccache集成
说实话,CMake 项目一大了,构建速度就成了让人头疼的事。我记得刚接手一个遗留项目时,一次完整构建要跑 40 分钟。同事们都习惯了去接杯咖啡再回来。我当时就想,这肯定不对劲。
这一章,我就跟你聊聊怎么让 CMake 构建跑得更快,以及当构建出问题时,怎么快速定位。说白了,就是两个方向:调试和加速。
28.1 调试利器:--trace 与 --debug-output
你想想看,CMake 脚本写复杂了,变量传递、条件判断、循环逻辑,有时候跑出来的结果跟你预期完全不一样。这时候怎么办?硬看代码?太累了。
CMake 提供了两个非常实用的调试选项,我个人几乎每周都会用到。
28.1.1 --trace:追踪每一行
--trace 选项会打印 CMake 执行的每一行代码,包括它来自哪个文件、哪一行。这就像给 CMake 装了一个“行车记录仪”。
# 基本用法
cmake --trace -B build
# 只追踪特定文件(更实用)
cmake --trace-source=CMakeLists.txt -B build
# 展开变量值
cmake --trace-expand -B build
我在项目中遇到过最典型的一个场景:一个复杂的 foreach 循环里,变量拼接总是出错。用 --trace-expand 一跑,所有变量的实际值一目了然。原来是一个空格符在作怪。
--trace 跑整个项目,信息量太大。先用 --trace-source 锁定你怀疑的那个文件,效率高得多。
28.1.2 --debug-output:更结构化的调试信息
--debug-output 跟 --trace 不同。它不打印每一行代码,而是输出 CMake 内部的关键决策过程。比如:
- 缓存变量的加载与覆盖
- 策略(Policy)的生效与警告
- 模块的查找路径
- 生成器的选择
cmake --debug-output -B build
我曾经遇到一个诡异的问题:同一个 find_package 调用,在 A 机器上能找到库,在 B 机器上就找不到。用 --debug-output 一对比,发现 B 机器上 CMake 搜索的路径少了一条。原来是环境变量 CMAKE_PREFIX_PATH 没设置对。
--debug-output 会输出大量信息。建议配合 2>&1 | grep 或重定向到文件来使用。比如:cmake --debug-output -B build 2>&1 | grep "find_package"
28.2 并行构建:让 cmake --build 跑满 CPU
构建慢?那就让它并行跑。现代 CPU 都有 8 核、16 核甚至更多,不用起来就是浪费。
28.2.1 使用 -j 参数
cmake --build 底层调用的是构建工具(Make、Ninja 等)。-j 参数就是告诉它:“你可以同时跑 N 个编译任务”。
# 使用 4 个并行任务
cmake --build build -j4
# 使用所有 CPU 核心(推荐)
cmake --build build -j$(nproc) # Linux/macOS
cmake --build build -j%NUMBER_OF_PROCESSORS% # Windows
我个人习惯在 CI 脚本里固定写 -j$(nproc)。这样不管换到什么机器上,都能自动利用全部核心。
28.2.2 Ninja vs Make:并行的差异
这里有个细节你可能不知道。Ninja 天生就是为并行设计的。它比 Make 更聪明,能更精确地调度依赖关系。我做过对比测试:同一个项目,用 Ninja 构建比用 Make 快了将近 30%。
| 构建工具 | 并行策略 | 我的评价 |
|---|---|---|
| Make | 基于时间戳,粒度较粗 | 能用,但容易有冗余编译 |
| Ninja | 基于依赖图,粒度精细 | 推荐,尤其适合大项目 |
cmake -B build 时加上 -G Ninja 就行。
28.3 ccache 集成:让重复编译消失
并行构建解决了“同时跑”的问题,但没解决“重复跑”的问题。你想想看,你只是改了一个 .cpp 文件,结果整个项目都要重新编译?太傻了。
ccache 就是干这个的。它缓存了编译器的输出。如果源文件没变,它就直接从缓存里拿结果,跳过真正的编译。
28.3.1 安装与基本配置
# Ubuntu/Debian
sudo apt install ccache
# macOS
brew install ccache
# 查看缓存状态
ccache --show-stats
28.3.2 在 CMake 中启用 ccache
有两种方式。第一种是全局的,设置环境变量:
export CC="ccache gcc"
export CXX="ccache g++"
cmake -B build
第二种是 CMake 3.4 以后推荐的,使用 CMAKE_<LANG>_COMPILER_LAUNCHER:
# 在 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()
或者更简洁的写法:
cmake -B build -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
/usr/bin/g++,有时候用 /usr/local/bin/g++。ccache 认为这是不同的编译器,所以不命中。解决方案:统一编译器路径,或者在 ccache 配置里设置 CCACHE_NOHASHDIR=true。
28.3.3 缓存策略与最佳实践
ccache 默认缓存大小是 5GB。对于大项目,这远远不够。我建议调大一些:
# 设置缓存大小为 20GB
ccache --max-size=20G
# 设置缓存目录(默认在 ~/.ccache)
export CCACHE_DIR=/path/to/your/ccache
还有几个实用的环境变量:
CCACHE_SLOPPINESS=pch_defines,time_macros:放宽一些检查,提高命中率CCACHE_BASEDIR=/path/to/project:让缓存跟绝对路径解耦,方便 CI 环境复用CCACHE_COMPRESS=1:压缩缓存,节省磁盘空间
28.4 知识体系总览
下面这张图,我把这一章的核心逻辑串起来了。你可以把它当作一个快速参考。
嗯,这一章的内容就这些。调试、并行、缓存,这三个工具用好了,你的 CMake 构建体验会提升一大截。我个人现在写 CMake 项目,第一件事就是配好 ccache,第二件事就是用 Ninja 生成器。剩下的,就是享受飞一般的构建速度了。
--trace,加速用 -j,去重用 ccache。三者配合,天下无敌。
公众号:蓝海资料掘金营,微信 deep3321