性能优化:并行下载、增量更新、缓存策略、减少网络请求

说实话,刚开始用 FetchContent 的时候,我根本没想过性能这回事。项目小嘛,几个依赖一拉就完事了。直到有一次,团队的项目膨胀到十几个依赖,每次 cmake --build 都要等上五六分钟去下载源码……嗯,那感觉就像在等老电脑开机。

后来我花了不少时间研究怎么让 FetchContent 跑得更快。说白了,就四个方向:并行下载、增量更新、缓存策略、减少网络请求。今天我把这些经验整理出来,希望能帮你少走弯路。

并行下载:别让依赖排队等

FetchContent 默认是串行下载的。一个依赖下载完,才开始下一个。你想想看,如果三个依赖分别需要 10 秒、8 秒、12 秒,串行就是 30 秒。但如果并行呢?理论上只需要 12 秒。

怎么开启并行?其实很简单,设置 FETCHCONTENT_QUIETCMAKE_BUILD_PARALLEL_LEVEL 就行。但我个人习惯用另一种方式——直接修改 CMake 的下载命令。

# 在调用 FetchContent 之前设置
set(FETCHCONTENT_QUIET OFF)
set(CMAKE_BUILD_PARALLEL_LEVEL 4)

# 或者更直接:修改下载命令模板
set(CMAKE_GET_DOWNLOAD_COMMAND
  "${CMAKE_COMMAND} -P \"${CMAKE_CURRENT_BINARY_DIR}/download.cmake\" --parallel 4"
)
我的经验:并行数不要超过 CPU 核心数的两倍。我曾经设成 16,结果网络带宽被占满,反而更慢了。一般 4 到 8 就够用。

增量更新:只下载变化的部分

增量更新这个功能,说实话,CMake 原生支持得不太好。FetchContent 每次都会重新下载整个仓库。但我们可以用一些小技巧来模拟增量更新。

我记得有一次,项目依赖了一个频繁更新的库,每次改一行代码就要重新下载整个 50MB 的仓库。后来我改用 Git 子模块 + 本地缓存的方式,才解决了这个问题。

这里分享一个我常用的方案:

# 先检查本地缓存
if(EXISTS "${CMAKE_SOURCE_DIR}/.cache/${dep_NAME}")
  message(STATUS "使用本地缓存: ${dep_NAME}")
  FetchContent_Declare(
    ${dep_NAME}
    SOURCE_DIR "${CMAKE_SOURCE_DIR}/.cache/${dep_NAME}"
  )
else()
  # 正常下载
  FetchContent_Declare(
    ${dep_NAME}
    GIT_REPOSITORY https://github.com/xxx/xxx.git
    GIT_TAG v1.0.0
  )
endif()
注意:这种方法需要手动管理缓存目录。如果依赖版本更新了,记得清理缓存。我一般会在 CMakeLists.txt 里加一个 --clean-cache 选项。

缓存策略:把下载过的依赖存起来

缓存是性能优化的重头戏。FetchContent 本身有缓存机制,但默认只缓存到构建目录。一旦你删了 build 文件夹,一切都要重新下载。

我建议把缓存放到一个全局位置,比如 ~/.cmake_cache。这样多个项目可以共享缓存,而且不会因为清理构建目录而丢失。

# 在 ~/.cmake/init.cmake 或 CMakeLists.txt 开头设置
set(ENV{FETCHCONTENT_BASE_DIR} "$ENV{HOME}/.cmake_cache/fetchcontent")

# 或者指定本地缓存路径
set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/.cache/fetchcontent")

这样做的好处很明显:

  • 第一次下载后,后续构建直接使用缓存
  • 多个项目共用同一个依赖时,只需下载一次
  • 即使删除 build 目录,缓存依然存在
核心思路:把缓存从构建目录中分离出来,让它成为项目级别的持久化存储。

减少网络请求:能省则省

网络请求是性能瓶颈的重灾区。每次请求都有延迟,哪怕只是检查版本更新。我见过有些项目每次构建都去 GitHub 检查 tag 列表,白白浪费好几秒。

减少网络请求的几个实用技巧:

  1. 固定版本号:不要用 mainlatest,指定具体的 tag 或 commit hash。
  2. 使用本地镜像:如果公司有 Git 镜像,优先用内网地址。
  3. 关闭更新检查:设置 FETCHCONTENT_UPDATES_DISCONNECTEDON
# 关闭所有更新检查
set(FETCHCONTENT_UPDATES_DISCONNECTED ON)

# 或者针对单个依赖关闭
FetchContent_Declare(
  my_dep
  GIT_REPOSITORY https://github.com/xxx/xxx.git
  GIT_TAG v1.2.3
  GIT_SHALLOW TRUE  # 只拉取最新 commit,减少数据量
  GIT_PROGRESS TRUE
)
避坑指南:我曾经把 GIT_SHALLOW 设为 TRUE,结果发现有些依赖需要完整的 git 历史来做版本比较。所以,如果你的依赖需要 git describe 之类的操作,别用 shallow clone。

整体优化流程图

下面这张图总结了 FetchContent 性能优化的整体流程。从下载到缓存,每一步都有对应的优化策略。

FetchContent 性能优化流程 并行下载 多线程同时拉取 增量更新 只下载变化部分 缓存策略 持久化存储依赖 减少网络请求 固定版本+镜像 设置并行数 修改下载命令 控制带宽占用 本地缓存目录 Git 子模块 增量拉取脚本 全局缓存路径 项目级缓存 缓存清理策略 固定版本号 内网镜像 关闭更新检查 构建时间减少 50% ~ 80% 四种策略协同工作,从下载到缓存全链路优化 实际效果取决于网络环境、依赖数量和项目规模

综合配置示例

最后,我把上面所有的优化策略整合到一个完整的配置里。你可以直接复制到项目中使用。

# 性能优化配置
# 放在 CMakeLists.txt 最前面

# 1. 缓存策略:使用项目级缓存
set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/.cache/fetchcontent")

# 2. 减少网络请求:关闭更新检查
set(FETCHCONTENT_UPDATES_DISCONNECTED ON)

# 3. 并行下载:设置并行数
set(CMAKE_BUILD_PARALLEL_LEVEL 4)

# 4. 使用 shallow clone 减少数据量
set(FETCHCONTENT_QUIET OFF)

# 声明依赖时指定具体版本
FetchContent_Declare(
  json
  GIT_REPOSITORY https://github.com/nlohmann/json.git
  GIT_TAG v3.11.2
  GIT_SHALLOW TRUE
)

FetchContent_Declare(
  fmt
  GIT_REPOSITORY https://github.com/fmtlib/fmt.git
  GIT_TAG 10.1.0
  GIT_SHALLOW TRUE
)

FetchContent_MakeAvailable(json fmt)
总结一下:性能优化不是一蹴而就的事。我建议你先从缓存策略入手,把 FETCHCONTENT_BASE_DIR 设置好,就能看到立竿见影的效果。然后再逐步加上并行下载和增量更新。至于减少网络请求,那是锦上添花的事,但做了一定不亏。

嗯,这些就是我这些年折腾 FetchContent 性能优化的一些心得。希望能帮你把构建时间降下来,把摸鱼时间提上去。


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