Em geral, com que frequência e quando devo otimizar meu código?

13

No passo "normal" da otimização da programação de negócios, muitas vezes é deixado até realmente necessário. Significando que você não deve otimizar até que seja realmente necessário.

Lembre-se do que Donald Knuth disse: "Devemos esquecer pequenas eficiências, digamos, 97% das vezes: a otimização prematura é a raiz de todo mal"

Quando é a hora de otimizar para ter certeza de que não estou desperdiçando esforço. Devo fazer isso no nível do método? Nível de classe? Nível do módulo?

Além disso, qual deve ser minha medida de otimização? Carrapatos? Taxa de quadros? Tempo total?

David Basarab
fonte

Respostas:

18

Onde trabalhei, sempre usamos vários níveis de criação de perfil; se você encontrar um problema, basta mover a lista um pouco mais até descobrir o que está acontecendo:

  • O "profiler humano", também conhecido como apenas jogar o jogo ; sente lento ou "engate" ocasionalmente? Notando animações espasmódicas? (Como desenvolvedor, observe que você será mais sensível a alguns tipos de problemas de desempenho e alheio a outros. Planeje testes extras de acordo.)
  • Ligue a tela do FPS , que é um FPS médio de 5 segundos na janela deslizante. Muito pouca sobrecarga para calcular e exibir.
  • Ligue as barras de perfil , que são apenas uma série de quads (cores ROYGBIV) que representam diferentes partes do quadro (por exemplo, vblank, preframe, atualização, colisão, renderização, pós-quadro) usando um simples cronômetro "cronômetro" em cada seção do código . Para enfatizar o que queremos, definimos uma barra de largura de tela como representativa de um quadro de destino de 60Hz, para que seja realmente fácil ver se você está, por exemplo, com 50% abaixo do orçamento (apenas meia barra) ou 50% acima ( a barra envolve e se torna uma barra e meia). Também é muito fácil dizer o que geralmente está comendo a maior parte do quadro: red = render, yellow = update, etc ...
  • Crie uma compilação instrumentada especial que insira "cronômetro" como código em todas as funções. (Observe que você pode ter um desempenho massivo, dcache e icache atingido ao fazer isso, por isso é definitivamente intrusivo. Mas se você não tiver um perfil de amostragem adequado ou um suporte decente na CPU, essa é uma opção aceitável. Você também pode ser inteligente sobre como gravar um mínimo de dados na função, entrar / sair e reconstruir rastros de chamadas posteriormente.) Quando criamos o nosso, imitamos grande parte do formato de saída do gprof .
  • O melhor de tudo, execute um criador de perfil de amostragem ; O VTune e o CodeAnalyst estão disponíveis para x86 e x64; você tem vários ambientes de simulação ou emulação que podem fornecer dados aqui.

(Há uma história divertida da GDC do ano passado de um programador gráfico que tirou quatro fotos de si mesmo - feliz, indiferente, irritado e irritado - e exibiu uma imagem apropriada no canto das construções internas com base na taxa de quadros. os criadores de conteúdo aprenderam rapidamente a não ativar shaders complicados para todos os seus objetos e ambientes: eles irritariam o programador. Veja o poder do feedback.)

Observe que você também pode fazer coisas divertidas, como representar graficamente as "barras de perfil" continuamente, para ver padrões de pico ("estamos perdendo um quadro a cada 7 quadros") ou algo semelhante.

Porém, para responder sua pergunta diretamente: na minha experiência, embora seja tentador (e geralmente gratificante - geralmente aprendo algo) reescrever funções / módulos únicos para otimizar o número de instruções ou o desempenho do icache ou dcache, e realmente precisamos fazer Às vezes, quando temos um problema de desempenho particularmente desagradável, a grande maioria dos problemas de desempenho com os quais lidamos regularmente se resume ao design . Por exemplo:

  • Devemos armazenar em cache na RAM ou recarregar do disco os quadros de animação de estado de "ataque" para o player? Que tal para cada inimigo? Não temos RAM para fazer tudo, mas as cargas de disco são caras! Você pode ver o engate se 5 ou 6 inimigos diferentes aparecerem ao mesmo tempo! (Ok, que tal desova impressionante?)
  • Estamos realizando um único tipo de operação em todas as partículas ou todas as operações em uma única partícula? (Essa é uma troca do icache / dcache, e a resposta nem sempre é clara.) Que tal separar todas as partículas e armazenar as posições juntas (a famosa "estrutura de matrizes") versus manter todos os dados de partículas em um só lugar (" matriz de estruturas ").

