Otimização guiada por perfil

A otimização guiada por perfil (PGO, na sigla em inglês) é uma técnica conhecida de otimização de compiladores. Na PGO, os perfis de ambiente de execução de um programa são usados pelo compilador para fazer escolhas ideais sobre in-line e o layout do código. Isso melhora o desempenho e diminui o tamanho do código.

É possível implantar a PGO em seu aplicativo ou biblioteca seguindo estas etapas: 1. Identificar uma carga de trabalho representativa. 2. Coletar perfis. 3. Usar os perfis em um build de lançamento.

Etapa 1: identificar uma carga de trabalho representativa

Primeiro, identifique um comparativo de mercado ou uma carga de trabalho representativos para seu app. Essa é uma etapa essencial, porque os perfis coletados da carga de trabalho identificam as regiões quentes e frias no código. Ao usar os perfis, o compilador executa otimizações agressivas e in-line nas regiões quentes. O compilador também pode reduzir o tamanho de regiões frias do código, comprometendo o desempenho.

Identificar uma boa carga de trabalho também é útil para monitorar o desempenho de modo geral.

Etapa 2: coletar perfis

A coleta de perfis envolve três etapas: criar o código nativo com instrumentação; executar o app instrumentado no dispositivo e gerar perfis; e mesclar/pós-processar os perfis no host.

Criar um build instrumentado

Os perfis são coletados ao executar a carga de trabalho da etapa 1 em um build instrumentado do aplicativo. Para gerar um build instrumentado, adicione -fprofile-generate às sinalizações do compilador e do vinculador. Essa sinalização precisa ser controlada por uma variante de build separada, porque ela não é necessária durante a criação padrão.

Gerar perfis

Em seguida, execute o app instrumentado no dispositivo e gere perfis. Esses perfis são coletados na memória quando o binário instrumentado é executado e gravados em um arquivo na saída. No entanto, as funções registradas com atexit não são chamadas em um app Android, e ele acaba sendo eliminado.

O app/a carga de trabalho precisa fazer um trabalho extra para definir um caminho para o arquivo de perfil e depois acionar explicitamente uma gravação de perfil.

  • Para definir o caminho do arquivo de perfil, chame __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw. O valor %m é útil quando há várias bibliotecas compartilhadas. O %m se expande para uma assinatura de módulo exclusiva para essa biblioteca, resultando em um perfil separado por biblioteca. Confira aqui outros especificadores de padrão úteis. PROFILE_DIR é um diretório gravável do app. Consulte a demonstração para detectar esse diretório no momento da execução.
  • Para acionar explicitamente uma gravação de perfil, chame a função __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;
}

Atenção: a geração do arquivo de perfil será mais simples se a carga de trabalho for um binário autônomo. Basta definir a variável de ambiente LLVM_PROFILE_FILE como %t/default-%m.profraw antes de executar o binário.

Perfis de pós-processamento

Os arquivos de perfil estão no formato .profraw. Primeiro, eles precisam ser buscados no dispositivo usando o adb pull. Depois da busca, use o utilitário llvm-profdata no NDK para converter de .profraw para .profdata, que pode ser transmitido para o compilador.

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

Use o llvm-profdata e o clang do mesmo NDK para evitar incompatibilidade de versão dos formatos de arquivo de perfil.

Etapa 3: usar os perfis para criar um aplicativo

Use o perfil da etapa anterior em um build de lançamento do seu aplicativo, transmitindo -fprofile-use=<>.profdata para o compilador e o vinculador. Os perfis podem ser usados mesmo enquanto o código evolui. O compilador Clang pode tolerar uma leve incompatibilidade entre a origem e os perfis.

Atenção: para a maioria das bibliotecas, os perfis são comuns a todas as arquiteturas. Por exemplo, os perfis gerados pelo build arm64 da biblioteca podem ser usados para todas as arquiteturas. No entanto, se houver caminhos de código específicos da arquitetura na biblioteca (arm vs. x86 ou 32 bits vs. 64 bits), perfis separados precisam ser usados para cada configuração.

Como tudo funciona em conjunto

Na página https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo (em inglês), é possível ver uma demonstração completa sobre como usar a PGO em um app. Ela fornece detalhes que vão além do descrito neste documento.

  • As regras de compilação do CMake (link em inglês) mostram como configurar uma variável do CMake que cria código nativo com instrumentação. Quando a variante de build não está definida, o código nativo é otimizado usando perfis de PGO gerados anteriormente.
  • Em um build instrumentado, pgodemo.cpp (link em inglês) grava os perfis como execução de carga de trabalho.
  • Um local gravável para os perfis é recebido no momento da execução no MainActivity.kt (link em inglês) usando applicationContext.cacheDir.toString().
  • Para extrair perfis do dispositivo sem precisar do adb root, use o roteiro do adb mostrado aqui (link em inglês).