Профильная оптимизация

Оптимизация на основе профиля (PGO) — это хорошо известный метод оптимизации компилятора. В PGO профили времени выполнения программы используются компилятором для принятия оптимальных решений по встраиванию и компоновке кода. Это приводит к повышению производительности и уменьшению размера кода.

PGO можно развернуть в вашем приложении или библиотеке, выполнив следующие действия: 1. Определите репрезентативную рабочую нагрузку. 2. Соберите анкеты. 3. Используйте профили в сборке Release.

Шаг 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 . После выборки используйте утилиту llvm-profdata в NDK для преобразования .profraw в .profdata , который затем можно будет передать компилятору.

$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-profdata \
    merge --output=pgo_profile.profdata \
    <list-of-profraw-files>

Используйте llvm-profdata и clang из одной и той же версии NDK, чтобы избежать несоответствия версий форматов файлов профилей.

Шаг 3. Использование профилей для создания приложения

Используйте профиль из предыдущего шага во время сборки выпуска вашего приложения, передав -fprofile-use=<>.profdata компилятору и компоновщику. Профили можно использовать даже по мере развития кода — компилятор Clang допускает небольшое несоответствие между исходным кодом и профилями.

NB: В целом, для большинства библиотек профили являются общими для разных архитектур. Например, профили, созданные из сборки библиотеки 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 здесь .