O que você está procurando?
Hero background image

Dicas de perfil de desempenho para desenvolvedores de jogos

Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.

Um desempenho suave é essencial para criar experiências de jogo imersivas para os jogadores. Para garantir que seu jogo esteja otimizado, um fluxo de trabalho de perfil consistente e de ponta a ponta é um "item indispensável" para um desenvolvimento de jogos eficiente, e começa com um simples procedimento de três etapas:

  • Perfil antes de fazer mudanças significativas: Estabeleça uma linha de base.
  • Perfilando durante o desenvolvimento: Acompanhe e garanta que as mudanças não quebrem o desempenho ou os orçamentos.
  • Perfil após: Prove que as mudanças tiveram o efeito desejado.

Esta página descreve um fluxo de trabalho geral de perfil para desenvolvedores de jogos. É extraído do e-book, Guia definitivo para perfilar jogos Unity, disponível para download gratuito (a versão Unity 6 do guia estará disponível em breve). O e-book foi criado por especialistas em desenvolvimento de jogos, perfil e otimização, tanto externos quanto internos da Unity.

Neste artigo, você pode aprender sobre metas úteis a serem definidas com o perfil, gargalos de desempenho comuns, como estar limitado pela CPU ou pela GPU, e como identificar e investigar essas situações em mais detalhes.

Gráfico da taxa de quadros de um FPS

Defina um orçamento de quadros

Os jogadores costumam medir o desempenho usando a taxa de quadros, ou quadros por segundo (fps), mas como desenvolvedor, geralmente é recomendado usar tempo de quadro em milissegundos em vez disso. Considere o seguinte cenário simplificado:

Durante a execução, seu jogo renderiza 59 quadros em 0,75 segundos. No entanto, o próximo quadro leva 0,25 segundos para ser renderizado. A taxa média de quadros entregue de 60 fps parece boa, mas na realidade os jogadores notarão um efeito de gagueira, já que o último quadro leva um quarto de segundo para ser renderizado.

Esta é uma das razões pelas quais é importante ter como objetivo um orçamento de tempo específico por quadro. Isso fornece a você uma meta sólida para trabalhar ao perfilar e otimizar seu jogo e, em última análise, cria uma experiência mais suave e consistente para seus jogadores.

Cada quadro terá um orçamento de tempo baseado em seu fps alvo. Um aplicativo com alvo de 30 fps deve sempre levar menos de 33,33 ms por quadro (1000 ms / 30 fps). Da mesma forma, um alvo de 60 fps deixa 16,66 ms por quadro (1000 ms / 60 fps).

Você pode exceder esse orçamento durante sequências não interativas, por exemplo, ao exibir menus de UI ou carregamento de cena, mas não durante o jogo. Mesmo um único quadro que excede o orçamento de quadro alvo causará travamentos.

Nota: Uma taxa de quadros consistentemente alta em jogos de VR é essencial para evitar causar náusea ou desconforto aos jogadores e é frequentemente necessária para que seu jogo obtenha certificação do detentor da plataforma.

Quadros por segundo: Uma métrica enganosa

Uma maneira comum que os jogadores medem o desempenho é com a taxa de quadros, ou quadros por segundo. No entanto, é recomendado que você use o tempo de quadro em milissegundos em vez disso. Para entender por que, olhe para o gráfico acima de fps versus tempo de quadro.

Considere esses números:

1000 ms/sec / 900 fps = 1,111 ms por quadro

1000 ms/seg / 450 fps = 2.222 ms por quadro

1000 ms/seg / 60 fps = 16.666 ms por quadro

1000 ms/seg / 56.25 fps = 17.777 ms por quadro

Se sua aplicação estiver rodando a 900 fps, isso se traduz em um tempo de quadro de 1.111 milissegundos por quadro. A 450 fps, isso é 2.222 milissegundos por quadro. Isso representa uma diferença de apenas 1.111 milissegundos por quadro, mesmo que a taxa de quadros pareça cair pela metade.

Se você olhar as diferenças entre 60 fps e 56.25 fps, isso se traduz em 16.666 milissegundos por quadro e 17.777 milissegundos por quadro, respectivamente. Isso também representa 1.111 milissegundos extras por quadro, mas aqui, a queda na taxa de quadros parece muito menos dramática em termos percentuais.

É por isso que os desenvolvedores usam o tempo médio de quadro para medir a velocidade do jogo em vez de fps.

Não se preocupe com fps a menos que você caia abaixo da sua taxa de quadros alvo. Concentre-se no tempo de quadro para medir quão rápido seu jogo está rodando, e então mantenha-se dentro do seu orçamento de quadros.

Leia o artigo original, “Robert Dunlop’s fps versus frame time,” para mais informações.

Desafios para dispositivos móveis

Desafios para dispositivos móveis

