26、性能优化:如何通过Unity Build、预编译头文件(PCH)等手段加速CMake项目的构建速度
说实话,C++项目的编译速度,一直是很多团队的痛点。我见过一个中等规模的项目,改一行代码,重新链接要等三分钟。三分钟啊,你想想看,一天下来能浪费多少时间?
这一章,我们就来聊聊怎么用CMake自带的手段,把构建速度提上去。核心就两招:Unity Build(合并编译)和预编译头文件(PCH)。另外还有一些辅助技巧,我也会一并讲清楚。
核心观点:构建速度优化的本质,是减少编译器重复解析头文件、重复实例化模板的开销。Unity Build减少编译单元数量,PCH减少每个编译单元的解析量。
26.1 Unity Build:把多个源文件“捏”在一起编译
Unity Build的原理很简单:把多个.cpp文件合并成一个.cpp文件来编译。这样编译器只需要启动一次,就能处理多个源文件,省掉了大量重复的头文件解析和模板实例化开销。
我在一个游戏引擎项目里用过这个技巧。原本全量编译需要40分钟,开启Unity Build后,直接降到12分钟。当然,代价是增量编译变慢了一些,但全量编译的场景下,收益非常明显。
26.1.1 手动Unity Build vs CMake内置支持
以前的做法是手动写一个“大.cpp”,把所有源文件#include进去。比如:
// unity_physics.cpp
#include "physics/rigid_body.cpp"
#include "physics/collision_detection.cpp"
#include "physics/constraint_solver.cpp"
但这种方式维护起来很痛苦。你加一个源文件,就得改这个“大.cpp”。
CMake 3.16开始,提供了原生的Unity Build支持。你只需要设置一个属性:
set_target_properties(my_target PROPERTIES
UNITY_BUILD ON
UNITY_BUILD_BATCH_SIZE 8
)
UNITY_BUILD_BATCH_SIZE控制每个“合并单元”里放多少个源文件。我个人习惯设成8-16,太小了效果不明显,太大了容易触发编译器的内存限制。
26.1.2 避坑指南:Unity Build的常见问题
我曾经踩过一个坑:两个源文件里定义了同名的全局变量或静态函数,合并编译时直接报“重定义”错误。这是因为Unity Build把原本隔离的编译单元合并了,命名空间冲突就暴露出来了。
解决办法:
- 尽量使用匿名命名空间(
namespace { ... })代替文件内静态变量 - 头文件中的inline函数要确保真的被inline了
- 对于无法兼容Unity Build的源文件,可以单独排除:
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/src/legacy_code.cpp
PROPERTIES UNITY_GROUP ""
)
把UNITY_GROUP设为空字符串,这个文件就不会参与合并编译。
26.2 预编译头文件(PCH):让常用头文件只编译一次
预编译头文件的思路更直接:把那些几乎每个源文件都会include的“大头”头文件,提前编译成二进制格式。编译器再遇到这些头文件时,直接加载预编译结果,不用重新解析。
哪些头文件适合放进PCH?
- STL头文件(
<vector>、<string>、<map>等) - 项目内部的基础库头文件(日志、内存管理、数学库等)
- 第三方库头文件(Boost、fmt、spdlog等)
26.2.1 CMake中配置PCH
CMake 3.16也提供了原生PCH支持:
target_precompile_headers(my_target PRIVATE
<vector>
<string>
<algorithm>
"project/base/logging.h"
"project/base/math_utils.h"
)
这里PRIVATE表示这些头文件只对my_target本身的源文件生效。如果你想让链接了my_target的其他目标也受益,可以用INTERFACE或PUBLIC。
一个小技巧:我习惯把PCH头文件单独放在一个pch.h里,然后让target_precompile_headers引用这个文件。这样方便统一管理,也方便其他目标复用。
// pch.h
#pragma once
#include <vector>
#include <string>
#include <map>
#include <memory>
#include <algorithm>
#include "project/base/logging.h"
#include "project/base/math_utils.h"
// CMakeLists.txt
target_precompile_headers(my_target PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/pch.h
)
26.2.2 PCH的注意事项
嗯,这里要注意:PCH不是银弹。用不好反而会拖慢构建速度。
- PCH文件本身很大:预编译后的.pch文件可能有几十甚至上百MB。磁盘IO慢的话,加载PCH的时间可能抵消掉节省的解析时间。
- 修改PCH会触发全量重编译:只要PCH里的任何一个头文件变了,所有依赖它的源文件都得重新编译。所以PCH里只放“几乎不变”的头文件。
- 不同编译器不兼容:GCC生成的.pch文件,Clang不能用。跨平台项目要分别配置。
26.3 其他加速构建的技巧
除了Unity Build和PCH,还有几个小技巧,我平时也会用。
26.3.1 使用Ninja代替Make
Ninja是一个更现代的构建系统,它的设计目标就是快。同样的项目,用Ninja构建比用Make快30%-50%。切换方式很简单:
cmake -B build -G Ninja
cmake --build build
我个人的经验是,Ninja在增量编译时优势尤其明显。因为它能更精确地计算依赖关系,避免不必要的重新编译。
26.3.2 控制并行编译任务数
默认情况下,cmake --build会使用所有CPU核心。但有时候,内存会成为瓶颈。尤其是Unity Build合并了大量源文件后,每个编译进程的内存占用会飙升。
你可以手动限制并行数:
cmake --build build -j 4 # 只用4个并行任务
或者通过CMake预设:
# CMakePresets.json
{
"name": "release",
"jobs": 8
}
26.3.3 使用ccache/distcc缓存编译结果
ccache是一个编译缓存工具。它会把每次编译的结果缓存起来,下次编译同样的源文件时,直接返回缓存结果。对于频繁切换分支、反复全量编译的场景,效果非常显著。
配置方式:
# 设置编译器包装器
export CC="ccache gcc"
export CXX="ccache g++"
# 或者在CMake中设置
set(CMAKE_C_COMPILER_LAUNCHER ccache)
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
distcc则是分布式编译,适合有多个编译节点的团队。不过配置起来稍微复杂一些,小团队用ccache就够了。
26.4 本章知识体系
下面这张图,把本章的核心知识点串起来了。你可以看到,构建加速主要从“减少编译单元数量”和“减少每个单元的解析量”两个方向入手。
26.5 综合配置示例
最后,给一个完整的CMakeLists.txt示例,把Unity Build、PCH、Ninja、ccache都整合在一起:
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)
# 启用ccache(如果可用)
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
message(STATUS "Using ccache: ${CCACHE_PROGRAM}")
endif()
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加可执行文件
add_executable(my_app src/main.cpp src/engine.cpp src/renderer.cpp)
# 启用Unity Build
set_target_properties(my_app PROPERTIES
UNITY_BUILD ON
UNITY_BUILD_BATCH_SIZE 8
)
# 配置PCH
target_precompile_headers(my_app PRIVATE
<vector>
<string>
<memory>
"project/base/logging.h"
)
# 排除不兼容Unity Build的文件
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/src/legacy_physics.cpp
PROPERTIES UNITY_GROUP ""
)
我的建议:不要一上来就全量开启所有优化。先只开PCH,看看编译时间有没有明显下降。如果效果不错,再叠加Unity Build。每次只改一个变量,这样你能清楚地知道每个优化手段到底带来了多少收益。
好了,这一章的内容就到这里。构建加速是个持续优化的过程,没有银弹。但只要你理解了Unity Build和PCH的原理,再配合Ninja和ccache这些工具,把编译时间砍掉一半以上,是完全能做到的。