Você o ouve até que se torne desagradável em qualquer curso de ciência da computação em nível universitário, mas: é realmente tudo sobre estruturas e algoritmos de dados. Passar algum tempo com o design de algoritmos e fluxo de dados vai lhe trazer mais benefícios em geral. (Certifique-se de ler os excelentes slides das Armadilhas da Programação Orientada a Objetos de um membro dos Serviços de Desenvolvedor da Sony para obter algumas dicas aqui.) Isso não parece otimização; é na maior parte do tempo gasto com um quadro branco ou ferramenta UML ou criando muitos protótipos, em vez de tornar o código atual mais rápido. Mas geralmente vale muito mais a pena.

E outra heurística útil: se você estiver próximo do "núcleo" do seu mecanismo, pode valer algum esforço e experimentação extra para otimizar (por exemplo, vetorizar essas matrizes multiplica!). Quanto mais longe do núcleo, menos você deve se preocupar com isso, a menos que uma de suas ferramentas de criação de perfil diga o contrário.

leander
fonte
6
  1. Use as estruturas de dados e algoritmos corretos antecipadamente.
  2. Não micro-otimize até você criar um perfil e saber exatamente onde estão seus pontos de acesso.
  3. Não se preocupe em ser inteligente. O compilador já faz todos os pequenos truques que você está pensando ("Oh! Preciso multiplicar por quatro! Vou mudar para a esquerda!")
  4. Preste atenção às falhas de cache.
maravilhoso
fonte
1
Confiar no compilador só é inteligente até um certo ponto. Sim, ele fará algumas otimizações de olho mágico que você não pensaria (e não poderia prescindir da montagem), mas não tem noção do que seu algoritmo deve fazer, portanto não pode fazer otimizações inteligentes. Além disso, você ficaria surpreso com a quantidade de ciclos que pode ganhar implementando código crítico em assembly ou intrínseco ... se você souber o que está fazendo. Os compiladores não são tão inteligentes quanto parecem, não sabem o que você faz, a menos que você os diga explicitamente em todos os lugares (como usar 'restringir' religiosamente).
Kaj
1
E, novamente, devo comentar que, se você procurar apenas pontos de interesse, perderá muitos ciclos porque não encontrará ciclos complicados em toda a linha (por exemplo, ponteiros inteligentes ... desreferências em qualquer lugar, nunca aparecendo como um ponto ativo, porque efetivamente todo o seu programa é um ponto ativo).
Kaj
1
Concordo com os dois pontos de vista, mas agruparia a maior parte em "usar as estruturas e algoritmos de dados corretos". Se você está passando por ponteiros inteligentes recontados em todos os lugares e está passando por ciclos sangrentos através da contagem, você definitivamente escolheu a estrutura de dados errada.
munificent
5

Lembre-se também de "pessimização prematura". Embora não haja necessidade de ser incondicional em todas as linhas de código, há justificativa para perceber que você está realmente trabalhando em um jogo, o que tem implicações de desempenho em tempo real.
Enquanto todo mundo pede para você medir e otimizar os pontos de acesso, essa técnica não mostra o desempenho perdido em locais ocultos. Por exemplo, se cada operação '+' no seu código levasse o dobro do tempo necessário, ela não aparecerá como um ponto de acesso e, portanto, você nunca a otimizará nem perceberá, no entanto, uma vez que está sendo usada em todo o coloque-o pode custar muito desempenho. Você ficaria surpreso com quantos desses ciclos desaparecem sem nunca serem detectados. Portanto, esteja ciente do que você faz.
Além disso, costumo escrever perfis regularmente para ter uma idéia do que está lá e quanto tempo resta por quadro. Para mim, o tempo por quadro é o mais lógico, pois me diz diretamente onde estou com os objetivos da taxa de quadros. Tente também descobrir onde estão os picos e o que os causa - prefiro uma taxa de quadros estável a uma taxa de quadros alta com picos.