O controle térmico é uma das áreas mais importantes a otimizar ao desenvolver aplicações para dispositivos móveis. Se a CPU ou GPU gastarem muito tempo trabalhando em plena carga devido a código ineficiente, esses chips ficarão quentes. Para evitar superaquecimento e danos potenciais aos chips, o sistema operacional reduzirá a velocidade do clock do dispositivo para permitir que ele esfrie, causando gagueira nos quadros e uma experiência de usuário ruim. Essa redução de desempenho é conhecida como estrangulamento térmico.

Taxas de quadros mais altas e aumento na execução de código (ou operações de acesso a DRAM) levam a um aumento no consumo de bateria e geração de calor. Um desempenho ruim também pode tornar seu jogo injogável para segmentos inteiros de dispositivos móveis de baixo desempenho, o que pode levar a oportunidades de mercado perdidas.

Ao lidar com o problema térmico, considere o orçamento que você tem para trabalhar como um orçamento de sistema.

Combata o estrangulamento térmico e o consumo de bateria perfilando cedo para otimizar seu jogo desde o início. Ajuste as configurações do seu projeto para o hardware da plataforma alvo para combater problemas de aquecimento e drenagem da bateria.

Ajuste os orçamentos de quadros em dispositivos móveis

Uma dica geral para combater problemas térmicos do dispositivo durante longos períodos de jogo é deixar um tempo ocioso de quadro de cerca de 35%. Isso dá tempo para os chips móveis esfriarem e ajuda a prevenir a drenagem excessiva da bateria. Usando um tempo de quadro alvo de 33,33 ms por quadro (para 30 fps), o orçamento de quadro para dispositivos móveis será de aproximadamente 22 ms por quadro.

O cálculo é assim: (1000 ms / 30) * 0.65 = 21.66 ms

Para alcançar 60 fps em dispositivos móveis usando o mesmo cálculo, seria necessário um tempo de quadro alvo de (1000 ms / 60) * 0.65 = 10.83 ms. Isso é difícil de alcançar em muitos dispositivos móveis e drenaria a bateria duas vezes mais rápido do que visando 30 fps. Por essas razões, muitos jogos móveis visam 30 fps em vez de 60. Use Application.targetFrameRate para controlar essa configuração e consulte a seção "Defina um orçamento de quadro" no e-book de perfilagem para mais detalhes sobre o tempo de quadro.

A escalabilidade de frequência em chips móveis pode dificultar a identificação das alocações do seu orçamento de tempo ocioso de quadro ao fazer a perfilagem. Suas melhorias e otimizações podem ter um efeito positivo líquido, mas o dispositivo móvel pode estar reduzindo a frequência e, como resultado, funcionando mais frio. Use ferramentas personalizadas como FTrace ou Perfetto para monitorar as frequências dos chips móveis, o tempo ocioso e a escalabilidade antes e depois das otimizações.

Desde que você permaneça dentro do seu orçamento total de tempo de quadro para o seu fps alvo (digamos 33,33 ms para 30 fps) e veja seu dispositivo trabalhando menos ou registrando temperaturas mais baixas para manter essa taxa de quadros, então você está no caminho certo.

Outra razão para adicionar margem ao orçamento de quadros em dispositivos móveis é levar em conta as flutuações de temperatura do mundo real. Em um dia quente, um dispositivo móvel aquecerá e terá dificuldade em dissipar calor, o que pode levar ao estrangulamento térmico e ao desempenho ruim do jogo. Reserve uma porcentagem do orçamento de quadros para ajudar a evitar esse cenário.

Reduza as operações de acesso à memória

Reduza as operações de acesso à memória

O acesso à DRAM é tipicamente uma operação que consome muita energia em dispositivos móveis. O conselho de otimização da Arm para conteúdo gráfico em dispositivos móveis diz que o acesso à memória LPDDR4 custa aproximadamente 100 picojoules por byte.

Reduzir o número de operações de acesso à memória por quadro através de:

  • Reduzindo a taxa de quadros
  • Reduzindo a resolução de exibição sempre que possível
  • Usando malhas mais simples com contagem de vértices reduzida e precisão de atributos
  • Usando compressão de textura e mipmapping

Quando você precisa se concentrar em dispositivos que utilizam hardware de CPU ou GPU Arm, a ferramenta Arm Performance Studio (especificamente, Streamline Performance Analyzer) inclui alguns ótimos contadores de desempenho para identificar problemas de largura de banda de memória. Os contadores disponíveis estão listados e explicados para cada geração de GPU Arm em um guia do usuário correspondente, por exemplo, Mali-G710 Performance Counter Reference Guide . Observe que a profilagem de GPU do Arm Performance Studio requer uma GPU Arm Immortalis ou Mali.

Estabelecer níveis de hardware para benchmarking

