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 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到,四个优化方向分别作用于编译流程的不同阶段:

CMake 编译性能优化 · 知识体系 编译性能优化 减少重复配置 CMakePresets.json 缓存变量固化 ccache 缓存 缓存 .o 文件 增量编译秒过 并行编译 -j $(nproc) CPU 满负荷 Unity Build 合并编译单元 减少头文件解析 Release 全开 · Debug 只开 ccache + 并行

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里「投入产出比」最高的几个设置之一了。


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