Kaj
fonte
Isso parece tão errado para mim. Claro, meu '+' pode demorar o dobro de cada vez que é chamado, mas isso realmente importa apenas em um loop apertado. Dentro de um loop apertado, alterar um único '+' pode fazer ordens de magnitude mais do que alterar um '+' fora do loop. Por que pensar em um décimo de microssegundo, quando um milissegundo pode ser salvo?
Wilduck 5/08
1
Então você não entende a idéia por trás da perda de gota. '+' (como exemplo) é chamado centenas de milhares de vezes por quadro, não apenas em loops apertados. Se isso perder alguns ciclos toda vez que você perder muitos ciclos em geral, mas nunca será exibido como um ponto de acesso, pois as chamadas são distribuídas igualmente no caminho da base de código / execução. Portanto, você não está falando de um décimo de microssegundo, mas, na verdade, milhares de vezes esses décimos de microssegundos, somando vários milissegundos. Depois de ir para as frutas baixas (laços apertados), ganhei milissegundos dessa maneira mais de uma vez.
Kaj
É como uma torneira que pinga. Por que se preocupar em salvar essa pequena gota? - "Se a sua torneira está pingando a uma gota por segundo, você pode perder 2700 galões por ano".
Kaj
Ah, acho que não estava claro o que eu quis dizer quando o operador + estava sobrecarregado, portanto isso afetaria todos os '+' no código - você realmente não gostaria de otimizar todos os '+' no código. Exemplo ruim, eu acho ... eu quis dizer isso como um exemplo de 'funcionalidade principal que é chamada em todo o lugar onde a implementação pode ser mais lenta do que o esperado, especialmente quando oculta por sobrecarga do operador ou outras construções C ++ ofuscantes'.
Kaj
3

Quando um jogo estiver pronto para ser lançado (final ou beta), ou é visivelmente lento, provavelmente é o melhor momento para criar um perfil do seu aplicativo. Obviamente, você sempre pode executar o criador de perfil a qualquer momento; mas sim, a otimização prematura é a raiz de todo mal. Otimização infundada também; você precisa de dados reais para mostrar que algum código é lento, antes de tentar "otimizá-lo". Um criador de perfil faz isso por você.

Se você não conhece um perfilador, aprenda-o! Aqui está uma boa postagem no blog demonstrando a utilidade de um criador de perfil.

A maior parte da otimização do código do jogo se resume a reduzir os ciclos de CPU necessários para cada quadro. Uma maneira de fazer isso é otimizar todas as rotinas enquanto você as escreve e garantir que seja o mais rápido possível. No entanto, é comum dizer que 90% dos ciclos da CPU são gastos em 10% do código. Isso significa que direcionar todo o seu trabalho de otimização para essas rotinas de gargalo terá um efeito 10x de otimizar tudo uniformemente. Então, como você identifica essas rotinas? A criação de perfil facilita.

Caso contrário, se o seu pequeno jogo estiver rodando a 200 FPS, mesmo com um algoritmo ineficiente, você realmente tem um motivo para otimizar? Você deve ter uma boa idéia das especificações da sua máquina-alvo e garantir que o jogo corra bem nessa máquina, mas qualquer coisa além disso é (sem dúvida) desperdício de tempo que poderia ser melhor gasto em codificação ou polimento do jogo.

Ricket
fonte
Embora a fruta pendente de fato tenda a estar em 10% do código e seja facilmente capturada pela criação de perfis no final das contas, trabalhar apenas com a criação de perfis para isso fará com que você perca as rotinas que são chamadas de muito, mas têm apenas um pouco um pouco de código incorreto cada - eles não aparecem no seu perfil, mas sangram muitos ciclos por chamada. Isso realmente se soma.
Kaj
@Kaj, Bons criadores de perfil somam todas as centenas de execuções individuais do algoritmo ruim e mostram o total. Em seguida, você dirá "Mas e se você tivesse 10 métodos ruins e todos chamados em 1/10 da frequência?" Se você gastar todo o seu tempo com esses 10 métodos, estará perdendo todas as frutas baixas, onde obterá um retorno muito maior pelo seu dinheiro.
John McDonald
2

Acho útil criar perfis. Mesmo que você não esteja otimizando ativamente, é bom ter uma idéia do que está limitando seu desempenho a qualquer momento. Muitos jogos têm algum tipo de HUD sobreposto, que exibe um gráfico simples (geralmente apenas uma barra colorida) mostrando quanto tempo várias partes do ciclo do jogo levam cada quadro.

Seria uma má idéia deixar a análise e a otimização do desempenho para um estágio tardio demais. Se você já construiu o jogo e está 200% acima do orçamento da CPU e não consegue encontrá-lo através da otimização, está ferrado.

Você precisa saber quais são os orçamentos para gráficos, física etc., enquanto escreve. Você não pode fazer isso se não tem idéia de qual será o seu desempenho e não consegue adivinhar sem saber qual é o seu desempenho e o quanto de folga pode haver.

Portanto, crie algumas estatísticas de desempenho desde o primeiro dia.

Quanto a quando lidar com as coisas - novamente, provavelmente é melhor não deixar tarde demais, para que você não precise refatorar metade do seu motor. Por outro lado, não se envolva muito em otimizar coisas para espremer todos os ciclos se você acha que pode mudar o algoritmo inteiramente amanhã, ou se não colocou dados reais do jogo nele.