Além de usar ferramentas de perfilagem específicas da plataforma, estabeleça níveis ou um dispositivo de especificação mais baixa para cada plataforma e nível de qualidade que você deseja suportar, em seguida, perfil e otimize o desempenho para cada uma dessas especificações.

Como exemplo, se você está direcionando plataformas móveis, pode decidir suportar três níveis com controles de qualidade que ativam ou desativam recursos com base no hardware alvo. Você então otimiza para a especificação do dispositivo mais baixa em cada nível. Como outro exemplo, se você está desenvolvendo um jogo para consoles, certifique-se de fazer a profilagem em versões mais antigas e mais novas.

Nosso último guia de otimização para dispositivos móveis tem muitas dicas e truques que ajudarão você a reduzir o estrangulamento térmico e aumentar a vida útil da bateria para dispositivos móveis que executam seus jogos.

Do perfil de alto a baixo nível

Ao fazer a profilagem, você quer garantir que está concentrando seu tempo e esforço em áreas onde pode criar o maior impacto. Assim, é recomendado começar com uma abordagem de cima para baixo ao fazer a profilagem, o que significa que você começa com uma visão geral de alto nível de categorias como renderização, scripts, física e alocações de coleta de lixo (GC). Uma vez que você tenha identificado áreas de preocupação, pode aprofundar-se nos detalhes mais profundos. Use esta passagem de alto nível para coletar dados e fazer anotações sobre os problemas de desempenho mais críticos, incluindo cenários que causam alocações gerenciadas indesejadas ou uso excessivo de CPU em seu loop de jogo principal.

Você precisará primeiro reunir pilhas de chamadas para marcadores GC.Alloc. Se você não estiver familiarizado com esse processo, encontre algumas dicas e truques na seção intitulada "Localizando alocações de memória recorrentes ao longo da vida útil do aplicativo" no e-book.

Se as pilhas de chamadas relatadas não forem detalhadas o suficiente para rastrear a origem das alocações ou outras lentidões, você pode realizar uma segunda sessão de perfilagem com a Profilagem Profunda ativada para encontrar a origem das alocações. Cobrimos a perfilagem profunda em mais detalhes no e-book, mas em resumo, é um modo no Profiler que captura dados de desempenho detalhados para cada chamada de função, fornecendo insights granulares sobre tempos de execução e comportamentos, mas com um overhead significativamente maior em comparação com a perfilagem padrão.

Ao coletar notas sobre os "infratores" do tempo de quadro, certifique-se de notar como eles se comparam em relação ao restante do quadro. Esse impacto relativo pode ser distorcido quando a perfilagem profunda está ativada, porque a perfilagem profunda adiciona um overhead significativo ao instrumentar cada chamada de método.

Perfil early

Embora você deva sempre perfilar durante todo o ciclo de desenvolvimento do seu projeto, os ganhos mais significativos da perfilagem são feitos quando você começa nas fases iniciais.

Perfil early e frequentemente para que você e sua equipe entendam e memorizem uma "assinatura de desempenho" para o projeto que podem usar como referência. Se o desempenho despencar, você poderá facilmente identificar quando as coisas dão errado e corrigir o problema.

Embora a perfilagem no Editor lhe dê uma maneira fácil de identificar os principais problemas, os resultados de perfilagem mais precisos sempre vêm de executar e perfilar builds em dispositivos-alvo, juntamente com o uso de ferramentas específicas da plataforma para investigar as características de hardware de cada plataforma. Essa combinação fornecerá uma visão holística do desempenho do aplicativo em todos os seus dispositivos-alvo. Por exemplo, você pode estar limitado pela GPU em alguns dispositivos móveis, mas limitado pela CPU em outros, e você só pode aprender isso medindo nesses dispositivos.

Fluxograma para perfis

Identificar problemas de desempenho

Baixe a versão PDF imprimível deste gráfico aqui.

O objetivo da perfilagem é identificar gargalos como alvos para otimização. Se você confiar em suposições, pode acabar otimizando partes do jogo que não são gargalos, resultando em pouca ou nenhuma melhoria no desempenho geral. Algumas "otimizações" podem até piorar o desempenho geral do seu jogo, enquanto outras podem ser trabalhosas, mas resultar em resultados insignificantes. A chave é otimizar o impacto do seu investimento de tempo focado.

O fluxograma acima ilustra o processo inicial de perfilagem, com as seções seguintes fornecendo informações detalhadas sobre cada etapa. Eles também apresentam capturas do Profiler de projetos reais do Unity para ilustrar os tipos de coisas a serem observadas.

