配置文件引导优化 (PGO) 的工作原理

配置文件引导优化(也称为 PGO 或“pogo”)是一种方法,它使用有关游戏在现实世界中运行方式的信息来进一步优化游戏的优化构建。通过这种方式,错误或边缘情况等不常运行的代码会被从代码的关键执行路径中降级,从而加快其速度。

A diagram showing a visual overview of how PGO
works

图 1. PGO 的工作原理概述。

要使用 PGO,首先要对构建进行检测以生成编译器可以使用的配置文件数据。然后,通过运行该构建并生成一个或多个配置文件数据文件来执行代码。最后,将这些文件从设备复制回来,并将其与编译器一起使用,根据捕获的配置文件信息优化可执行文件。

不使用 PGO 的优化构建的工作原理

不使用配置文件数据进行优化的构建在决定如何生成优化代码时会使用多种启发式方法。

一些启发式方法是由开发者明确指示的 - 例如,在 C++ 20 或更高版本中,通过使用分支方向提示,例如 [[likely]][[unlikely]]。另一个例子是使用 inline 关键字,甚至 __forceinline(尽管总的来说,坚持使用前者更好、更灵活)。默认情况下,一些编译器假设分支的第一条路径(即 if 语句,而不是 else 部分)是最可能的路径。优化器还可以从对代码的静态分析中做出关于其如何执行的假设 - 但这通常范围有限。

这些启发式方法的问题在于,它们无法在所有情况下正确地帮助编译器 - 即使经过详尽的手动标记 - 因此,虽然生成的代码通常经过良好优化,但如果编译器有更多关于其在运行时的行为的信息,它会变得更好。

生成配置文件

当你的可执行文件在检测模式下启用 PGO 构建时,你的可执行文件会在每个代码块的开头添加代码 - 例如,函数的开头,或分支每个分支的开头。此代码用于跟踪代码块每次被运行代码执行的次数,编译器之后可以使用这些信息来生成优化代码。

还会执行一些其他跟踪 - 例如,块中典型复制操作的大小,以便之后可以生成快速、内联版本的该操作。

在游戏执行了一些代表性的工作后,可执行文件必须调用一个函数 - __llvm_profile_write_file() - 将配置文件数据写入设备上的自定义位置。当你的构建配置启用了 PGO 检测时,此函数会自动链接到你的游戏。

然后应将写入的配置文件数据文件复制回主机计算机,最好与来自同一构建的其他配置文件一起保存,以便它们可以一起使用。

例如,你可以修改游戏代码,以便在当前游戏场景结束时调用 __llvm_profile_write_file()。然后,要获取配置文件,你需要构建启用了检测功能的游戏,然后将其部署到 Android 设备。在运行过程中,会自动捕获配置文件数据 - 你的 QA 工程师会运行游戏,执行不同的场景(或者只是执行他们正常的测试流程)。

完成对不同游戏部分的执行后,你可以返回主菜单,这将结束当前游戏场景并写入配置文件数据。

然后,可以使用脚本将配置文件数据从测试设备复制出来,并将其上传到中央存储库,以便稍后使用。

合并配置文件数据

从设备获得配置文件后,需要将其从检测构建生成的配置文件数据文件转换为编译器可以使用的形式。AGDE 会为你自动完成此操作,适用于你添加到项目的任何配置文件数据文件。

PGO 旨在将来自多个检测配置文件运行的结果合并在一起 - 如果项目中有多个文件,AGDE 也会为你自动执行此操作。

例如,假设你有一个实验室,里面有许多 QA 工程师都在玩游戏的不同关卡。他们每次游戏都会被记录下来,然后用于从游戏的 PGO 检测构建中生成配置文件数据。合并配置文件使你能够将来自所有这些不同测试运行的结果 - 这些测试运行可能执行代码的不同部分 - 组合在一起,从而获得更好的结果。

更棒的是,在进行纵向测试时,如果保留了从内部发布到内部发布的配置文件数据的副本,则重建不一定使旧配置文件数据失效。在大多数情况下,代码在发布之间相对稳定,因此来自旧构建的配置文件数据仍然有用,并且不会立即失效。

生成配置文件引导优化构建

将配置文件数据添加到项目后,可以通过在构建配置中启用 PGO 优化模式来使用它来构建可执行文件。

