Profile-guided Optimization

Profile-guided optimization (PGO) 是一种众所周知的编译器优化技术。在 PGO 中,编译器会使用程序执行的运行时配置文件来对内联和代码布局做出最佳选择。这将提高性能并减小代码大小。

PGO 可以通过以下步骤部署到您的应用程序或库中:1. 确定代表性工作负载。2. 收集配置文件。3. 在发布版本中使用配置文件。

步骤 1:确定代表性工作负载

首先,确定应用程序的代表性基准测试或工作负载。这是至关重要的步骤,因为从工作负载收集的配置文件会确定代码中的热点和冷点区域。使用配置文件时,编译器将在热点区域执行积极的优化和内联。编译器还可以选择缩减冷点区域的代码大小,同时牺牲性能。

确定合适的工作负载也有助于一般跟踪性能。

步骤 2:收集配置文件

配置文件收集涉及三个步骤:- 构建带有检测功能的原生代码,- 在设备上运行带有检测功能的应用程序并生成配置文件,以及 - 在主机上合并/后处理配置文件。

创建带有检测功能的构建

通过在设备上运行步骤 1 中的工作负载,从带有检测功能的应用程序构建中收集配置文件。要生成带有检测功能的构建,请将 -fprofile-generate 添加到编译器和链接器标志中。此标志应由单独的构建变量控制,因为在默认构建期间不需要此标志。

生成配置文件

接下来,在设备上运行带有检测功能的应用程序并生成配置文件。当运行带有检测功能的二进制文件时,配置文件会在内存中收集,并在退出时写入文件。但是,在 Android 应用程序中不会调用使用 atexit 注册的函数 - 应用程序只是被终止。

应用程序/工作负载必须执行额外工作来设置配置文件路径,然后显式触发配置文件写入。

  • 要设置配置文件路径,请调用 __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-profdataclang 以避免配置文件格式的版本不匹配。

步骤 3 使用配置文件构建应用程序

在应用程序的发布版本构建期间,通过将 -fprofile-use=<>.profdata 传递给编译器和链接器,使用上一步中的配置文件。即使代码在不断发展,也可以使用这些配置文件 - Clang 编译器可以容忍源代码和配置文件之间略微不匹配。

注意:一般情况下,对于大多数库,配置文件在不同架构之间是通用的。例如,从 arm64 版本的库生成的配置文件可以用于所有架构。需要注意的是,如果库中存在架构特定的代码路径(arm 与 x86 或 32 位与 64 位),则应为每种配置使用单独的配置文件。

将所有内容整合在一起

https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo 展示了从应用程序使用 PGO 的端到端演示。它提供了本文档中略过的更多详细信息。

  • CMake 构建规则 中,展示了如何设置 CMake 变量来构建包含 instrumentation 的原生代码。当未设置构建变量时,原生代码会使用先前生成的 PGO 配置文件进行优化。
  • 在包含 instrumentation 的构建中,pgodemo.cpp 会在工作负载执行时写入配置文件。
  • MainActivity.kt 中,使用 applicationContext.cacheDir.toString() 在运行时获取配置文件的可写位置。
  • 为了从设备中提取配置文件,而无需使用 adb root,请使用 adb 脚本,此处