Para obter uma visão holística de toda a atividade da CPU, incluindo quando está aguardando a GPU, use a Visão da linha do tempo no módulo da CPU do Profiler. Familiarize-se com os marcadores comuns do Profiler para interpretar as capturas corretamente. Alguns dos marcadores do Profiler podem aparecer de forma diferente dependendo da sua plataforma alvo, então passe um tempo explorando as capturas do seu jogo em cada uma das suas plataformas alvo para ter uma noção de como é uma captura "normal" para o seu projeto.

O desempenho de um projeto é limitado pelo chip e/ou thread que leva mais tempo. Essa é a área onde os esforços de otimização devem se concentrar. Por exemplo, imagine os seguintes cenários para um jogo com um orçamento de tempo de quadro alvo de 33,33 ms e VSync habilitado:

  • Se o tempo de quadro da CPU (excluindo VSync) é de 25 ms e o tempo da GPU é de 20 ms, sem problemas! Você está limitado pela CPU, mas tudo está dentro do orçamento, e otimizar as coisas não melhorará a taxa de quadros (a menos que você consiga que tanto a CPU quanto a GPU fiquem abaixo de 16,66 ms e suba para 60 fps).
  • Se o tempo de quadro da CPU é de 40 ms e o da GPU é de 20 ms, você está limitado pela CPU e precisará otimizar o desempenho da CPU. Otimizar o desempenho da GPU não ajudará; na verdade, você pode querer mover parte do trabalho da CPU para a GPU, por exemplo, usando shaders de computação em vez de código C# onde aplicável, para equilibrar as coisas.
  • Se o tempo de quadro da CPU é de 20 ms e o da GPU é de 40 ms, você está limitado pela GPU e precisa otimizar o trabalho da GPU.
  • Se a CPU e a GPU estão ambas em 40 ms, você está limitado por ambas e precisará otimizar ambas abaixo de 33,33 ms para alcançar 30 fps.

Veja esses recursos que exploram mais sobre estar limitado pela CPU ou GPU:

Você está dentro do intervalo de processamento de quadros?

Você está dentro do intervalo de processamento de quadros?

Perfilando e otimizando seu projeto cedo e frequentemente durante o desenvolvimento ajudará você a garantir que todas as threads da CPU da sua aplicação e o tempo de quadro geral da GPU estejam dentro do orçamento de quadro. A pergunta que guiará esse processo é, você está dentro do orçamento de quadro ou não?

Acima está uma imagem de uma captura de perfil de um jogo móvel Unity desenvolvido por uma equipe que fez perfilamento e otimização contínuos. O jogo tem como alvo 60 fps em celulares de alta especificação e 30 fps em celulares de especificação média/baixa, como o que está nesta captura.

Note como quase metade do tempo no quadro selecionado é ocupada pelo marcador do Profiler amarelo WaitForTargetFPS. A aplicação definiu Application.targetFrameRate para 30 fps, e VSync está habilitado. O trabalho de processamento real na thread principal termina por volta da marca de 19 ms, e o restante do tempo é gasto esperando o restante dos 33,33 ms passar antes de começar o próximo quadro. Embora este tempo seja representado com um marcador do Profiler, a thread principal da CPU está essencialmente ociosa durante esse tempo, permitindo que a CPU esfrie e use um mínimo de energia da bateria.

O marcador a ser observado pode ser diferente em outras plataformas ou se o VSync estiver desativado. O importante é verificar se a thread principal está rodando dentro do seu orçamento de quadros ou exatamente no seu orçamento de quadros com algum tipo de marcador que indica que a aplicação está aguardando o VSync e se as outras threads têm algum tempo ocioso.

O tempo ocioso é representado por marcadores do Profiler cinzas ou amarelos. A captura de tela acima mostra que a thread de renderização está ociosa em Gfx.WaitForGfxCommandsFromMainThread, o que indica momentos em que terminou de enviar chamadas de desenho para a GPU em um quadro e está aguardando mais solicitações de chamadas de desenho da CPU no próximo. Da mesma forma, embora a thread Job Worker 0 passe algum tempo em Canvas.GeometryJob, a maior parte do tempo está ociosa. Esses são todos sinais de uma aplicação que está confortavelmente dentro do orçamento de quadros.

Se o seu jogo está dentro do orçamento de quadros

Se você estiver dentro do orçamento de quadros, incluindo quaisquer ajustes feitos no orçamento para levar em conta o uso da bateria e o estrangulamento térmico, você terminou as principais tarefas de perfilagem. Você pode concluir executando o Memory Profiler para garantir que a aplicação também esteja dentro do seu orçamento de memória.

A imagem acima mostra um jogo rodando confortavelmente dentro do orçamento de ~22 ms necessário para 30 fps. Observe o WaitForTargetfps preenchendo o tempo da thread principal até o VSync e os tempos ociosos cinzas na thread de renderização e na thread de trabalho. Observe também que o intervalo VBlank pode ser observado olhando os tempos finais de Gfx.Present quadro a quadro, e que você pode traçar uma escala de tempo na área da Linha do Tempo ou na régua de Tempo na parte superior para medir de um desses para o próximo.

