第 16 章 · 编译加速、Hot Reload 与 Live Coding
Unreal 项目动辄百万行 C++,完整编译一次可能要十几分钟甚至更久。引擎和 UBT 用了几种策略把编译时间压下去:Unity Build(合并编译单元)、预编译头(PCH)、以及可选的Include What You Use(IWYU)。
另一方面,改几行代码就要重新编译、重启编辑器、重新加载关卡,迭代会非常慢。所以 Unreal 又提供了Hot Reload和Live Coding——在不重启进程的情况下替换已加载的 C++ 代码。
本章讲这三件事:编译加速的原理与代价、Hot Reload 的机制与约束、Live Coding 的替代方案。
16.1 Unity Build:合并编译单元
为什么慢
标准 C++ 的编译模型是"一个 .cpp 一个编译单元"。每个 .cpp 独立编译,会:
- 重复解析它 include 的所有头文件(可能几十上百个);
- 产生大量 .obj 文件,链接器要逐个读入并解析符号。
头文件被大量重复解析是 C++ 编译慢的主因之一。改一个被广泛 include 的头文件,会触发一大批 .cpp 重编。
Unity Build 做了什么
Unity Build 把多个 .cpp合并成一个大的 .cpp再编译。例如:
MyModule 原本有: A.cpp, B.cpp, C.cpp, ... , Z.cpp (几十个文件) Unity Build 后变成: MyModule.cpp (里面是 #include "Private/A.cpp" 等,把多个源文件当头文件一样包含进来)这样:
- 头文件只在合并后的"大 .cpp"里解析一次(在同一个编译单元内,头文件只需解析一遍);
- 产生的 .obj 数量大幅减少,链接次数减少;
- 但任何一个"子 .cpp"的改动都会导致整个 Unity 文件重编,所以增量编译时,改一个文件可能触发整个模块重编。
引擎通常会在"编译时间"和"增量编译时间"之间做权衡,例如每个模块只合并成几个 Unity 文件,而不是一个巨大的文件。
代价与陷阱
当多个"子 .cpp"被包含进同一个编译单元时,它们共享同一个翻译单元作用域,会带来:
1) 匿名命名空间中的符号冲突
// A.cppnamespace{intHelper(){return1;}}// B.cppnamespace{intHelper(){return2;}// 与 A.cpp 的 Helper 冲突!}在单独编译时,两个匿名命名空间彼此不可见。合并后它们在同一个编译单元,两个Helper冲突。解决:给匿名命名空间里的符号起唯一名字,或改成 static 函数并确保名字不同。
2) static 变量同名
// A.cppstaticints_Counter=0;// B.cppstaticints_Counter=0;// 合并后重复定义解决:用更具体的名字(如s_ACounter、s_BCounter),或放在匿名命名空间里并保证名字唯一。
3) using namespace 污染
某个 .cpp 里写了using namespace std;,在单独编译时只影响该文件。合并后会影响整个 Unity 里的所有"子 .cpp"。解决:避免在头文件里 using namespace;在 .cpp 里尽量在局部使用,或不用。
如何为单个文件禁用 Unity Build
如果某个 .cpp 必须单独编译(例如上面冲突无法轻易解决),可以在 .Build.cs 里排除它:
// 在 ModuleRules 中bUseUnityBuild=true;// 默认// 若要排除某个文件,通常通过命名约定或 Unreal 的 list 文件配置,具体查当前版本文档不同版本引擎的配置方式可能略有不同,有的通过特殊文件名(如*.NoUnity.cpp)排除,有的在 .Build.cs 里用 API 指定。
16.2 预编译头(PCH)
预编译头的思路是:把一坨常用的、几乎每个 .cpp 都会 include 的头文件预先编译成二进制形式(PCH),后续每个 .cpp 编译时先加载这份 PCH,再只编译自己特有的部分。
Unreal 里常见的是每个模块有一个 PCH,例如MyModule.cpp作为 PCH 的"宿主"(先被编译,生成 PCH),里面 include 了CoreMinimal.h、本模块的 Public 头文件等。其他 .cpp 第一行是:
#include"MyModule.h"// 或引擎规定的 PCH 头文件这样这些 .cpp 不需要重复解析 Engine、Core、CoreUObject 等一大串头文件,编译会快很多。
代价:PCH 本身要维护。往 PCH 里加一个很少用到的头文件,会让所有用这个 PCH 的 .cpp 都变慢一点。所以 PCH 里只放"几乎所有 .cpp 都需要"的头文件。
16.3 Include What You Use(IWYU)
传统 Unreal 风格是每个 .cpp 先 include 一个巨大的 PCH(或 CoreMinimal.h),再 include 自己需要的少量头文件。这样简单,但 PCH 会很大,任何 PCH 的变动都会影响很多文件。
IWYU的思路是:每个 .cpp 只 include真正用到的头文件,不依赖一个巨大的公共 PCH。这样:
- 改一个头文件时,只有真正 include 它的 .cpp 需要重编;
- 增量编译更细粒度,编译时间更可控。
迁移到 IWYU 需要:
- 清理每个 .cpp 的 include,去掉未使用的,补上直接依赖的(而不是依赖"别的头文件帮我 include 了");
- 可能调整 PCH 策略(例如不用模块级大 PCH,或缩小 PCH 内容)。
引擎和部分项目已经支持或部分采用了 IWYU,具体可查当前版本的构建选项。
16.4 Hot Reload
工作原理(概念)
Hot Reload 的大致流程是:
- 你修改了某个模块的 C++ 代码并保存;
- 编辑器(或已运行的游戏)检测到变更,触发该模块的重新编译;
- 编译得到新的 dll;
- 运行时加载新 dll,用新代码替换旧代码(通过替换函数指针、虚表等);
- 已有的 UObject 实例、CDO 等需要与"新类"对齐——例如 CDO 会按新类重新生成或更新,蓝图等引用要重新绑定到新的 UClass。
这样你可以改逻辑、改属性默认值等,而不必关掉编辑器、重新启动、重新打开关卡。
对代码编写的约束
Hot Reload 不是"任意 C++ 改动都能热更"。它有很多限制:
构造函数
- 构造函数在 Hot Reload 时可能被重新执行(用于更新 CDO)。所以构造函数里不要做"只做一次"的全局状态修改(如注册到单例、写文件),否则 Hot Reload 一次就执行一次。
- 避免在构造函数里依赖尚未初始化的全局状态(如 GEngine、GWorld),因为 Hot Reload 时这些可能处于不一致状态。
结构变更
- 增加/删除 UPROPERTY、改变继承关系等,可能无法正确热更,或导致旧实例与新的 UClass 不匹配。引擎会尽力更新,但复杂变更仍可能要求重启。
静态变量
- 模块内的静态变量在 dll 被"替换"后,新旧 dll 的静态变量可能并存或行为异常。依赖静态变量的代码在 Hot Reload 后可能出错。
蓝图与 CDO
- Hot Reload 会尝试更新 CDO 和蓝图对 C++ 类的引用。多数简单改动没问题,但若类结构变化大,可能出现蓝图报错或需要重新保存蓝图。
因此,开发时若发现 Hot Reload 后行为异常,首先尝试完全重启编辑器和游戏再验证,以排除热更带来的状态不一致。
16.5 Live Coding
Live Coding是 Unreal 提供的另一种"不重启进程就应用 C++ 修改"的机制,通常与 Hot Reload 二选一使用。
与 Hot Reload 的差异
- Hot Reload:重新编译整个模块的 dll,然后替换进程中已加载的 dll,并做 CDO/蓝图等更新。
- Live Coding:更偏向"补丁"式更新——只重新编译改动的函数或类,把新机器码"打"进正在运行的进程,替换对应函数的入口地址,而不替换整个 dll。
Live Coding 的优点是理论上更快(只编译改动的部分),缺点是:
- 对"结构级"变更支持更差:例如新增 UPROPERTY、改虚函数表布局,往往无法通过 Live Coding 应用,需要完整编译或 Hot Reload。
- 依赖编译器和链接器对"增量/补丁"的支持,不同平台和配置下稳定性可能不同。
使用建议
- 小改动(改函数实现、改默认值、加日志):可优先尝试 Live Coding 或 Hot Reload,看谁更稳定。
- 大改动(改类布局、增删 UPROPERTY/UFUNCTION、改继承):直接完整编译并重启更稳妥。
- 若 Hot Reload / Live Coding 后出现诡异 bug,先关掉热更,完整编译并重启再复现,以确认不是热更导致的状态不一致。
16.6 小结:编译与迭代的取舍
| 手段 | 目的 | 代价/注意 |
|---|---|---|
| Unity Build | 减少重复解析头文件、减少 .obj 数量 | 匿名/static 冲突、增量时可能整模块重编 |
| PCH | 一次解析常用头文件,多文件复用 | PCH 不宜过大、不宜乱加头文件 |
| IWYU | 细粒度依赖,改善增量编译 | 需要维护精确的 include |
| Hot Reload | 改代码不重启进程 | 构造函数/静态/结构变更有限制 |
| Live Coding | 更快地应用小改动 | 结构变更支持差,平台差异 |
理解这些机制,有助于在"编译时间"和"迭代体验"之间做合理预期,并在遇到编译或热更问题时知道该从哪排查。
一句话总结
Unreal 用 Unity Build 和 PCH 降低编译时间,用可选的 IWYU 改善增量编译;用 Hot Reload 或 Live Coding 在运行时应用 C++ 修改,但二者都对构造函数、静态变量和类结构变更有约束,复杂改动仍需完整编译并重启。