配置文件引导的优化

配置文件引导的优化 (PGO) 是一种常用的编译器优化技术。在 PGO 中,编译器会使用程序执行的运行时配置文件,对内嵌和代码布局进行优化。这有助于提高性能并缩减代码大小。

您可以按照以下步骤将 PGO 部署到您的应用或库中:1. 确定一个具有代表性的工作负载。2. 收集配置文件。 3. 在发布 build 中使用这些配置文件。

第 1 步:确定一个具有代表性的工作负载

首先,为您的应用确定一个具有代表性的基准或工作负载。这一步很关键,因为从工作负载收集的配置文件会标识代码中的热区域和冷区域。使用这些配置文件时,编译器会在热区域中主动执行优化和内嵌。编译器也可以缩减冷区域的代码大小,但性能也会随之降低。

总体而言,工作负载选择得当对于跟踪性能也会很有帮助。

第 2 步:收集配置文件

收集配置文件包括三个步骤:使用插桩构建原生代码;在设备上运行插桩应用并生成配置文件;在主机上合并/后处理配置文件。

创建插桩 build

在应用的插桩 build 中运行在第 1 步中确定的工作负载来收集配置文件。如需生成插桩 build,请将 -fprofile-generate 添加到编译器标记和链接器标记中。此标记应由单独的 build 变量控制,因为在默认 build 期间不需要此标记。

生成配置文件

接下来,在设备上运行插桩应用并生成配置文件。运行插桩二进制文件,并在退出时写入文件,系统便会在内存中收集配置文件。不过,使用 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-profdataclang,以免配置文件格式出现版本不匹配的问题。

第 3 步:使用配置文件构建应用

通过将 -fprofile-use=<>.profdata 传递给编译器和链接器,在应用的发布 build 中使用在上一步生成的配置文件。即使代码在不断演变,也可以继续使用这些配置文件。Clang 编译器可以容忍源文件和配置文件之间的细微不匹配。

注意:一般而言,对于大多数库来说,配置文件在各个架构中是相同的。例如,arm64 build 生成的配置文件可用于所有架构。需要注意的是,如果库中有特定于架构的代码路径(arm 与 x86 或 32 位与 64 位),则应针对每个此类配置使用单独的配置文件。

总结

https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo 演示了在应用中使用 PGO 的完整流程。该演示提供了本文档中略去的其他详细信息。

  • CMake 构建规则说明了如何设置 CMake 变量,从而通过插桩构建原生代码。如果未设置 build 变量,系统会使用之前生成的 PGO 配置文件来优化原生代码。
  • 在插桩 build 中,pgodemo.cpp 会在工作负载执行期间写入配置文件。
  • 可在运行时使用 applicationContext.cacheDir.toString()MainActivity.kt 中获取配置文件的可写入位置。
  • 如需在不使用 adb root 的情况下从设备提取配置文件,请使用此处adb 配方。