Vinculado à CPU

Limitado pela CPU

Se o seu jogo não estiver dentro do orçamento de quadros da CPU, o próximo passo é investigar qual parte da CPU é o gargalo – em outras palavras, qual thread está mais ocupada.

É raro que toda a carga de trabalho da CPU seja o gargalo. CPUs modernas têm um número de núcleos diferentes, capazes de realizar trabalho de forma independente e simultânea. Diferentes threads podem ser executadas em cada núcleo da CPU. Uma aplicação Unity completa usa uma variedade de threads para diferentes propósitos, mas aquelas que são mais comuns para encontrar problemas de desempenho são:

  • A thread principal: É aqui que a maioria da lógica/scripts do jogo realiza seu trabalho por padrão. A maioria dos sistemas Unity, como física, animação, UI e as etapas iniciais de renderização, executam aqui.
  • A thread de renderização: Isso lida com o trabalho de preparação (por exemplo, quais objetos na cena são visíveis para a câmera e quais são excluídos/invisíveis porque estão fora do frustum de visão, ocluídos ou eliminados por outros critérios) que deve acontecer antes de enviar instruções de renderização para a GPU.
  • Durante o processo de renderização, a thread principal examina a cena e realiza a eliminação de câmeras, ordenação de profundidade e agrupamento de chamadas de desenho, resultando em uma lista de coisas a serem renderizadas. Esta lista é passada para a thread de renderização, que a traduz da representação interna agnóstica da plataforma da Unity para as chamadas específicas da API gráfica necessárias para instruir a GPU em uma plataforma particular.
  • As threads de trabalho do Job: Os desenvolvedores podem fazer uso do sistema de jobs para agendar certos tipos de trabalho para serem executados em threads de trabalho, o que reduz a carga de trabalho na thread principal. Alguns dos sistemas e recursos da Unity também fazem uso do sistema de jobs, como física, animação e renderização.

Um exemplo do mundo real de otimização da thread principal

A imagem abaixo mostra como as coisas podem parecer em um projeto que está limitado pela thread principal. Este projeto está rodando em um Meta Quest 2, que normalmente visa orçamentos de quadro de 13,88 ms (72 fps) ou até mesmo 8,33 ms (120 fps), porque altas taxas de quadros são importantes para evitar enjoo em dispositivos VR. No entanto, mesmo que este jogo estivesse visando 30 fps, é claro que este projeto está em apuros.

Embora a thread de renderização e as threads de trabalho pareçam semelhantes ao exemplo que está dentro do orçamento de quadro, a thread principal está claramente ocupada com trabalho durante todo o quadro. Mesmo considerando a pequena quantidade de sobrecarga do profiler no final do quadro, a thread principal está ocupada por mais de 45 ms, o que significa que este projeto alcança taxas de quadros de menos de 22 fps. Não há marcador que mostre a thread principal esperando ociosa pelo VSync; ela está ocupada durante todo o quadro.

A próxima etapa da investigação é identificar as partes do quadro que levam mais tempo e entender por que isso acontece. Neste quadro, PostLateUpdate.FinishFrameRendering leva 16,23 ms, mais do que todo o orçamento do quadro. Uma inspeção mais próxima revela que há cinco instâncias de um marcador chamado Inl_RenderCameraStack, indicando que cinco câmeras estão ativas e renderizando a cena. Como cada câmera na Unity invoca todo o pipeline de renderização, incluindo eliminação, ordenação e agrupamento, a tarefa de maior prioridade para este projeto é reduzir o número de câmeras ativas, idealmente para apenas uma.

BehaviourUpdate, o marcador do Profiler que abrange todas as execuções do método MonoBehaviour.Update(), leva 7,27 milissegundos neste quadro.

Na visualização da Linha do Tempo, seções coloridas de magenta indicam pontos onde scripts estão alocando memória gerenciada do heap. Mudando para a visualização de Hierarquia e filtrando digitando GC.Alloc na barra de pesquisa, mostra que alocar essa memória leva cerca de 0,33 ms neste quadro. No entanto, essa é uma medição imprecisa do impacto que as alocações de memória têm no desempenho da sua CPU.

Os marcadores GC.Alloc não são cronometrados registrando um ponto de Início e Fim como amostras típicas do Profiler. Para minimizar sua sobrecarga, a Unity registra apenas o timestamp da alocação e o tamanho alocado.

O Profiler atribui uma pequena duração de amostra artificial aos marcadores GC.Alloc apenas para garantir que eles sejam visíveis nas visualizações do Profiler. A alocação real pode levar mais tempo, especialmente se um novo intervalo de memória precisar ser solicitado ao sistema. Para ver o impacto mais claramente, coloque marcadores do Profiler ao redor do código que faz a alocação; em um perfilamento profundo, as lacunas entre as amostras GC.Alloc coloridas de magenta na visualização da Linha do Tempo fornecem alguma indicação de quanto tempo elas podem ter levado.