Apanhe as frutas baixas à medida que avança, lide com as coisas grandes periodicamente e você ficará bem.

JasonD
fonte
Para adicionar ao criador de perfil no jogo (com o qual concordo totalmente), estender o criador de perfil no jogo para exibir várias barras (para vários quadros) ajuda a correlacionar o comportamento do jogo com picos e pode ajudar a encontrar gargalos que não aparecerão na captura média com um criador de perfil.
Kaj
2

Se olharmos para a citação de Knuth em seu contexto, ele continua explicando que devemos otimizar, mas com ferramentas, como um criador de perfil.

Você deve constantemente criar um perfil e um perfil de memória para o seu aplicativo após a arquitetura básica ser estabelecida.

A criação de perfil não apenas ajuda a aumentar a velocidade, mas também a encontrar bugs. Se o seu programa repentinamente muda drasticamente a velocidade, isso geralmente ocorre devido a um erro. Se você não criar um perfil, pode passar despercebido.

O truque para otimizar é fazê-lo por design. Não espere até o último minuto. Certifique-se de que o design do seu programa ofereça o desempenho que você precisa, sem realmente fazer truques desagradáveis ​​de loop interno.

Jonathan Fischoff
fonte
1

Para o meu projeto, costumo aplicar algumas otimizações MUITO necessárias no meu mecanismo básico. Por exemplo, eu sempre gosto de implementar uma boa implementação sólida de SIMD usando SSE2 e 3DNow! Isso garante que minha matemática de ponto flutuante esteja na sugestão de onde eu quero que ela esteja. Outra boa prática é criar o hábito das otimizações à medida que você codifica, em vez de voltar. Na maioria das vezes, essas pequenas práticas consomem tanto tempo quanto o que você estava codificando de qualquer maneira. Antes de codificar um recurso, pesquise a maneira mais eficiente de fazê-lo.

Bottom line, na minha opinião, é mais difícil tornar seu código mais eficiente depois que ele já é péssimo.

Krankzinnig
fonte
0

Eu diria que a maneira mais fácil seria usar seu bom senso - se algo parece estar lento, então dê uma olhada. Veja se é um gargalo.
Use um criador de perfil para dar uma olhada nas funções de velocidade que estão sendo executadas e com que frequência elas estão sendo chamadas.
Não faz sentido otimizar ou gastar tempo tentando otimizar algo que não precisa.

O Pato Comunista
fonte
0

Se o seu código estiver lento, execute um criador de perfil e veja o que exatamente está causando a execução mais lenta. Ou você pode ser proativo e já ter um gerador de perfil em execução antes de começar a perceber problemas de desempenho.

Você desejará otimizar quando a taxa de quadros cair a um ponto em que o jogo começa a sofrer. O culpado mais provável será o uso excessivo da CPU (100%).

Bryan Denny
fonte
Eu diria que a GPU é tão provável quanto a CPU. De fato, dependendo de como as coisas estão fortemente acopladas, é totalmente possível ter uma CPU fortemente ligada na metade do quadro e uma GPU fortemente na outra metade. Os perfis mudos podem até mostrar uma maneira menos de 100% de utilização em ambos. Verifique se o seu perfil é de grão fino o suficiente para mostrar que (mas não tão fino granulado a ser intrusiva!)
JasonD
0

Você deve otimizar seu código ... quantas vezes precisar.

O que eu fiz no passado é apenas executar o jogo continuamente com a criação de perfis ativada (pelo menos um contador de quadros na tela o tempo todo). Se o jogo estiver ficando lento (abaixo da taxa de quadros de destino na sua máquina de especificações mínimas, por exemplo), ligue o criador de perfil e veja se algum hot spot aparece.

Às vezes não é o código. Muitos dos problemas que encontrei no passado foram orientados a gpu (é verdade, isso ocorreu no iPhone). Problemas de taxa de preenchimento, muitas chamadas de desenho, sem lotes de geometria suficiente, sombreadores ineficientes ...

Além de algoritmos ineficientes para problemas difíceis (ou seja, busca de caminhos, física), raramente encontrei problemas em que o próprio código era o culpado. E esses problemas difíceis devem ser coisas que você gasta muito do seu esforço para acertar o algoritmo e não se preocupar com coisas menores.

Tetrad
fonte
0

Para mim é o melhor seguir um modelo de dados bem preparado. E otimização - antes do principal passo adiante. Quero dizer, antes de começar a implementar algo grande novo. Outra razão para a otimização é quando estou perdendo o controle sobre os recursos, o aplicativo precisa de muita carga de CPU / carga de GPU ou memória e não sei por que :) ou é demais.

samboush
fonte