29、性能优化:通过 Presets 配置并行构建、ccache、LTO 等

说实话,很多团队的项目越做越大,编译一次能喝三杯咖啡。我见过不少开发者,每天把大量时间浪费在等编译上。其实,CMake Presets 里藏着不少提速的妙招。今天我们就聊聊怎么通过 Presets 把并行构建、ccache 缓存、LTO 链接优化这些东西安排得明明白白。

并行构建:别让 CPU 闲着

并行构建,说白了就是让电脑多核同时干活。你想想看,现在谁电脑不是 8 核 16 核的?只用一个核编译,那简直是暴殄天物。

在 Presets 里,我们可以通过 jobs 字段来控制并行度。我个人习惯设成 0,意思是让 Ninja 或 Make 自己判断最优并行数。但有些场景下,比如你在服务器上编译,同时跑着好几个任务,那就得手动限制一下。

{
  "configurePresets": [
    {
      "name": "default",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "release",
      "configurePreset": "default",
      "jobs": 0
    },
    {
      "name": "release-limited",
      "configurePreset": "default",
      "jobs": 4
    }
  ]
}
小提示:Ninja 比 Make 更擅长并行调度。如果你还在用 Make,我建议试试 Ninja,编译速度能快不少。我在一个嵌入式项目里换用 Ninja 后,全量编译时间从 12 分钟降到了 7 分钟。

ccache:让重复编译见鬼去

ccache 是个好东西。它会把编译过的 .o 文件缓存起来。下次再编译时,如果源文件没变,就直接从缓存里拿结果。嗯,这里要注意:ccache 不是万能的,它只对 C/C++/Objective-C 有效。

在 Presets 里启用 ccache 很简单,设置 CMAKE_C_COMPILER_LAUNCHERCMAKE_CXX_COMPILER_LAUNCHER 就行。

{
  "configurePresets": [
    {
      "name": "with-ccache",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_C_COMPILER_LAUNCHER": "ccache",
        "CMAKE_CXX_COMPILER_LAUNCHER": "ccache"
      }
    }
  ]
}
避坑指南:我曾经在一个项目里发现 ccache 命中率特别低,排查了半天,原来是编译器路径不一致导致的。比如你有时候用 /usr/bin/gcc,有时候用 /usr/local/bin/gcc,ccache 会认为这是不同的编译器,缓存就失效了。解决办法:统一编译器路径,或者在 ccache 配置里设置 CCACHE_NOHASHDIR=true

你还可以通过环境变量控制 ccache 的行为。比如设置缓存大小、日志级别等。我个人习惯把缓存上限设成 5GB,太小了容易频繁淘汰,太大了浪费磁盘。

{
  "configurePresets": [
    {
      "name": "with-ccache-tuned",
      "inherits": "default",
      "environment": {
        "CCACHE_MAXSIZE": "5G",
        "CCACHE_LOGFILE": "${sourceDir}/ccache.log"
      },
      "cacheVariables": {
        "CMAKE_C_COMPILER_LAUNCHER": "ccache",
        "CMAKE_CXX_COMPILER_LAUNCHER": "ccache"
      }
    }
  ]
}

LTO:链接时优化,让代码跑得更快

LTO(Link Time Optimization)是个好东西,但也是个双刃剑。它能在链接阶段做全局优化,比如内联跨编译单元的函数、消除死代码。代价是链接时间会变长,内存占用也会增加。

在 Presets 里启用 LTO,主要靠 CMAKE_INTERPROCEDURAL_OPTIMIZATION 这个变量。不同编译器对这个变量的支持程度不一样:

编译器 CMAKE_INTERPROCEDURAL_OPTIMIZATION 实际效果
GCC ON 启用 -flto
Clang ON 启用 -flto=thin 或 -flto=full
MSVC ON 启用 /GL 和 /LTCG
{
  "configurePresets": [
    {
      "name": "release-lto",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
      }
    }
  ]
}
重要:LTO 不是银弹。我建议只在 Release 版本里开启,Debug 版本就别折腾了。另外,如果你用了 ccache,记得检查 ccache 是否支持 LTO 缓存。ccache 4.0 以上版本对 LTO 的支持已经比较好了。

把这些组合起来:一个完整的性能优化 Preset

好了,我们把并行构建、ccache、LTO 都揉到一起,做一个完整的 Preset。我个人习惯把这种 Preset 命名为 release-optimized,一看就知道是干啥的。

{
  "configurePresets": [
    {
      "name": "release-optimized",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_C_COMPILER_LAUNCHER": "ccache",
        "CMAKE_CXX_COMPILER_LAUNCHER": "ccache",
        "CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
      },
      "environment": {
        "CCACHE_MAXSIZE": "5G",
        "CCACHE_LOGFILE": "${sourceDir}/ccache.log"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "release-optimized",
      "configurePreset": "release-optimized",
      "jobs": 0
    }
  ]
}

使用的时候,一行命令搞定:

cmake --preset release-optimized
cmake --build --preset release-optimized

知识体系一览

下面这张图帮你理清本章的核心逻辑:

CMake Presets 性能优化体系 并行构建 ccache 缓存 LTO 优化 jobs: 0 (自动) jobs: 4 (手动限制) 生成器: Ninja CMAKE_C_COMPILER_LAUNCHER CMAKE_CXX_COMPILER_LAUNCHER CCACHE_MAXSIZE: 5G CMAKE_INTERPROCEDURAL _OPTIMIZATION: ON 仅 Release 模式 release-optimized Preset

一些额外的建议

最后,分享几个我在实战中踩过的坑:

  • ccache 和 LTO 的兼容性:ccache 4.0 以下版本对 LTO 支持不好,缓存命中率会下降。升级到 4.0 以上基本没问题。
  • 并行数不是越大越好:我见过有人把 jobs 设成 64,结果内存爆了,编译直接 OOM。建议根据机器内存来,一般每个编译任务需要 1-2GB 内存。
  • LTO 的链接时间:如果你项目特别大,LTO 链接可能会很慢。可以考虑用 ThinLTO(Clang 支持),它比 Full LTO 快不少,优化效果也接近。
  • 团队共享:把这些 Preset 提交到 Git 仓库里,团队成员直接 cmake --preset release-optimized 就能用,不用每个人手动配环境变量。
我的习惯:我会在项目根目录放一个 CMakePresets.json,里面包含 debug、release、release-optimized 三个 Preset。debug 不开优化,release 开基本优化,release-optimized 才开 LTO 和 ccache。这样不同场景各取所需。

好了,关于性能优化就聊这么多。记住一句话:编译速度是开发效率的一部分,花点时间配置好 Presets,长期来看绝对值得。


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