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的其他目标也受益,可以用INTERFACEPUBLIC

一个小技巧:我习惯把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 本章知识体系

下面这张图,把本章的核心知识点串起来了。你可以看到,构建加速主要从“减少编译单元数量”和“减少每个单元的解析量”两个方向入手。

CMake构建加速核心手段 构建加速 减少编译单元数量 减少每个单元解析量 Unity Build(合并编译) Ninja + 并行任务控制 ccache / distcc 缓存 预编译头文件(PCH) 减少不必要的模板实例化 注:Unity Build和PCH可以同时使用,效果叠加

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这些工具,把编译时间砍掉一半以上,是完全能做到的。

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