配置文件引导优化 (PGO) 是一种众所周知的编译器优化技术。在 PGO 中,编译器利用程序运行时的配置文件对内联和代码布局做出最优选择。这可以提高性能并减小代码大小。
可以通过以下步骤将 PGO 应用于你的应用或库:1. 识别代表性工作负载。2. 收集配置文件。3. 在发布版本中使用配置文件。
第 1 步:识别代表性工作负载
首先,为你的应用识别一个代表性的基准测试或工作负载。这是关键一步,因为从工作负载中收集的配置文件可识别代码中的热点区域和冷点区域。使用这些配置文件时,编译器将在热点区域执行积极的优化和内联。编译器也可能选择在冷点区域减小代码大小,同时权衡性能。
识别良好的工作负载通常也有助于跟踪性能。
第 2 步:收集配置文件
配置文件收集包括三个步骤:- 构建带有插装的原生代码,- 在设备上运行带插装的应用并生成配置文件,以及 - 在主机上合并/后处理配置文件。
创建插装版本
通过在应用的插装版本上运行第 1 步中的工作负载来收集配置文件。要生成插装版本,请将 -fprofile-generate
添加到编译器和链接器标志。该标志应由单独的构建变量控制,因为在默认构建期间不需要此标志。
生成配置文件
接下来,在设备上运行带插装的应用并生成配置文件。带插装的二进制文件运行时,配置文件在内存中收集,并在退出时写入文件。但是,注册到 atexit
的函数在 Android 应用中不会被调用,应用会直接被杀死。
应用/工作负载必须执行额外的工作来设置配置文件路径,然后显式触发配置文件写入。
- 要设置配置文件路径,请调用
__llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw")
。%m
在存在多个共享库时很有用。%m
会展开为该库的唯一模块签名,从而为每个库生成单独的配置文件。有关其他有用的模式说明符,请参阅此处。PROFILE_DIR
是应用可写的目录。有关在运行时检测此目录的信息,请参阅演示。 - 要显式触发配置文件写入,请调用
__llvm_profile_write_file
函数。
extern "C" {
extern int __llvm_profile_set_filename(const char*);
extern int __llvm_profile_write_file(void);
}
#define PROFILE_DIR "<location-writable-from-app>"
void workload() {
// ...
// run workload
// ...
// set path and write profiles after workload execution
__llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw");
__llvm_profile_write_file();
return;
}
注意:如果工作负载是独立二进制文件,则生成配置文件会更简单,只需在运行二进制文件之前将 LLVM_PROFILE_FILE
环境变量设置为 %t/default-%m.profraw
。
后处理配置文件
配置文件采用 .profraw 格式。必须先使用 adb pull
从设备中提取它们。提取后,使用 NDK 中的 llvm-profdata
工具将 .profraw
格式转换为 .profdata
,然后才能将其传递给编译器。
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-profdata \
merge --output=pgo_profile.profdata \
<list-of-profraw-files>
使用与 NDK 版本相同的 llvm-profdata
和 clang
,以避免配置文件格式的版本不匹配。
第 3 步:使用配置文件构建应用
在应用的发布构建期间使用上一步中的配置文件,方法是将 -fprofile-use=<>.profdata
传递给编译器和链接器。即使代码发生变化,也可以使用这些配置文件,Clang 编译器可以容忍源代码和配置文件之间的细微不匹配。
注意:通常,对于大多数库来说,配置文件在不同架构之间是通用的。例如,从库的 arm64 构建生成的配置文件可以用于所有架构。需要注意的是,如果库中存在特定于架构的代码路径(arm 与 x86 或 32 位与 64 位),则应为每个此类配置使用单独的配置文件。
综合运用
https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo 展示了一个从应用使用 PGO 的端到端演示。它提供了本文档中略过的一些额外细节。
- CMake 构建规则展示了如何设置 CMake 变量以构建带有插装的原生代码。当未设置构建变量时,原生代码将使用之前生成的 PGO 配置文件进行优化。
- 在插装构建中,pgodemo.cpp 在工作负载执行后写入配置文件。
- 在MainActivity.kt 中使用
applicationContext.cacheDir.toString()
在运行时获取可写入配置文件的位置。 - 要在不需要
adb root
的情况下从设备中提取配置文件,请使用此处的adb
方法。