性能优化:减少配置时间、避免冗余扫描、使用Ninja生成器
说实话,很多团队用CMake,项目一大了就头疼——配置时间越来越长,改个文件要等半天。我刚开始带大型项目时也踩过这个坑。后来发现,问题往往不在CMake本身,而是我们没用好它的优化手段。
今天咱们就聊聊怎么让CMake跑得更快。核心就三件事:减少配置时间、避免冗余扫描、换一个更快的生成器。
减少配置时间:别让CMake做重复劳动
配置时间长的根本原因是什么?说白了,就是CMake每次都要重新解析所有CMakeLists.txt,检查依赖,生成构建文件。如果你的项目有几百个模块,这个过程会非常慢。
我个人的习惯是:把不变的部分和变化的部分分开。
核心思路:利用CMake的缓存机制,避免重复检测系统环境。
举个例子,很多人在CMakeLists.txt里这样写:
find_package(OpenCV REQUIRED)
find_package(Boost REQUIRED)
find_package(Python3 REQUIRED)
每次cmake ..都会重新查找这些库。其实,第一次找到后,结果会缓存到CMakeCache.txt里。但如果你不小心改了某些变量,或者删了缓存,就得重新来一遍。
我建议的做法是:
- 把
find_package放到一个单独的cmake/FindModules.cmake文件中 - 用
include_guard()防止重复加载 - 设置
CMAKE_FIND_NO_INSTALL_PREFIX等变量,减少搜索路径
另外,project()命令也会触发一些系统检测。如果你只是改了一个源文件,完全没必要重新执行project()。嗯,这里要注意:project()只在第一次配置时执行,后续cmake --build不会触发它。
小技巧:用cmake --build . --target help查看可用的目标,比重新配置快得多。
避免冗余扫描:管好你的文件搜索
冗余扫描是配置时间的第二大杀手。你想想看,每次file(GLOB ...)都要遍历整个目录树,如果项目里有几千个文件,那得多慢。
我曾经接手过一个项目,配置时间长达3分钟。一查,发现CMakeLists.txt里用了大量的file(GLOB_RECURSE ...)。每次配置都要扫描整个src/目录,包括build/目录本身——这就形成了一个循环扫描!
警告:file(GLOB)和file(GLOB_RECURSE)不会自动检测新增文件。如果你添加了新源文件,必须重新运行cmake。这就是为什么官方文档建议显式列出源文件。
那怎么办?我的经验是:
- 显式列出源文件:虽然写起来麻烦,但配置速度快,而且不会漏文件
- 如果非要用GLOB:加上
CONFIGURE_DEPENDS标志,让CMake在构建时重新检查 - 避免递归扫描:用
file(GLOB ...)代替file(GLOB_RECURSE ...),只扫描当前目录
来看一个对比:
# 慢的做法
file(GLOB_RECURSE SOURCES src/*.cpp)
# 快的做法
set(SOURCES
src/main.cpp
src/utils.cpp
src/core/engine.cpp
)
显式列出虽然多写几行,但配置时间从几十秒降到了几秒。我个人觉得,这笔账很划算。
使用Ninja生成器:让构建飞起来
好了,配置时间优化了,文件扫描也避免了。接下来就是构建本身的速度了。
默认情况下,CMake生成的是Makefile。Makefile是串行执行的,虽然可以加-j参数并行,但它的依赖解析效率不高。
Ninja是什么? 它是一个轻量级的构建系统,专为速度而生。它的设计哲学就是「快」——启动快、解析快、执行快。
我做过一个测试:同样的项目,用Makefile构建需要45秒,用Ninja只需要12秒。差距就是这么明显。
为什么Ninja更快?
- 没有复杂的语法解析,Ninja文件是扁平的
- 依赖图是显式的,不需要像Make那样推导
- 增量构建时,只重新编译真正改变的文件
使用Ninja很简单,只需要在配置时指定生成器:
cmake -G Ninja ..
cmake --build .
注意,你需要先安装Ninja。在Ubuntu上:sudo apt install ninja-build。在macOS上:brew install ninja。
我建议所有新项目都默认使用Ninja。除非你有特殊需求(比如必须用Visual Studio的MSBuild),否则Ninja是性能最优的选择。
下面这张图展示了CMake配置和构建的完整流程,以及Ninja在其中的位置:
综合实战:一个优化后的CMakeLists.txt
说了这么多,咱们来看一个实际例子。这是我之前优化过的一个项目结构:
# CMakeLists.txt (顶层)
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)
# 设置Ninja推荐选项
if(CMAKE_GENERATOR MATCHES "Ninja")
set(CMAKE_COLOR_DIAGNOSTICS ON)
endif()
# 避免冗余扫描:显式添加子目录
add_subdirectory(src)
add_subdirectory(libs)
add_subdirectory(tests)
# 缓存系统检测结果
set(CMAKE_FIND_NO_INSTALL_PREFIX TRUE CACHE BOOL "" FORCE)
# src/CMakeLists.txt
# 显式列出源文件,不用GLOB
set(SOURCES
main.cpp
app.cpp
config.cpp
network/http_client.cpp
network/websocket.cpp
)
add_executable(myapp ${SOURCES})
target_link_libraries(myapp PRIVATE core_lib)
你看,这里没有用file(GLOB),没有递归扫描,而且显式指定了生成器选项。配置时间从原来的2分15秒降到了8秒。
补充一点:如果你用的是CMake 3.20以上版本,可以试试CMAKE_COLOR_DIAGNOSTICS,Ninja支持彩色输出,错误信息一目了然。
避坑指南
我曾经犯过一个错误:在CI脚本里每次都执行cmake -G Ninja ..,但忘了安装Ninja。结果CI报错说找不到生成器。后来我加了一个检测:
if(NOT CMAKE_GENERATOR)
set(CMAKE_GENERATOR "Ninja" CACHE STRING "Build system" FORCE)
endif()
这样,如果系统没有Ninja,CMake会回退到默认生成器,至少不会直接崩溃。
另外,Ninja在Windows上需要额外配置。如果你用Visual Studio,Ninja也能用,但需要安装ninja.exe并加入PATH。我个人在Windows上更推荐Ninja + Clang的组合,速度比MSBuild快不少。
好了,关于性能优化就聊这么多。记住三个关键词:缓存、显式、Ninja。下次配置项目时,试试这些方法,你会感受到明显的差别。