这会指示编译器的优化器在做出优化决策时使用之前捕获的配置文件数据。

何时使用配置文件引导优化

PGO 不适合在开发初期或在日常代码迭代期间启用。在开发期间,你应该专注于算法和数据布局方面的优化,因为它们会给你带来更大的收益。

PGO 出现在开发过程的后期,当你为发布进行润色时。将配置文件引导优化视为锦上添花,在你已经花费了一些时间优化代码之后,它可以帮助你从代码中榨取最后的性能。

预期使用 PGO 的性能改进

这取决于许多因素,包括配置文件的全面性和陈旧程度,以及代码在使用传统优化构建时的优化程度。

总的来说,一个非常保守的估计是,关键线程的 CPU 成本将降低约 5%。你可能会看到不同的结果。

检测开销

PGO 的检测功能非常全面,虽然它是自动生成的,但它不是免费的。PGO 检测的开销可能会因代码库而异。

配置文件引导检测的性能成本

你可能会看到检测构建的帧速率下降。在某些情况下 - 取决于在正常运行期间 CPU 使用率接近 100% 的程度 - 这种下降可能非常大,以至于难以进行正常游戏。

我们建议大多数开发者为他们的游戏构建一个半确定性重放模式。这种功能使您的 QA 团队能够从游戏中已知可重复的起始位置(例如存档游戏或特定测试关卡)开始游戏,然后记录他们的输入。从测试版本中记录的这种输入可以被馈送到 PGO 检测的版本中,重放并生成真实的配置文件数据,而不管处理单个帧需要多长时间 - 即使游戏运行缓慢以至于无法玩。

这种功能还有其他主要优势,例如成倍地提高测试人员的工作效率:一个测试人员可以在一个设备上记录他们的输入,然后可以在多个不同类型的设备上回放,以进行冒烟测试。

这种重放系统在 Android 上具有巨大优势,因为 Android 生态系统中存在大量的设备变体 - 而且优势不止于此:它还可以成为您持续集成构建系统的重要组成部分,使您能够定期执行夜间性能回归和冒烟测试。

记录应在游戏输入机制中最合适的点记录用户输入(可能不是直接的触摸屏事件,而是记录它们的结果作为命令)。这些输入还应包含一个在游戏过程中单调递增的帧计数,以便在回放期间,回放机制可以等待应该触发事件的适当帧。

在回放模式下,您的游戏应避免在线登录,不应显示广告,并且应以固定时间步长(以您的目标帧速率)运行。您应该考虑禁用垂直同步。

游戏中的所有内容(例如粒子系统)是否完全确定性可重复并不重要,但相同的动作应该产生相同的游戏内后果和结果 - 也就是说,游戏玩法应该相同。

配置文件检测的内存成本

PGO 检测的内存开销根据被编译的特定库而有很大差异。在我们的测试中,我们看到测试可执行文件的大小总体上增加了约 2.2 倍。这种大小增加包括检测代码块所需的额外代码以及存储计数器所需的存储空间。这些测试并不详尽,您的体验可能会有所不同。

何时更新或丢弃您的配置文件数据

每当您对代码(或游戏内容)进行重大更改时,都应更新您的配置文件。

这的确切含义取决于您的构建环境以及您在开发过程中的位置。

如前所述,您不应跨主要构建环境更改保留配置文件数据;虽然这不会阻止您构建或破坏您的构建,但这会降低使用 PGO 的性能优势,因为很少有配置文件数据适用于新的构建环境。但是,这并不是您的配置文件数据可能过时的唯一情况。

让我们首先假设您不会在接近开发结束时准备发布之前使用 PGO,除了可能每周收集一次以让专注于性能的工程师能够验证在接近发布时是否不会出现意外的故障。

当您接近发布窗口时,这种情况会发生变化,届时您的 QA 团队每天都会测试,并会彻底运行游戏。在此阶段,您可以每天从这些数据生成配置文件,并使用这些配置文件来告知未来的构建以进行性能测试并调整您自己的性能预算。

准备发布时,您应该锁定计划发布的构建版本,然后让 QA 运行它以生成新的配置文件数据。然后,使用此数据进行构建以生成可执行文件的最终版本。

然后,QA 可以对这种经过优化的发布版本进行最终运行,以确保其可以发布。