Além disso, alocar nova memória pode ter efeitos negativos no desempenho que são mais difíceis de medir e atribuir diretamente a elas:

  • Solicitar nova memória do sistema pode afetar o orçamento de energia em um dispositivo móvel, o que pode levar o sistema a desacelerar a CPU ou GPU.
  • A nova memória provavelmente precisa ser carregada no Cache L1 da CPU e, assim, empurra linhas de Cache existentes para fora.
  • A coleta de lixo incremental ou síncrona pode ser acionada diretamente ou com um atraso à medida que o espaço livre existente na Memória Gerenciada é eventualmente excedido.

No início do quadro, quatro instâncias de Physics.FixedUpdate somam 4,57 ms. Mais tarde, LateBehaviourUpdate (chamadas para MonoBehaviour.LateUpdate()) levam 4 ms, e Animadores representam cerca de 1 ms. Para garantir que este projeto atinja seu orçamento e taxa de quadros desejados, todas essas questões da thread principal precisam ser investigadas para encontrar otimizações adequadas.

Armadilhas comuns para gargalos da thread principal

Os maiores ganhos de desempenho serão feitos otimizando as coisas que levam mais tempo. As seguintes áreas costumam ser lugares frutíferos para procurar otimizações em projetos que estão limitados pela thread principal:

  • Cálculos de física
  • Atualizações de scripts MonoBehaviour
  • Alocação e/ou coleta de lixo
  • Culling e renderização da câmera na thread principal
  • Agrupamento de chamadas de desenho ineficiente
  • Atualizações de UI, layouts e reconstruções
  • Animação

Leia nossos guias de otimização que oferecem uma longa lista de dicas práticas para otimizar algumas das armadilhas mais comuns:

Dependendo do problema que você deseja investigar, outras ferramentas também podem ser úteis:

  • Para scripts MonoBehaviour que demoram muito, mas não mostram exatamente por que isso acontece, adicione Marcadores de Profiler ao código ou tente profiling profundo para ver a pilha de chamadas completa.
  • Para scripts que alocam memória gerenciada, ative Pilhas de Chamadas de Alocação para ver exatamente de onde vêm as alocações. Alternativamente, ative o profiling profundo ou use Auditor de Projeto, que mostra problemas de código filtrados por memória, para que você possa identificar todas as linhas de código que resultam em alocações gerenciadas.
  • Use o Depurador de Quadro para investigar as causas do agrupamento de chamadas de desenho ruins.

Para dicas abrangentes sobre como otimizar seu jogo, baixe esses guias de especialistas da Unity gratuitamente:

Limitado pela CPU: Thread de renderização

Limitado pela CPU: Thread de renderização

Aqui está um projeto real que é limitado pela sua thread de renderização. Este é um jogo de console com uma perspectiva isométrica e um orçamento de quadro alvo de 33,33 ms.

A captura do Profiler mostra que antes que a renderização possa começar no quadro atual, a thread principal espera pela thread de renderização, como indicado pelo marcador Gfx.WaitForPresentOnGfxThread . A thread de renderização ainda está enviando comandos de chamadas de desenho do quadro anterior e não está pronta para aceitar novas chamadas de desenho da thread principal; também está gastando tempo em Camera.Render.

Você pode distinguir entre marcadores relacionados ao quadro atual e marcadores de outros quadros, porque os últimos aparecem mais escuros. Você também pode ver que, uma vez que a thread principal consegue continuar e começar a emitir chamadas de desenho para a thread de renderização processar, a thread de renderização leva mais de 100 ms para processar o quadro atual, o que também cria um gargalo durante o próximo quadro.

Uma investigação mais aprofundada mostrou que este jogo tinha uma configuração de renderização complexa, envolvendo nove câmeras diferentes e muitos passes extras causados por shaders de substituição. O jogo também estava renderizando mais de 130 luzes pontuais usando um caminho de renderização para frente, o que pode adicionar várias chamadas de desenho transparentes adicionais para cada luz. No total, esses problemas se combinaram para criar mais de 3000 chamadas de desenho por quadro.

Armadilhas comuns para gargalos de thread de renderização

As seguintes são causas comuns a serem investigadas para projetos que estão limitados pela thread de renderização:

  • Baixo agrupamento de chamadas de desenho: Isso se aplica particularmente a APIs gráficas mais antigas, como OpenGL ou DirectX 11.
  • Demasiadas câmeras: A menos que você esteja fazendo um jogo multiplayer de tela dividida, as chances são de que você deve ter apenas uma Câmera ativa.
  • Baixa culling: Isso resulta em muitas coisas sendo desenhadas. Investigue as dimensões do frustum da sua Câmera e as máscaras de camada de culling.

