Como a otimização guiada por perfil (PGO) funciona

A otimização guiada por perfil (PGO, na sigla em inglês, também conhecida como "pogo") é uma forma de otimizar ainda mais os builds do seu jogo usando informações sobre o comportamento dele. Assim, um código executado com pouca frequência, como em erros ou casos extremos, perde ênfase nos caminhos críticos de execução, deixando o jogo mais rápido.

Diagrama com uma visão geral do funcionamento da
PGO

Figura 1. Visão geral do funcionamento da PGO.

Se quiser usar a PGO, instrumente seu build para gerar dados de perfil com os quais o compilador possa trabalhar. Em seguida, use o código executando esse build e gerando um ou mais arquivos de dados do perfil. Por fim, copie esses arquivos do dispositivo e use-os com o compilador para otimizar seu executável utilizando as informações de perfil coletadas.

Como os builds otimizados sem PGO funcionam

Um build otimizado sem dados de perfil usa várias heurísticas para decidir como gerar o código.

Alguns são sinalizados explicitamente pelo desenvolvedor. Por exemplo, no C++ 20 ou mais recente, isso é feito usando dicas de direção de ramificação, como [[likely]] e [[unlikely]]. Outro exemplo seria usar a palavra-chave inline ou até mesmo __forceinline, embora seja melhor e mais flexível continuar com a primeira. Por padrão, alguns compiladores presumem que o primeiro trecho de uma ramificação, ou seja, a instrução if, é o mais provável, e não a parte else. O otimizador também pode fazer suposições pela análise estática do código sobre como ele será executado, mas isso costuma ter um escopo limitado.

O problema dessas heurísticas é que elas não conseguem ajudar corretamente o compilador em todas as situações, mesmo com uma marcação manual exaustiva. Portanto, embora o código gerado seja bem otimizado, ele não é tão bom quanto poderia ser se o compilador tivesse mais informações sobre o comportamento dele no momento da execução.

Como gerar um perfil

Ao criar o executável com a PGO ativada no modo instrumentado, ele é aumentado com código no início de cada bloco, como no começo de uma função ou de cada parte de uma ramificação. Esse código é usado para monitorar a contagem de todas as vezes que ocorre a inserção no bloco, que o compilador pode usar mais tarde para gerar um código otimizado.

Outros monitoramentos também são realizados, como o tamanho de operações de cópia típicas em um bloco, para que versões rápidas inline da operação possam ser geradas mais tarde.

Depois que o jogo realiza algum tipo de trabalho representativo, o executável precisa chamar uma função (__llvm_profile_write_file()) para gravar os dados do perfil em um local personalizável no dispositivo. Essa função é vinculada ao jogo automaticamente quando a configuração do build tem a instrumentação de PGO ativada.

O arquivo de dados do perfil gravado precisa ser copiado para o computador host e, de preferência, mantido em um local com outros perfis do mesmo build para que possam ser usados juntos.

Por exemplo, você pode modificar o código do jogo para chamar __llvm_profile_write_file() quando a cena atual dele terminar. Em seguida, você precisa criar seu jogo com a instrumentação ativada e implantá-lo no dispositivo Android. Durante a execução, os dados do perfil são coletados automaticamente. O engenheiro de controle de qualidade executa o jogo, conferindo diferentes cenários, ou apenas fazendo os testes normais.

Quando terminar de conferir diferentes partes do jogo, você poderá retornar ao menu principal, o que encerraria a cena atual e gravaria os dados do perfil.

Um script pode ser usado para copiar os dados do perfil do dispositivo de teste e fazer upload deles para um repositório central em que eles possam ser capturados para uso futuro.

Como mesclar dados de perfil

Depois que um perfil é recebido de um dispositivo, ele precisa ser convertido do arquivo de dados de perfil gerado pelo build instrumentado para um formato que o compilador possa consumir. A AGDE faz isso automaticamente para todos os arquivos de dados de perfil que você adicionar ao seu projeto.

A PGO foi projetada para combinar os resultados de várias execuções de perfil instrumentado. A AGDE também faz isso automaticamente caso você tenha vários arquivos em um único projeto.

Para mostrar o quanto a mesclagem de conjuntos de dados de perfil pode ser útil, digamos que você tenha um laboratório com engenheiros de controle de qualidade que passam por diferentes níveis do seu jogo. Cada sessão de jogo é gravada e usada para gerar dados de perfil com base em um build instrumentado pela PGO. A mesclagem de perfis permite combinar os resultados de todas essas execuções de teste diferentes, que podem executar partes muito distintas do código, para melhorar os resultados.

Além disso, ao realizar testes longitudinais, em que você mantém cópias dos dados de perfil de cada versão interna, a recriação não invalida necessariamente os dados de perfil antigos. A maior parte do código é relativamente estável de uma versão para a outra. Portanto, os dados de perfil de builds mais antigos ainda podem ser úteis e não ficam desatualizados imediatamente.

Como gerar builds com a otimização guiada por perfil

Depois que você adicionar os dados de perfil ao projeto, eles poderão ser usados para criar o executável. Basta ativar a PGO no modo de otimização na configuração do build.

