配置文件引导优化 (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 设备。在游戏运行期间,配置文件数据会自动捕获,您的质量保证工程师将通玩游戏,演练不同的场景(或者只是进行正常的测试流程)。

演练完游戏的各个部分后,您可以返回主菜单,这会结束当前游戏场景并写入配置文件数据。

然后,可以使用脚本将配置文件数据从测试设备中复制出来,并上传到中央代码库以供日后使用。

合并配置文件数据

从设备获取配置文件后,需要将仪器化构建生成的配置文件数据文件转换为编译器可以使用的形式。对于您添加到项目的任何配置文件数据文件,AGDE 都会自动执行此操作。

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

以合并配置文件数据集的实用性为例:假设您有一个实验室,里面坐满了质量保证工程师,他们都在玩您的游戏的不同关卡。他们的每次通玩都会被记录下来,然后用于从游戏的 PGO 检测构建版本中生成配置文件数据。合并配置文件可让您组合所有这些不同测试运行的结果(这些运行可能会执行代码中截然不同的部分),以获得更好的结果。

更好的是,在执行纵向测试时(您会在内部发布版本之间保留配置文件数据的副本),重新构建不一定会使旧的配置文件数据失效。在大多数情况下,代码在不同版本之间相对稳定,因此旧构建版本中的配置文件数据仍然有用,不会立即失效。

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

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

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

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

PGO 并非旨在在开发开始时或日常代码迭代期间启用。在开发期间,您应侧重于基于算法和数据布局的优化,因为它们会带来更大的收益。

PGO 在开发过程的后期阶段使用,即您为发布进行优化时。配置文件引导优化好比锦上添花,可让您在自行优化代码一段时间后,从代码中榨取最后一丝性能。

PGO 的预期性能提升

这取决于许多因素,包括您的配置文件的全面性和过时程度,以及在传统优化构建版本中您的代码接近最佳状态的程度。

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

检测开销

PGO 的检测是全面的,虽然它是自动生成的,但并非没有成本。PGO 检测的开销可能会因您的代码库而异。

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

您可能会发现,经过检测的构建版本会导致帧速率下降。在某些情况下,根据 CPU 在正常运行期间的利用率是否接近 100%,此下降幅度可能会非常大,以至于使正常游戏变得困难。

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

这种功能还具有其他主要优势,例如倍增测试人员的工作量:一个测试人员可以在一台设备上记录他们的输入,然后可以在多种不同类型的设备上播放,用于冒烟测试。

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

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

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

您的游戏中的所有内容(例如粒子系统)是否完美地确定性可重复并不重要,但相同的操作应产生相同的游戏内结果和效果,也就是说,游戏体验应保持不变。

配置文件引导检测的内存成本

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

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

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

这具体意味着什么取决于您的构建环境以及您所处的开发阶段。

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

我们首先假设您在开发接近尾声、准备发布之前不会使用 PGO,除了可能每周收集一次数据,以便性能工程师可以验证在发布临近时不会出现任何意外问题。

随着您临近发布窗口,这种情况会发生变化,届时您的质量保证团队会每天进行测试,并详尽地运行游戏。在此阶段,您可以每天从这些数据生成配置文件,并使用它们来为未来的构建版本提供性能测试和调整您自己的性能预算的信息。

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

然后,质量保证团队可以对这个已优化且可发布的构建版本进行最终运行,以确保其已准备好发布。