O Módulo de Profiler de Renderização mostra uma visão geral do número de lotes de chamadas de desenho e chamadas de SetPass a cada quadro. A melhor ferramenta para investigar quais lotes de chamadas de desenho sua thread de renderização está emitindo para a GPU é o Depurador de Quadro.

Ferramentas para resolver os gargalos identificados

Embora o foco deste e-book seja identificar problemas de desempenho, os dois guias complementares de otimização de desempenho que destacamos anteriormente oferecem sugestões sobre como resolver os gargalos, dependendo se sua plataforma alvo é PC ou console ou mobile. No contexto de gargalos de thread de renderização, vale a pena enfatizar que a Unity oferece diferentes sistemas e opções de agrupamento, dependendo dos problemas que você identificou. Aqui está uma visão geral rápida de algumas das opções que explicamos em mais detalhes nos e-books:

  • Agrupamento SRP reduz a sobrecarga da CPU armazenando dados de material persistentemente na memória da GPU. Embora não reduza a contagem real de chamadas de desenho, torna cada chamada de desenho mais barata.
  • Instanciação de GPU combina várias instâncias da mesma malha usando o mesmo material em uma única chamada de desenho.
  • Agrupamento Estático combina malhas estáticas (não móveis) que compartilham o mesmo material e, assim, pode lhe trazer vantagens ao trabalhar com um design de nível com muitos elementos estáticos.
  • GPU resident drawer usa automaticamente a instância de GPU para reduzir a sobrecarga da CPU e chamadas de desenho, agrupando GameObjects semelhantes juntos.
  • Dynamic Batching combina malhas pequenas em tempo de execução, o que pode ser uma vantagem em dispositivos móveis mais antigos com altos custos de chamadas de desenho. No entanto, a desvantagem é que a transformação de vértices também pode ser intensiva em recursos.
  • GPU occlusion culling usa shaders de computação para determinar a visibilidade dos objetos comparando buffers de profundidade dos quadros atuais e anteriores, reduzindo a renderização desnecessária de objetos ocluídos sem exigir dados pré-processados.

Além disso, do lado da CPU, técnicas como Camera.layerCullDistances podem ser usadas para reduzir o número de objetos enviados para a thread de renderização, eliminando objetos com base na distância da câmera, ajudando a aliviar gargalos da CPU durante a eliminação de câmera.

Estas são apenas algumas das opções disponíveis. Cada uma delas tem diferentes vantagens e desvantagens. Algumas são limitadas a certas plataformas. Os projetos precisam frequentemente usar uma combinação de vários desses sistemas e, para isso, é necessário entender como obter o máximo deles.

Limitado pela CPU: Threads de trabalho

Limitado pela CPU: Threads de trabalho

Projetos limitados por threads de CPU que não sejam a principal ou a de renderização não são tão comuns. No entanto, isso pode ocorrer se seu projeto usar o Data-Oriented Technology Stack (DOTS), especialmente se o trabalho for movido da thread principal para threads de trabalho usando o job system.

A imagem acima é uma captura do modo de reprodução no Editor, mostrando um projeto DOTS executando uma simulação de fluido de partículas na CPU.

Parece um sucesso à primeira vista. As threads de trabalho estão compactadas com trabalhos Burst-compiled, indicando que uma grande quantidade de trabalho foi movida da thread principal. Normalmente, essa é uma decisão sensata.

No entanto, neste caso, o tempo de quadro de 48,14 ms e o marcador cinza WaitForJobGroupID de 35,57 ms na thread principal são sinais de que nem tudo está bem. WaitForJobGroupID indica que a thread principal agendou trabalhos para serem executados assíncronamente em threads de trabalho, mas precisa dos resultados desses trabalhos antes que as threads de trabalho tenham terminado de executá-los. Os marcadores azuis do Profiler abaixo de WaitForJobGroupID mostram a thread principal executando trabalhos enquanto espera, na tentativa de garantir que os trabalhos terminem mais cedo.

Embora os trabalhos sejam Burst-compiled, eles ainda estão fazendo muito trabalho. Talvez a estrutura de consulta espacial usada por este projeto para encontrar rapidamente quais partículas estão próximas umas das outras deva ser otimizada ou trocada por uma estrutura mais eficiente. Ou, os trabalhos de consulta espacial podem ser agendados para o final do quadro em vez do início, com os resultados não sendo necessários até o início do próximo quadro. Talvez este projeto esteja tentando simular muitas partículas. Uma análise mais aprofundada do código dos trabalhos é necessária para encontrar a solução, então adicionar marcadores de Profiler mais detalhados pode ajudar a identificar suas partes mais lentas.