Isso orienta o otimizador do compilador a usar os dados de perfil coletados anteriormente ao tomar decisões de otimização.

Quando usar a otimização guiada por perfil

A PGO não é algo que você ativa no início do desenvolvimento ou durante a iteração diária no código. Durante o desenvolvimento, foque em otimizações baseadas em algoritmos e layout de dados, porque elas oferecem benefícios muito maiores.

A PGO entra mais tarde no processo de desenvolvimento, quando você precisa aperfeiçoar o jogo para o lançamento. Pense na otimização guiada por perfil como um toque final no desempenho depois que você já tiver trabalhado ao máximo no código.

Melhoria de desempenho esperada com a PGO

Isso depende de muitos fatores, incluindo o nível de abrangência e desatualização dos perfis e quão perto você estaria de um código ideal com um build otimizado tradicional.

No geral, uma estimativa bem conservadora é que os custos de CPU sejam reduzidos em cerca de 5% nas linhas de execução principais. Você pode ter resultados diferentes.

Overhead de instrumentação

A instrumentação da PGO é abrangente e, embora seja gerada de forma automática, tem custos. O overhead com a instrumentação da PGO pode variar dependendo da sua base de código.

Custo de desempenho da instrumentação guiada por perfil

É possível que haja uma queda no frame rate com builds instrumentados. Em alguns casos, dependendo do uso da CPU durante a operação normal, essa queda pode dificultar a jogabilidade.

Recomendamos que a maioria dos desenvolvedores crie um modo de repetição semideterminista para o jogo. Esse tipo de funcionalidade permite que a equipe de controle de qualidade inicie o jogo em um local de início conhecido e repetível (como um jogo salvo ou nível de teste específico) e registre a entrada. Essa entrada gravada em um build de teste pode ser inserida em um build instrumentado pela PGO, ser reproduzida e gerar dados reais de perfil, seja qual for o tempo necessário para processar um frame individual, mesmo que o jogo fique tão lento que impossibilite a jogabilidade.

Há outros benefícios importantes, como a multiplicação de esforço: um único testador pode gravar a entrada em um dispositivo e depois a reproduzir em vários tipos diferentes de dispositivos para realizar um teste de fumaça.

Um sistema de repetição como esse pode ter enormes benefícios no Android, em que há muitas variantes de dispositivos no ecossistema. E tem mais: ele pode formar uma parte essencial do seu sistema de build de integração contínua, ajudando você com testes de fumaça e de regressão de desempenho feitos de um dia para o outro.

É necessário registrar a entrada do usuário no ponto mais adequado dentro do mecanismo de entrada do jogo. Provavelmente, isso não inclui eventos diretos de tela touchscreen, mas sim a gravação das consequências deles como comandos. Essas entradas também precisam incluir uma contagem de frames que aumenta monotonicamente ao longo do jogo para que, durante a reprodução, o mecanismo de repetição possa aguardar o frame adequado para acionar um evento.

No modo de reprodução, evite login on-line, não mostre anúncios e opere em um intervalo fixo (com o frame rate desejado). Além disso, recomendamos desativar o VSync.

Não é importante que tudo (por exemplo, sistemas de partículas) seja repetido perfeitamente no seu jogo, mas as ações precisam gerar os mesmos resultados e consequências, ou seja, a jogabilidade precisa continuar igual.

Custo de memória da instrumentação guiada por perfil

O overhead de memória da instrumentação da PGO varia muito de acordo com a biblioteca específica que está sendo compilada. Nos testes, notamos um aumento geral de aproximadamente 2,2 vezes no tamanho do executável. Esse aumento incluía o código extra necessário para instrumentar os blocos, assim como o espaço necessário para armazenar os contadores. Os testes não foram completos, então sua experiência pode ser diferente.

Quando atualizar ou descartar os dados do perfil

Atualize os perfis sempre que fizer uma grande mudança no código ou no conteúdo do jogo.

O ambiente de build e o estágio do desenvolvimento indicam se as mudanças são significativas.

Como já mencionado, não carregue os dados do perfil nas principais mudanças do ambiente de build. Embora isso não impeça a criação nem corrompa seu build, os benefícios de desempenho do uso da PGO vão diminuir, já que pouquíssimos dados de perfil serão aplicáveis ao novo ambiente. No entanto, esse não é o único caso em que os dados do perfil podem ficar desatualizados.

Vamos começar supondo que você não usará a PGO até o fim do desenvolvimento ao se preparar para um lançamento, além de talvez coletar uma captura semanal para que os engenheiros de desempenho possam verificar se há problemas inesperados.

Isso muda conforme você se aproxima da janela de lançamento, quando a equipe de controle de qualidade está testando o jogo todos os dias e fazendo uma análise completa dele. Durante essa fase, é possível gerar perfis com esses dados diariamente e usá-los ao informar builds futuros para testar o desempenho e ajustar os seus montantes.

Quando você estiver se preparando para o lançamento, é importante bloquear a versão do build que planeja lançar e, em seguida, executar o controle de qualidade para gerar os novos dados de perfil. Depois, você poderá criar usando esses dados para produzir uma versão final do executável.

O controle de qualidade pode fazer uma execução final desse build otimizado para garantir que ele é uma boa opção de lançamento.