中EN
概念页

构建配置与编译优化

Build Configurations & Compiler Optimization

更新于 2026-10-12 约 10 分钟构建编译器链接器优化调试性能分析GEA

C++ 游戏代码从源文件到可执行文件要经过预处理、编译、链接。游戏项目通常不止 Debug/Release 两种构建配置 (build configuration),而是 Debug、Development、Ship,有时再加混合构建;配置越多测试越难。编译器优化分为局部优化和全局优化(链接时优化 LTO、剖析引导优化 PGO),它们让程序更快,但也让调试器“说谎”。所以要学会调试优化过的构建,并用统计式或插桩式性能分析器 (profiler) 找出最耗时的代码。

翻译单元、目标文件与链接(卷 I 2.2.1–2.2.2)

  • 翻译单元 (translation unit):编译器一次翻译一个源文件(.cpp 等)。头文件并不直接交给编译器,而是由预处理器把 #include 替换成头文件的内容,所以编译器看到的只有翻译单元。
  • 目标文件(.obj / .o)里的机器码有两个特点:可重定位(还没有确定地址)和未链接(引用的外部函数和全局数据还没解析)。库只是若干目标文件的归档,类似 ZIP。
  • 链接器有两项工作:算出所有机器码的最终相对地址,并解析各翻译单元之间的外部引用。可执行文件仍然是可重定位的,绝对基址要到载入内存时才确定。
  • 动态链接库 (DLL) 介于库和可执行文件之间:像库一样可以被多个程序调用,又像可执行文件一样能被操作系统独立载入,带有启动和关闭代码。使用 DLL 的程序中,指向 DLL 的引用保持未链接,运行时由操作系统载入 DLL 并修补地址,因此 DLL 可以单独更新。
  • Visual Studio 中,项目 (project)(.vcxproj,XML 格式)产出一个库、可执行文件或 DLL;解决方案 (solution)(.sln)管理多个项目。

构建配置(卷 I 2.2.4)

构建配置就是绑定在项目上的一组预处理器、编译器、链接器选项,免得每次都手写一长串命令行(例如 cl /c foo.cpp /Fo foo.obj /Wall /Od /Zi,或 Clang 的 -O0 -g)。主要选项有三类:

类别典型选项说明
预处理器-D / /D 定义宏Debug 构建定义 _DEBUG,非 Debug 构建定义 NDEBUG,代码据此进行条件编译;编译器还会自动定义 __cplusplus、_MSC_VER、__GNUC__、_WIN32 等宏,方便写跨平台代码
编译器调试信息、内联展开、优化级别调试信息会让可执行文件变大,也方便别人逆向,所以发行版总会剥离;关闭内联后每个内联函数在内存中只有一份,便于跟踪;优化会重排语句、删除或移动变量、复用寄存器,使调试器难以如实显示
链接器输出类型、外部库与搜索路径、栈大小、基址、目标机器、是否开启全局优化常见做法是 Debug 构建链接调试版库,非 Debug 构建链接优化版库;VS 还能用源码中的特殊 #pragma 让链接器自动链接某个库,所以“附加依赖项”里未必列出全部实际链接的库(DirectX 程序就是例子)

作者建议不要给单个翻译单元单独覆盖项目设置,因为之后很难分清哪些 .cpp 有特殊设置。在 VS 属性页中尽量选“所有配置”统一编辑,只把确实需要不同的设置(如内联、优化)分开;包含目录尽量写成相对路径或用 $(OutDir)、$(IntDir)、$(ConfigurationName) 等宏表示,并把中间文件和最终产物放在不同目录(卷 I 2.2.4.4)。

局部优化与全局优化(卷 I 2.2.4.2)

除局部和全局两大类之外还有别的优化,例如窥孔优化 (peep-hole optimization),让优化器做平台或 CPU 相关的优化。

局部优化只作用于基本块 (basic block),粗略地说就是一段不含分支的指令。包括:代数化简、运算强度削减(如把 x / 2 换成 x >> 1)、内联、常量折叠、常量传播、循环展开、死代码消除、指令重排(减少流水线停顿)。

全局优化考虑整个控制流图,例如公共子表达式消除。理想情况下它跨越翻译单元,所以由链接器完成,称为链接时优化 (LTO)。LLVM/Clang 等还支持剖析引导优化 (PGO):用之前运行时采集的剖析数据,反复优化最关键的路径。代价是 LTO 会大大延长链接时间,PGO 则需要反复运行程序(由 QA 或自动化测试驱动)来生成剖析数据。gcc、Clang、Visual C++ 的优化级别都从 -O0(不优化)到 -O3(全部开启)。

游戏常用的构建配置(卷 I 2.2.4.3)

作者主张少用含义模糊的“release build”,只区分 debug build(局部和全局优化都关闭)与 non-debug build(开启局部和/或全局优化)。下图把常见配置排成一条从易调试到高性能的光谱:

flowchart LR
  D["Debug:关闭全部优化与内联,带完整调试信息"] --> H["Hybrid:大部分代码按 Development 编译,少数翻译单元按 Debug 编译"]
  H --> V["Development:开启大部分局部优化,保留调试信息和断言"]
  V --> S["Ship:剥离调试信息,去掉大部分断言,开启 LTO/PGO 等全部优化"]

越往右越快、越难调试。

  • 混合构建 (Hybrid):只把正在排查的那部分代码按 Debug 编译,其余部分以接近全速运行。用 make 很容易实现:定义一个 $HYBRID_SOURCES 变量列出要按 Debug 编译的文件,两套目标文件放进不同目录,链接时按列表挑选。VS 的配置以项目为单位,较难做到;可以用脚本生成 .vcxproj,或者在解决方案级按项目(即按库)混合。
  • 可测试性:配置越多越难测,因为某个严重 bug 可能只出现在其中一种配置里。一般不正式测试 Debug 构建;但如果平时只测 Development,就不能在 Gold Master 前一晚才做 Ship 构建,指望它的 bug 情况和 Development 一样。整个 alpha 和 beta 阶段,Development 和 Ship 都要同样充分地测试。所以有些工作室干脆不设单独的 Ship 配置,测好 Development 后剥掉调试信息就直接发行。

调试技巧(卷 I 2.2.5)

  • 基础操作:启动项目、断点(F9)、单步(F10 跳过函数,F11 进入函数)、调用栈窗口、监视窗口。监视窗口能求任意 C/C++ 表达式、做类型转换,甚至调用程序里的函数(如 quatToAngleDeg();注意监视表达式在每次命中断点或单步时都会自动重新求值,所以这个函数每次都会被调用);后缀 ,d 显示十进制、,x 显示十六进制、,! 绕过 Natvis 显示原始格式、,n 把指针当作 n 个元素的数组,也可写成 my_array,[my_array_count]。展开特别大的数据结构可能让调试器慢到无法使用。
  • 数据断点 (data breakpoint),也叫硬件断点:利用 CPU 在指定地址被写入时触发中断的能力。例如可以监视 &object.m_angle,看是谁写进了 0。
  • 条件断点:例如用 (uintptr_t)this == 0x12345678 只在某个实例上停下;也可以设置命中次数(比如只看第 376 次循环)。代价是每次命中都要计算条件表达式,会拖慢调试器和游戏。
  • 只在非 Debug 构建出现的 bug,常见原因有:未初始化变量(Debug 模式下常被清零)、重要代码被误写进断言、数据结构大小或成员打包随配置变化、内联或优化触发的问题,少数情况是优化器本身的 bug。调试优化后的代码可以:在反汇编模式下单步;从寄存器推断变量的值或地址;把已知地址强制转换成类型来查看,如 (Foo*)0x1378A0C0;借助静态或全局变量(如全局 PhysicsWorld)找到内部对象;修改代码,加打印、加全局变量、加检测条件。

性能分析器与内存工具(卷 I 2.3–2.4)

帕累托原则(80/20 法则) 用在优化上的意思是:大约 80% 的运行时间花在约 20% 的代码上,所以先用性能分析器找到这 20%。函数耗时多有两种原因:单次执行慢,或者调用次数多,因此调用次数和调用图(父函数/子函数及各自的耗时占比)都很重要。

类别原理特点书中示例
统计式 (statistical)周期性采样程序计数器,按样本数估算各函数的时间占比几乎不影响运行速度Intel VTune
插桩式 (instrumenting)在每个函数插入序言和尾声代码,调用剖析库记录调用栈、调用者和次数,甚至可以逐行计时极为精确全面,但会让游戏慢到几乎无法游玩IBM Rational Quantify
混合式低开销采样,同时在每个样本上分析调用栈能得到调用者分布Microsoft LOP;PS5 上的 Razor CPU 两种方式都支持,Razor GPU 用于着色器和计算任务

内存泄漏是分配了却从不释放;内存破坏是写到了错误的地址,覆盖了原有数据,同时该写的地方却没写。两者都源于指针。书中列出的检测工具有 IBM Rational Purify(插桩所有解引用和分配/释放操作,实时报告问题,退出时输出泄漏报告)、Parasoft Insure++、Valgrind。其他常用工具还有 diff 工具、三方合并工具(合并两组 diff,涉及原始版本和两人各自的版本)、十六进制编辑器(排查二进制文件格式)。

版本控制部分(2.1:集中式与分布式、独占签出与多重签出、三方合并、删除文件仍保留历史)在 GEA 中只是通用入门,本页不展开。书中的个人评价是:Subversion 每次都要扫描整棵目录树才能找出改动,大项目上会变慢;大型商业项目作者更倾向 Perforce 或 git。

与 UE 的对应

UE 的构建目标 DebugGame / Development / Shipping 与书中的 Debug / Development / Ship 基本对应,由 UnrealBuildTool 按模块组织,见 模块、插件与构建系统;Live Coding 用增量编译和补丁缩短迭代周期,见 Live Coding 实时编译;Unreal Insights 等工具属于书中所说的插桩式剖析(作用域计时),见 UE5 性能分析。

  1. Game_Engine_Architecture_4Ed_-_Jason_Gregory.pdf
  2. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 I 第 2 章 2.2–2.4 节