Os trabalhos em seu projeto podem não estar tão paralelizados quanto neste exemplo. Talvez você tenha apenas um trabalho longo rodando em uma única thread de trabalhador. Isso é aceitável, desde que o tempo entre o agendamento do trabalho e o tempo que precisa ser concluído seja longo o suficiente para que o trabalho seja executado. Se não for, você verá a thread principal travar enquanto espera o trabalho ser concluído, como na captura de tela acima.

Armadilhas comuns para gargalos de threads de trabalhador

Causas comuns de pontos de sincronização e gargalos de threads de trabalhador incluem:

  • Trabalhos não sendo compilados pelo compilador Burst
  • Trabalhos de longa duração em uma única thread de trabalhador em vez de serem paralelizados em várias threads de trabalhador
  • Tempo insuficiente entre o ponto no quadro em que um trabalho é agendado e o ponto em que o resultado é necessário
  • Múltiplos "pontos de sincronização" em um quadro, que exigem que todos os trabalhos sejam concluídos imediatamente

Você pode usar o Eventos de Fluxo recurso na visualização da Linha do Tempo do módulo de Profiler de Uso da CPU para investigar quando os trabalhos são agendados e quando seus resultados são esperados pela thread principal.

Para mais informações sobre como escrever código DOTS eficiente, consulte o guia Melhores Práticas DOTS.

Dependente da GPU

Dependente da GPU

Seu aplicativo está limitado pela GPU se a thread principal gastar muito tempo em marcadores de Profiler como Gfx.WaitForPresentOnGfxThread, e sua thread de renderização exibir simultaneamente marcadores como Gfx.PresentFrame ou .WaitForLastPresent.

A melhor maneira de obter tempos de quadro da GPU é usar uma ferramenta de perfilagem de GPU específica para a plataforma-alvo, mas nem todos os dispositivos facilitam a captura de dados confiáveis.

A API FrameTimingManager pode ser útil nesses casos, fornecendo tempos de quadro de alto nível e baixa sobrecarga tanto na CPU quanto na GPU.

A captura acima foi feita em um telefone móvel Android usando a API gráfica Vulkan. Embora parte do tempo gasto em Gfx.PresentFrame neste exemplo possa estar relacionado à espera pelo VSync, a extrema duração deste marcador de Profiler indica que a maior parte desse tempo é gasta esperando a GPU terminar de renderizar o quadro anterior.

Neste jogo, certos eventos de jogabilidade acionaram o uso de um shader que triplicou o número de chamadas de desenho renderizadas pela GPU. Questões comuns a investigar ao perfilar o desempenho da GPU incluem:

  • Efeitos de pós-processamento em tela cheia caros, como Oclusão Ambiental e Bloom
  • Shaders de fragmento caros causados por:
  • Lógica de ramificação dentro do código do shader
  • Usar precisão de ponto flutuante total em vez de meia precisão, especialmente em dispositivos móveis
  • Uso excessivo de registradores, que afetam a ocupação do wavefront das GPUs
  • Overdraw na fila de renderização Transparente causado por:
  • Renderização de UI ineficiente
  • Uso sobreposto ou excessivo de sistemas de partículas
  • Efeitos de pós-processamento
  • Resoluções de tela excessivamente altas, como:
  • Displays 4K
  • Displays Retina em dispositivos móveis
  • Micro triângulos causados por:
  • Geometria de malha densa
  • Falta de sistemas de Nível de Detalhe (LOD), que é um problema particular em GPUs móveis, mas pode afetar GPUs de PC e console também
  • Faltas de cache e largura de banda de memória GPU desperdiçada causadas por:
  • Texturas não comprimidas
  • Texturas de alta resolução sem mipmaps
  • Shaders de geometria ou tesselação, que podem ser executados várias vezes por quadro se as sombras dinâmicas estiverem habilitadas

Se sua aplicação parecer estar limitada pela GPU, você pode usar o Depurador de Quadro como uma maneira rápida de entender os lotes de chamadas de desenho que estão sendo enviados para a GPU. No entanto, esta ferramenta não pode apresentar informações específicas de tempo da GPU, apenas como a cena geral é construída.

A melhor maneira de investigar a causa dos gargalos da GPU é examinar uma captura da GPU de um profiler de GPU adequado. Qual ferramenta você usa depende do hardware alvo e da API gráfica escolhida. Veja a seção de ferramentas de perfilagem e depuração no e-book para mais informações.

Guias de melhores práticas da Unity
Mais dicas para desenvolvedores e criadores da Unity

Encontre mais melhores práticas e dicas do centro de melhores práticas da Unity. Escolha entre mais de 30 guias, criados por especialistas da indústria, engenheiros da Unity e artistas técnicos, que ajudarão você a desenvolver de forma eficiente com as ferramentas e sistemas da Unity.