排序文件

排序文件是一种最近的链接器优化技术。这些排序文件是包含代表函数的符号的文本文件。像 lld 这样的链接器使用排序文件按特定顺序排列函数。由于在程序冷启动期间有效加载符号,因此具有排序符号的这些二进制文件或库减少了页面错误并提高了程序的启动时间。

可以通过以下三个步骤将排序文件功能添加到您的应用程序中

  1. 生成配置文件和映射文件
  2. 从配置文件和映射文件创建排序文件
  3. 在发布版本期间使用排序文件来排列符号

生成排序文件

生成排序文件需要三个步骤

  1. 构建应用程序的检测版本,该版本写入排序文件
  2. 运行应用程序以生成配置文件
  3. 对配置文件和映射文件进行后处理

创建检测版本

配置文件是通过运行应用程序的检测版本生成的。检测版本需要将 -forder-file-instrumentation 添加到编译器和链接器标志中,并且 -mllvm -orderfile-write-mapping=<filename>-mapping.txt 严格添加到编译器标志中。检测标志启用排序文件检测以进行分析,并加载特定库以进行分析。另一方面,映射标志只是输出映射文件,该文件显示二进制文件或库中每个函数的 MD5 哈希。

此外,请确保传递任何优化标志,但不要传递 -O0,因为检测标志和映射标志都需要一个优化标志。如果没有传递优化标志,则不会生成映射文件,并且检测版本可能会输出错误的哈希值到配置文件。

ndk-build

请确保使用 APP_OPTIM=release 构建,以便 ndk-build 使用除 -O0 之外的优化模式。使用 AGP 构建时,这对于发布版本是自动的。

LOCAL_CFLAGS += \
    -forder-file-instrumentation \
    -mllvm -orderfile-write-mapping=mapping.txt \

LOCAL_LDFLAGS += -forder-file-instrumentation

CMake

请确保使用除 Debug 之外的 CMAKE_BUILD_TYPE,以便 CMake 使用除 -O0 之外的优化模式。使用 AGP 构建时,这对于发布版本是自动的。

target_compile_options(orderfiledemo PRIVATE
    -forder-file-instrumentation
    -mllvm -orderfile-write-mapping=mapping.txt
)
target_link_options(orderfiledemo PRIVATE -forder-file-instrumentation)

其他构建系统

使用 -forder-file-instrumentation -O1 -mllvm -orderfile-write-mapping=mapping.txt 编译您的代码。

-O1 并非必需,但不要使用 -O0

在链接时省略 -mllvm -orderfile-write-mapping=mapping.txt

所有这些标志对于发布版本都是不必要的,因此应该由构建变量控制。为了简单起见,您可以在 CMakeLists.txt 中设置所有这些,就像在我们的 示例 中一样。

创建排序文件库

调用 __llvm_profile_set_filename(PROFILE_DIR "/<filename>-%m.profraw") 来设置配置文件路径。尽管传递的参数是 <filename>-%m.profraw,但配置文件将保存为 <filename>-%m.profraw.order。确保 PROFILE_DIR 可由应用程序写入,并且您有权访问该目录。

  • 除了标志之外,还需要设置配置文件,并且检测版本的二进制文件需要在执行期间显式触发配置文件写入。
    • 由于对许多共享库进行了分析,因此 %m 很有用,因为它扩展为库的唯一模块签名,从而为每个库生成一个单独的配置文件。有关更多模式说明符,您可以查看此 链接
  • 调用 __llvm_profile_initialize_file() 来设置配置文件。
  • 调用 __llvm_orderfile_dump() 来显式写入配置文件。

配置文件会在内存中收集,并由 dump 函数写入文件。您需要确保在启动结束时调用 dump 函数,以便您的配置文件包含所有符号,直到启动结束。

extern "C" {
extern int __llvm_profile_set_filename(const char*);
extern int __llvm_profile_initialize_file(void);
extern int __llvm_orderfile_dump(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_initialize_file();
  __llvm_orderfile_dump();
  return;
}

运行用于配置文件的构建

在物理或虚拟设备上运行已插入工具的应用程序以生成配置文件。您可以使用 adb pull 提取配置文件。

adb shell "run-as <package-name> sh -c 'cat /data/user/0/<package-name>/cache/default-%m.profraw.order' | cat > /data/local/tmp/default-%m.profraw.order"
adb pull /data/local/tmp/default-%m.profraw.order .

如前所述,请确保您能够访问包含已写入配置文件的文件夹。如果是虚拟设备,您可能需要避免使用带有 Play Store 的模拟器,因为无法访问许多文件夹。

后处理配置文件和映射文件

获得配置文件后,您需要找到映射文件并将每个配置文件转换为十六进制格式。通常,您可以在应用程序的构建文件夹中找到映射文件。当您拥有两者后,您可以使用我们的 脚本 从配置文件和正确的映射文件生成一个排序文件。

Linux/Mac/ChromeOS

hexdump -C default-%m.profraw.order > default-%m.prof
python3 create_orderfile.py --profile-file default-%m.prof --mapping-file <filename>-mapping.txt

Windows

certutil -f -encodeHex default-%m.profraw.order default-%m.prof
python3 create_orderfile.py --profile-file default-%m.prof --mapping-file <filename>-mapping.txt

如果您想了解有关脚本的更多信息,可以查看此 README

使用排序文件构建应用程序

生成排序文件后,您应该删除早期的标志和排序文件函数,因为这些函数只用于生成步骤。您只需要将 -Wl,--symbol-ordering-file=<filename>.orderfile 传递给编译和链接器标志。有时,符号可能无法找到或无法移动,并发出警告,因此您可以传递 -Wl,--no-warn-symbol-ordering 来抑制这些警告。

ndk-build

LOCAL_CFLAGS += \
    -Wl,--symbol-ordering-file=<filename>.orderfile \
    -Wl,--no-warn-symbol-ordering \

LOCAL_LDFLAGS += \
    -Wl,--symbol-ordering-file=<filename>.orderfile \
    -Wl,--no-warn-symbol-ordering \

CMake

target_compile_options(orderfiledemo PRIVATE
    -Wl,--symbol-ordering-file=<filename>.orderfile
    -Wl,--no-warn-symbol-ordering
)
target_link_options(orderfiledemo PRIVATE
    -Wl,--symbol-ordering-file=<filename>.orderfile
    -Wl,--no-warn-symbol-ordering
)

其他构建系统

使用 -Wl,--symbol-ordering-file=<filename>.orderfile -Wl,--no-warn-symbol-ordering 编译您的代码。

有关更多信息,请查看 排序文件示例

排序文件实现细节

有很多方法可以生成排序文件并将其用于构建。NDK 使用 LLVM 的方法,因此它对于您的 C 或 C++ 共享库比实际的 Java 或 Kotlin 应用程序更有用。Clang 获取每个函数名称(符号)并创建其 MD5 哈希,并将此关系输出到映射文件。函数的 MD5 哈希在函数首次执行时写入配置文件(profraw 格式)。函数的任何后续执行都不会将其 MD5 哈希写入配置文件,因为它希望避免重复项。因此,只有函数的第一次执行被记录在排序中。通过遍历配置文件和映射文件,您可以获取每个 MD5 哈希并将其替换为相应的函数,从而获得一个排序文件。

十六进制格式的配置文件和映射文件的示例分别可以在 example.profexample-mapping.txt 中找到。