相关页面

游戏开发 · UE5模块、插件与构建系统Modules, Plugins & Build SystemUE 的代码由模块 (Module) 组成:每个模块是一个独立编译单元,用 [模块名].Build.cs 声明依赖。插件 (Plugin) 是可按项目启用/禁用的代码与内容集合,用 .uplugin 描述,可包含多个模块。虚幻构建工具 (UnrealBuildTool, UBT) 根据 .Target.cs 和 .Build.cs(而不是 IDE 的 .sln)编译,默认把模块编成 DLL 由一个可执行文件加载,也可整体链接成单体 (Monolithic) 可执行文件。游戏开发 · UE5UE5 性能分析UE5 ProfilingUE5 性能分析的基本流程是:先用 stat unit 判断瓶颈在游戏线程、渲染线程还是 GPU,再用对应的 stat 组、profilegpu/GPU 统计或 Unreal Insights 的 Timing/Memory 等视图向下钻取;渲染特性(RDG、VSM、TSR)各自提供作用域和可视化。测量时要避开会扭曲数据的设置(调试构建、异步计算、可视化模式)。游戏开发 · UE5Live Coding 实时编译Live CodingLive Coding 集成了 Live++,在引擎运行时重新编译 C++ 并给二进制打补丁;它比旧的 Hot Reload 快得多、也更灵活,新安装默认启用,快捷键 Ctrl+Alt+F11。默认开启的「对象重实例化 (Object Reinstancing)」会替换掉现有的 UCLASS/USTRUCT 等对象实例,从而支持新增函数、变量这类结构性修改;关闭它时只适合小改动,大改通常会崩溃。坑:在 .cpp 构造函数里改的默认值不会作用到已存在的实例。游戏开发运行时引擎分层架构Runtime Engine ArchitectureGregory 把游戏视为“软实时、交互式、基于代理的计算机模拟”,把游戏引擎定义为数据驱动、可复用来做多款游戏的软件。运行时引擎按层组织:从目标硬件、驱动、操作系统、第三方 SDK、平台无关层、核心系统、资源管理器,到渲染、物理、动画、音频、网络等子系统,再到游戏性基础层和游戏专属子系统;上层依赖下层,避免循环依赖。游戏开发错误处理与断言Error Handling & AssertionsGEA 把错误分为用户错误和程序员错误,两者处理方式相反:用户错误要尽量平稳地告知并继续,程序员错误应立即停机并给出详细的底层信息。“用户”又分玩家和制作游戏的开发者。实现手段有错误返回码、异常和断言。Naughty Dog、EA、Midway、Insomniac 等在运行时代码中通常关闭 C++ 异常,靠断言和静态断言 (staticassert) 捕获 bug,并用 RAII(Naughty Dog 称为 janitor)管理资源。游戏开发C++ 内存布局与对齐C++ Memory Layout & Alignment理解 C/C++ 程序在内存中的样子,是写高性能引擎代码的前提。主要内容包括:声明与定义、内部与外部链接;可执行映像的几个段(text、data、BSS、rodata)以及栈和堆;结构体成员的自然对齐和填充;继承和虚函数如何改变类的布局(vpointer 和 vtable)。GEA 也简要回顾了面向对象:多重继承容易产生致命菱形,所以通常只允许 mix-in;组合往往比继承更好。游戏开发并发与并行基础Concurrency & Parallelism FundamentalsGEA 区分两个正交的概念:并发 (Concurrency) 是软件层面多个控制流读写共享数据,核心难题是数据竞争;并行 (Parallelism) 是硬件层面两个以上部件同时工作。硬件并行又分为提升单线程速度的隐式并行(流水线、超标量、VLIW,即指令级并行 ILP)和为同时运行多条指令流而设计的显式并行(超线程、多核、多处理器、集群/云)。游戏开发字符串与唯一标识符Strings & Unique Identifiers字符串在 C/C++ 中只是字符数组,变长意味着要么硬编码长度上限、要么动态分配;比较和复制都是 O(n)。引擎内部大量用字符串做资源名和对象 ID,处理不当会严重拖慢游戏。GEA 的建议:运行时代码尽量避免字符串类,若用则了解其成本并按引用传递;路径类是值得专门做的例外;对象与资产的唯一标识用哈希字符串 ID(string id),兼得字符串的可读性与整数的比较速度。游戏开发游戏内性能与内存剖析In-Game Profiling & Memory Tracking游戏是实时系统,要保持 30 或 60 FPS,同时受目标硬件内存限制。第三方剖析工具有局限、在主机上未必可用,所以许多引擎自带游戏内剖析器(手动插桩的代码块计时、层级关系、调用次数、导出 CSV)和内存统计与泄漏检测工具。GEA 讲解它们的实现要点和常见限制。(卷 I 10.8–10.9 节)

被这些页面引用2 篇

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原