配置文件引导优化(也称为 PGO 或“pogo”)是一种使用有关游戏在现实世界中运行方式的信息来进一步优化游戏优化版本的构建的方法。通过这种方式,不常运行的代码(例如错误或极端情况)会从代码的关键执行路径中降级,从而加快速度。
图 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 出现在开发过程的后期,当您准备发布时。将 Profile-Guided Optimization 视为锦上添花,让您在花费一些时间自己优化代码后,能够从代码中榨取最后的性能。
PGO 预期性能提升
这取决于许多因素,包括您的配置文件的全面性和陈旧程度,以及使用传统优化构建时您的代码距离最优有多近。
通常,一个*非常*保守的估计是关键线程的 CPU 成本将降低约 5%。您可能会看到不同的结果。
插桩开销
PGO 的插桩非常全面,虽然它是自动生成的,但它不是免费的。PGO 插桩的开销可能会因您的代码库而异。
Profile-Guided Instrumentation 的性能成本
您可能会在插桩构建中看到帧率下降。在某些情况下,根据 CPU 在正常运行期间利用率接近 100% 的程度,这种下降可能非常大,以至于难以进行正常游戏。
我们建议大多数开发者为他们的游戏构建一个半确定性的回放模式。这种功能可以让您的 QA 团队从游戏中已知且可重复的起始位置(例如存档游戏或特定的测试关卡)开始游戏,然后记录他们的输入。从测试构建中记录的此输入可以馈送到 PGO 插桩构建中,回放并生成真实世界的配置文件数据,而不管处理单个帧需要多长时间——即使游戏运行速度慢到无法玩。
这种功能还有其他主要优势,例如成倍增加测试人员的工作量:一名测试人员可以在设备上记录他们的输入,然后可以在多种不同类型的设备上回放以进行冒烟测试。
这种回放系统在 Android 上具有巨大的优势,因为 Android 生态系统中有大量的设备变体——而且优势不止于此:它也可以构成您持续集成构建系统的重要组成部分,允许您定期执行夜间性能回归和冒烟测试。
录制应在游戏输入机制中最合适的位置记录用户输入(可能不是直接的触摸屏事件,而是记录其作为命令的结果)。这些输入还应该包含一个在游戏过程中单调递增的帧计数,以便在回放过程中,回放机制可以等待应触发事件的相应帧。
在回放模式下,您的游戏应该避免在线登录,不应该显示广告,并且应该以固定的时间步长运行(以您的目标帧率)。您应该考虑禁用垂直同步。
游戏中的所有内容(例如粒子系统)都完美地确定性地可重复并不重要,但相同的动作应该产生相同的游戏内结果和后果——也就是说,游戏玩法应该相同。
Profile-Guided Instrumentation 的内存成本
PGO 插桩的内存开销根据编译的特定库而异。在我们的测试中,我们发现测试可执行文件的大小总体增加了约 2.2 倍。此大小增加包括插桩代码块所需的额外代码以及存储计数器所需的空间。这些测试并不详尽,您的体验可能会有所不同。
何时更新或丢弃配置文件数据
每当您对代码(或游戏内容)进行重大更改时,都应更新配置文件。
这的确切含义取决于您的构建环境以及您在开发中的位置。
如前所述,您不应在主要的构建环境更改之间保留配置文件数据;虽然这不会阻止您构建或破坏您的构建,但这会降低使用 PGO 的性能优势,因为很少有配置文件数据适用于新的构建环境。但是,这并不是您的配置文件数据可能变得陈旧的唯一情况。
让我们首先假设您不会在开发接近尾声准备发布之前使用 PGO,除了可能每周捕获一次数据,以便性能工程师可以验证在发布前不会出现任何意外的故障。
当您接近发布窗口时,情况会发生变化,届时您的 QA 团队每天都在测试,并详尽地运行游戏。在此阶段,您可以每天从这些数据生成配置文件,并使用它们来告知未来构建的性能测试和调整您自己的性能预算。
当您准备发布时,您应该锁定计划发布的构建版本,然后让 QA 运行它以生成新的配置文件数据。然后,您使用此数据构建以生成可执行文件的最终版本。
然后,QA 可以对这个经过优化的发布版本进行最终运行,以确保它可以发布。