Perfil de memória no Unity
Ao traçar o perfil e aprimorar o desempenho do seu jogo para uma ampla variedade de plataformas e dispositivos, você pode expandir sua base de jogadores e aumentar suas chances de sucesso.
Esta página fornece informações sobre duas ferramentas para analisar o uso de memória em seu aplicativo no Unity: o módulo integrado Memory Profilere o pacote Memory Profiler, um pacote Unity que você pode adicionar ao seu projeto.
As informações aqui foram extraídas do e-book Ultimate guide to profiling games Unity,disponível para download gratuito. O e-book foi criado por especialistas externos e internos da Unity em desenvolvimento, criação de perfil e otimização de jogos.
Continue lendo para aprender sobre o perfil de memória no Unity.
O perfil de memória é útil para testar as limitações de memória da plataforma de hardware, diminuindo o tempo de carregamento e travamentos e tornando seu projeto compatível com dispositivos mais antigos. Também pode ser relevante se você deseja melhorar o desempenho da CPU/GPU fazendo alterações que realmente aumentem o uso da memória. Em grande parte, não está relacionado ao desempenho do tempo de execução.
Existem duas maneiras de analisar o uso de memória em seu aplicativo no Unity.
O módulo Memory Profiler: Este é um módulo de perfil integrado que fornece informações básicas sobre onde seu aplicativo usa memória.
O pacote Profiler de memória Este é um pacote Unity que você pode adicionar ao seu projeto. Ele adiciona uma janela adicional do Memory Profiler ao Unity Editor, que você pode usar para analisar o uso de memória em seu aplicativo com ainda mais detalhes. Você pode armazenar e comparar snapshots para encontrar fugas de memória ou analisar o layout de memória para identificar problemas de fragmentação.
Com essas ferramentas integradas, você pode monitorar o uso da memória, localizar áreas de um aplicativo onde o uso da memória é maior que o esperado e encontrar e melhorar a fragmentação da memória.
Compreender e orçamentar as limitações de memória dos seus dispositivos alvo é fundamental para o desenvolvimento multiplataforma. Ao projetar cenas e níveis, siga o orçamento de memória definido para cada dispositivo de destino. Ao definir limites e diretrizes, você pode garantir que seu aplicativo funcione bem dentro dos limites das especificações de hardware de cada plataforma.
Você pode encontrar especificações de memória do dispositivo na documentação do desenvolvedor. Por exemplo, o console Xbox One está limitado a 5 GB de memória máxima disponível para jogos rodando em primeiro plano, de acordo com a documentação.
Também pode ser útil definir orçamentos de conteúdo em torno da complexidade de malha e sombreador, bem como para compactação de textura. Tudo isso influencia a quantidade de memória alocada. Estes valores orçamentais podem ser consultados durante o ciclo de desenvolvimento do projecto.
Determinar limites de RAM física
Cada plataforma de destino tem um limite de memória e, quando você souber disso, poderá definir um orçamento de memória para seu aplicativo. Use o Memory Profiler para ver um instantâneo de captura. Os Recursos de Hardware (veja a imagem acima) mostram os tamanhos da Memória Física de Acesso Aleatório (RAM) e da Memória de Acesso Aleatório de Vídeo (VRAM). Este número não leva em conta o fato de que nem todo esse espaço pode estar disponível para uso. No entanto, ele fornece um valor aproximado útil para começar a trabalhar.
É uma boa ideia cruzar as especificações de hardware das plataformas alvo, pois os números exibidos aqui nem sempre mostram o quadro completo. Às vezes, o hardware do kit do desenvolvedor tem mais memória ou você pode estar trabalhando com hardware que possui uma arquitetura de memória unificada.
Identifique o hardware com a especificação mais baixa em termos de RAM para cada plataforma que você suporta e use isso para orientar sua decisão de orçamento de memória. Lembre-se de que nem toda a memória física pode estar disponível para uso. Por exemplo, um console pode ter um hipervisor em execução para suportar jogos mais antigos que podem usar parte da memória total. Pense em uma porcentagem (por exemplo, 80% do total) a ser utilizada. Para plataformas móveis, você também pode considerar a divisão em vários níveis de especificações para oferecer suporte a melhor qualidade e recursos para aqueles com dispositivos de última geração.
Depois de definir um orçamento de memória, considere definir orçamentos de memória por equipe. Por exemplo, os artistas do ambiente recebem uma certa quantidade de memória para usar em cada nível ou cena carregada, a equipe de áudio obtém alocação de memória para música e efeitos sonoros e assim por diante.
É importante ser flexível com os orçamentos à medida que o projeto avança. Se uma equipe ficar abaixo do orçamento, atribua o excedente a outra equipe se ela puder melhorar as áreas do jogo que estão desenvolvendo.
Depois de decidir e definir os orçamentos de memória para suas plataformas de destino, a próxima etapa é usar ferramentas de criação de perfil para ajudá-lo a monitorar e rastrear o uso de memória em seu jogo.
O módulo Memory Profiler fornece duas visualizações: Simples e detalhado. Use a visualização Simples para obter uma visualização de alto nível do uso de memória do seu aplicativo. Quando necessário, alterne para a visualização detalhada para detalhar ainda mais.
Simples
O valor da memória total reservada é o “Total rastreado pela memória do Unity”. Inclui memória que o Unity reservou, mas não está usando no momento (esse número é o total de memória usada).
O valor da memória usada pelo sistema é o que o sistema operacional considera como estando em uso pelo seu aplicativo. Se esse número exibir 0, esteja ciente de que isso indica que o contador do Profiler não está implementado na plataforma em que você está criando o perfil. Nesse caso, o melhor indicador em que se pode confiar é a memória total reservada. Também é recomendável mudar para uma ferramenta de criação de perfil de plataforma nativa para obter informações detalhadas de memória nesses casos.
Para verificar quanta memória é usada pelo seu executável, DLLs e pela Máquina Virtual Mono, os números da memória quadro a quadro não serão suficientes. Use uma captura instantânea detalhada para investigar esse tipo de quebra de memória.
Observação: A árvore de referência na visualização detalhada do módulo Memory Profiler mostra apenas referências nativas. Referências de objetos de tipos herdados de UnityEngine.Object podem aparecer com o nome de seus shells gerenciados. No entanto, eles podem aparecer apenas porque possuem objetos nativos abaixo deles. Você não verá necessariamente nenhum tipo gerenciado. Tomemos como exemplo um objeto que tem um Texture2Din em um de seus campos como referência. Usando esta visualização, você também não verá qual campo contém essa referência. Para esse tipo de detalhe, use o pacote Memory Profiler.
Para determinar em alto nível quando o uso de memória começa a se aproximar dos orçamentos da plataforma, use o seguinte cálculo “no verso do guardanapo”:
Memória usada pelo sistema (ou memória total reservada se o sistema usado mostrar 0) + buffer aproximado de memória não rastreada/memória total da plataforma
Quando esse número começar a se aproximar de 100% do orçamento de memória da sua plataforma, use o pacote Memory Profiler para descobrir o porquê.
Muitos dos recursos do módulo Memory Profiler foram substituídos pelo pacote Memory Profiler, mas você ainda pode usar o módulo para complementar seus esforços de análise de memória.
Por exemplo:
- Para detectar alocações de GC: Embora apareçam no módulo, são mais fáceis de rastrear usando Project Auditor ou Deep Profiling.
- Para ver rapidamente o tamanho usado/reservado do heap
- Análise de memória de shader
Lembre-se de criar o perfil do dispositivo que possui as especificações mais baixas para sua plataforma de destino geral ao definir um orçamento de memória. Monitore de perto o uso da memória, tendo em mente os limites desejados.
Geralmente, você desejará criar o perfil usando um sistema de desenvolvedor poderoso com muita memória disponível (é importante espaço para armazenar grandes instantâneos de memória ou carregar e salvar esses instantâneos rapidamente).
O perfil de memória é uma fera diferente em comparação com o perfil de CPU e GPU, pois pode incorrer em sobrecarga adicional de memória. Talvez seja necessário criar um perfil de memória em dispositivos mais sofisticados (com mais memória), mas esteja atento especificamente ao limite de orçamento de memória para a especificação de destino inferior.
Pontos a serem considerados ao criar perfil de uso de memória:
- Configurações como níveis de qualidade, níveis gráficos e variantes do AssetBundle podem ter uso de memória diferente em dispositivos mais potentes. Por exemplo:
- As configurações de nível de qualidade e gráficos podem afetar o tamanho das RenderTextures usadas para mapas de sombras.
- O dimensionamento da resolução pode afetar o tamanho dos buffers de tela, RenderTextures e efeitos de pós-processamento.
- A configuração da qualidade da textura pode afetar o tamanho de todas as texturas.
- O LOD máximo pode afetar modelos e muito mais.
- Se você tiver variantes do AssetBundle, como uma versão HD (alta definição) e uma versão SD (definição padrão) e escolher qual delas usar com base nas especificações do dispositivo, você também poderá obter tamanhos de ativos diferentes com base no dispositivo em que está criando o perfil.
- A resolução da tela do seu dispositivo de destino afetará o tamanho das RenderTextures usadas para efeitos de pós-processamento.
- A API gráfica compatível de um dispositivo pode afetar o tamanho dos sombreadores com base em quais variantes deles são suportadas ou não pela API.
- Ter um sistema em camadas que usa diferentes configurações de qualidade, configurações de nível gráfico e variações de Asset Bundle é uma ótima maneira de atingir uma gama mais ampla de dispositivos, por exemplo, carregando uma versão de alta definição de um AssetBundle em um dispositivo móvel de 4 GB. e uma versão de definição padrão em um dispositivo de 2 GB. No entanto, leve em consideração as variações acima no uso de memória e teste ambos os tipos de dispositivos, bem como dispositivos com diferentes resoluções de tela ou APIs gráficas suportadas.
Observação: O Editor do Unity geralmente sempre mostrará um consumo de memória maior devido a objetos adicionais carregados do Editor e do Profiler. Pode até mostrar Memória de Ativos que não seriam carregados na memória em uma compilação, como de Asset Bundles (dependendo do modo de simulação de Endereçáveis) ou Sprites e Atlases, ou para Ativos mostrados no Inspetor. Algumas das cadeias de referência também podem ser mais confusas no Editor.
O Memory Profiler está atualmente em versão prévia para Unity 2019 LTS ou mais recente, mas espera-se que seja verificado no Unity 2022 LTS.
Um grande benefício do pacote Memory Profiler é que, além de capturar objetos nativos (como o módulo Memory Profiler faz), ele também permite visualizar a memória gerenciada, salvar e comparar instantâneos e explorar o conteúdo da memória com ainda mais detalhes, com análises visuais do uso da memória.
Um instantâneo mostra as alocações de memória no mecanismo, permitindo identificar rapidamente as causas do uso excessivo ou desnecessário de memória, rastrear vazamentos de memória ou ver a fragmentação de heap.
Depois de instalar o pacote Memory Profiler, abra-o clicando em Window > Analysis > Memory Profiler.
A barra de menu superior do Memory Profiler permite alterar o alvo de seleção do jogador e capturar ou importar instantâneos.
Observação: Crie um perfil de memória no hardware de destino conectando o Memory Profiler ao dispositivo remoto com o menu suspenso Seleção de destino. A criação de perfil no Unity Editor fornecerá números imprecisos devido a despesas gerais adicionadas pelo Editor e outras ferramentas.
À esquerda da janela do Memory Profiler está a área de trabalho. Use isto para gerenciar e abrir ou fechar instantâneos de memória salvos. Você também pode usar esta área para alternar entre as visualizações de instantâneos únicos e de comparação.
Semelhante ao Profile Analyzer, o Memory Profiler permite carregar dois conjuntos de dados (instantâneos de memória) para compará-los. Isso é especialmente útil ao observar como o uso da memória cresceu ao longo do tempo ou entre cenas e ao procurar vazamentos de memória.
O Memory Profiler possui várias guias na janela principal que permitem explorar instantâneos de memória, incluindo Resumo, Objetos e Alocações e Fragmentação. Vejamos cada uma dessas opções em detalhes.
Escolha esta visualização quando quiser obter uma visão geral rápida do uso de memória de um projeto. Ele também contém figuras úteis e importantes relacionadas à memória para o instantâneo de memória capturado em questão. É perfeito para uma rápida olhada no que está acontecendo no momento em que um instantâneo foi tirado.
A visualização Mapa em Árvore exibe um detalhamento da memória usada pelos Objetos como um Mapa em Árvore gráfico que você pode detalhar para descobrir o tipo de Objeto que consome mais memória.
Abaixo da visualização do Mapa em Árvore há uma tabela filtrada que é atualizada para exibir a lista de objetos nas células da grade selecionadas.
O Tree Map mostra a memória atribuída aos Objetos, sejam Nativos ou Gerenciados. A memória de objetos gerenciados tende a ser ofuscada pela memória de objetos nativos, tornando mais difícil identificá-la na visualização do mapa. Você pode ampliar o mapa em árvore para visualizá-los, mas para inspecionar objetos menores, as tabelas geralmente fornecem uma visão geral melhor. Clicar nas células do Mapa em Árvore filtrará a tabela abaixo dela para o tipo de seção e/ou selecionará o objeto específico de interesse na tabela.
Você pode rastrear quais itens fazem referência a objetos nesta lista e possivelmente em quais campos de classe gerenciados essas referências residem selecionando a linha da tabela ou a célula da grade do mapa de árvore que a representa e, em seguida, verificando a seção Referências no painel lateral Detalhes. Se o lado estiver oculto, você poderá torná-lo visível por meio de um botão de alternância na parte superior direita da barra de ferramentas da janela.
Observação: O Tree Map mostra apenas objetos na memória. Não é uma representação completa da memória rastreada. É importante entender isso caso você perceba que os números da visão geral do uso da memória não são iguais ao total da memória rastreada.
Isso resulta do fato de que nem toda memória nativa está vinculada a Objetos. Também pode consistir em alocações nativas não associadas a objetos, como executáveis e DLLs, NativeArrays e assim por diante. Conceitos ainda mais abstratos, como “espaço de memória reservado, mas não utilizado”, podem influenciar o total de alocações nativas.
A visualização Objetos e Alocações mostra uma tabela que pode ser alternada para filtrar com base em seleções prontas, como Todos os Objetos, Todos os Objetos Nativos, Todos os Objetos Gerenciados, Todas as Alocações Nativas e muito mais.
Você pode alternar a tabela inferior para exibir os objetos, as alocações ou as regiões de memória no intervalo selecionado. Conforme observado na visualização do mapa em árvore, nem toda a memória está associada a objetos, portanto, as páginas Todas as regiões de memória e Todas as alocações nativas podem fornecer uma imagem mais completa do uso da memória, onde as regiões de memória também incluem memória reservada, mas não utilizada no momento.
Use isso a seu favor ao otimizar o uso da memória e ao tentar compactar a memória com mais eficiência para plataformas de hardware onde os orçamentos de memória são limitados.
Carregue um instantâneo do Memory Profiler e passe pela visualização Tree Map para inspecionar as categorias, ordenadas da maior para a menor em tamanho de consumo de memória.
Os ativos do projeto costumam ser os que mais consomem memória. Usando a visualização Tabela, localize objetos de Textura, Malhas, AudioClips, RenderTextures, Shaders e buffers pré-alocados. Todos esses são bons candidatos para otimização de memória.
Um vazamento de memória normalmente acontece quando:
- Um objeto não é liberado manualmente da memória através do código
- Um objeto permanece na memória devido a uma referência não intencional
O modo Comparação do Memory Profiler pode ajudar a encontrar vazamentos de memória comparando dois instantâneos em um período específico.
Um cenário comum de vazamento de memória em jogos Unity pode ocorrer após o descarregamento de uma cena.
O pacote Memory Profiler possui um fluxo de trabalho que orienta você no processo de descoberta desses tipos de vazamentos usando o modo Comparar.
Através da comparação diferencial de vários instantâneos de memória, você pode identificar a origem das alocações contínuas de memória durante a vida útil do aplicativo.
As seções a seguir listam algumas dicas para ajudar a identificar alocações de heap gerenciadas em seus projetos.
O módulo Memory Profiler no Unity Profiler representa alocações gerenciadas por quadro com uma linha vermelha. Deve ser 0 na maioria das vezes, portanto, quaisquer picos nessa linha indicam quadros que você deve investigar para alocações gerenciadas.
A visualização Timeline no módulo CPU Usage Profiler mostra as alocações, incluindo as gerenciadas, em rosa, tornando-as fáceis de ver e aprimorar.
As pilhas de chamadas de alocação fornecem uma maneira rápida de descobrir alocações de memória gerenciada em seu código. Eles fornecerão os detalhes da pilha de chamadas de que você precisa com menos sobrecarga em comparação com o que o perfil profundo normalmente adicionaria, e podem ser habilitados instantaneamente usando o Profiler padrão.
As pilhas de chamadas de alocação estão desabilitadas por padrão no Profiler. Para habilitá-los, clique no botão Call Stacks na barra de ferramentas principal da janela Profiler. Altere a visualização de detalhes para dados relacionados.
Observação: Se você estiver usando uma versão mais antiga do Unity (antes do suporte à pilha de chamadas de alocação), a criação de perfil profundo é uma boa maneira de obter pilhas de chamadas completas para ajudar a encontrar alocações gerenciadas.
As amostras GC.Alloc selecionadas na Hierarquia ou na Hierarquia Bruta agora conterão suas pilhas de chamadas. Você também pode ver as pilhas de chamadas de amostras GC.Alloc na dica de ferramenta de seleção na Linha do tempo.
A visualização Hierarquia no CPU Usage Profiler permite clicar nos cabeçalhos das colunas para usá-los como critérios de classificação. Classificar por GC Alloc é uma ótima maneira de focar neles.
Project Auditor é uma ferramenta experimental de análise estática. Ele faz muitas coisas úteis, muitas das quais estão fora do escopo deste guia, mas pode produzir uma lista de cada linha de código em um projeto que causa uma alocação gerenciada, sem precisar executar o projeto. É uma maneira muito eficiente de encontrar e investigar esse tipo de problema.
O Unity usa o coletor de lixo Boehm-Demers-Weiser, que interrompe a execução do código do seu programa e só retoma a execução normal quando seu trabalho for concluído.
Esteja ciente das alocações desnecessárias de heap que podem causar picos de GC.
- Cordas: Em C#, strings são tipos de referência, não tipos de valor. Isso significa que cada nova string será alocada no heap gerenciado, mesmo que seja usada apenas temporariamente. Reduza a criação ou manipulação desnecessária de strings. Evite analisar arquivos de dados baseados em strings, como JSON e XML, e armazene dados em ScriptableObjects ou formatos como MessagePack ou Protobuf. Use a classe StringBuilder se precisar construir strings em tempo de execução.
- Chamadas de função de unidade: Algumas funções da API Unity criam alocações de heap, especialmente aquelas que retornam uma matriz de objetos gerenciados. Armazene referências em cache a matrizes em vez de alocá-las no meio de um loop. Além disso, aproveite algumas funções que evitam a geração de lixo. Por exemplo, use GameObject.CompareTag em vez de comparar manualmente uma string com GameObject.tag (pois retornar uma nova string cria lixo).
- Boxe: Evite passar uma variável digitada por valor no lugar de uma variável digitada por referência. Isso cria um objeto temporário, e o lixo potencial que vem com ele converte implicitamente o tipo de valor em um objeto de tipo (por exemplo, int i = 123; object o = i). Em vez disso, tente fornecer substituições concretas com o tipo de valor que você deseja transmitir. Genéricos também podem ser usados para essas substituições.
- Corrotinas: Embora o rendimento não produza lixo, a criação de um novo objeto WaitForSeconds produz. Armazene em cache e reutilize o objeto WaitForSeconds em vez de criá-lo na linha de rendimento ou use rendimento retorno nulo.
- LINQ e expressões regulares: Ambos geram lixo no boxe nos bastidores. Evite LINQ e expressões regulares se o desempenho for um problema. Escreva loops for e use listas como alternativa à criação de novos arrays.
- Coleções genéricas e outros tipos gerenciados: Não declare e preencha uma lista ou coleção em cada quadro da atualização (por exemplo, uma lista de inimigos dentro de um determinado raio do jogador). Em vez disso, torne List um membro do MonoBehaviour e inicialize-o em Start. Basta esvaziar a coleção com Limpar todos os quadros antes de usá-la.
Cronometre a coleta de lixo sempre que possível
Se tiver certeza de que o congelamento da coleta de lixo não afetará um ponto específico do jogo, você pode acionar a coleta de lixo com System.GC.Collect.
Consulte Compreendendo o gerenciamento automático de memória para obter exemplos de como usar isso a seu favor.
Use o coletor de lixo incremental para dividir a carga de trabalho do GC
Em vez de criar uma única e longa interrupção durante a execução do programa, a coleta de lixo incremental usa múltiplas interrupções mais curtas que distribuem a carga de trabalho por vários quadros. Se a coleta de lixo estiver causando uma taxa de quadros irregular, tente esta opção para ver se ela pode reduzir o problema de picos de GC. Use o Profile Analyzer para verificar seus benefícios para seu aplicativo.
Observe que usar o GC no modo Incremental adiciona barreiras de leitura e gravação a algumas chamadas C#, o que vem com alguma sobrecarga que pode adicionar até aproximadamente 1 ms por quadro de sobrecarga de chamada de script. Para um desempenho ideal, é ideal não ter GC Allocs nos principais loops de jogo, para que você não precise do GC Incremental para obter uma taxa de quadros suave e possa ocultar o GC. Colete onde um usuário não notará, por exemplo, quando abrindo o menu ou carregando um novo nível.
Para saber mais sobre o Memory Profiler, verifique os seguintes recursos:
- Documentação do Memory Profiler
- Melhore o uso de memória com o tutorial Memory Profiler no Unity
- Profiler de memória A ferramenta para solucionar problemas relacionados à memória Sessão de união
- Trabalhando com a sessão do Memory Profiler Unity Learn
Baixe gratuitamente o e-book Guia definitivo para criar perfis de jogos Unitypara obter todas as dicas e práticas recomendadas.