Pular para o conteúdo
Fundamento · Depuração e desempenhoIntermediário

Custo e precisão

O que deixa um shader lento, o que o deixa errado e como ler a linha de custo de cada card.

Como funciona

Um shader de fragmento roda uma vez para cada pixel que cobre, a cada quadro. Um efeito de tela cheia em 1920×1080 roda cerca de dois milhões de vezes por quadro; em 3840×2160, mais de oito milhões. Tudo o que você acrescenta é multiplicado por esse número, e é por isso que algumas linhas a mais podem custar um milissegundo num notebook e muito mais num celular. Esta página cobre as fontes de custo mais comuns e os problemas de precisão que deixam um shader errado em vez de lento.

O que custa o quê

  • Aritmética é o trabalho mais barato: somas, multiplicações, mix, dot, clamp. As GPUs são feitas para fazer muito disso em paralelo. Funções como sin, exp, pow e sqrt custam algumas vezes mais e ainda assim são baratas perto da memória.
  • Leituras de textura buscam dados na memória. A GPU esconde boa parte da espera trabalhando em outros pixels, mas toda leitura consome banda, e muitas leituras por pixel (um blur largo, um kernel grande) são o motivo mais comum de um efeito ser lento. Uma leitura dependente, cujas coordenadas vêm de uma leitura anterior ou de contas pesadas, não pode começar antes, e é mais lenta em algumas GPUs de celular.
  • Desvios são baratos quando pixels vizinhos vão para o mesmo lado. As GPUs rodam os pixels em grupos que executam em sincronia, então quando os pixels de um grupo discordam, o grupo roda os dois lados e guarda os resultados de que precisa. Um if sobre um uniform sai praticamente de graça; um if sobre ruído pode custar os dois lados.
  • Laços custam cada iteração. Um laço com contagem fixa é fácil para o compilador; um laço que roda conforme um valor calculado por pixel se comporta como um desvio divergente.

Pixels e passes

Quantos pixels você sombreia muitas vezes importa mais do que como você os sombreia.

  • Overdraw. Cada camada de geometria transparente roda o shader de fragmento de novo nos mesmos pixels. Dez partículas sobrepostas custam dez vezes uma. Geometria opaca desenhada da frente para trás deixa o teste de profundidade descartar pixels escondidos antes de sombreá-los, mas discard e escrever em gl_FragDepth podem desligar esse teste antecipado.
  • Passes. Um pass de pós-processamento lê e escreve uma imagem de tela cheia. Cada pass a mais acrescenta essa banda, mesmo que o shader dele seja trivial. Renderizar um pass na metade da resolução reduz os pixels a um quarto.
  • Filtros separáveis. Um blur que pondera uma vizinhança de 9×9 lê 81 texels por pixel. Dividi-lo num pass horizontal de 9 e um vertical de 9 lê 18, ao preço de um pass a mais. Para qualquer coisa maior que alguns texels a divisão ganha, e é por isso que o blur gaussiano separável usa dois passes.

Meça, não chute

Os compiladores reorganizam o código e as GPUs diferem, então raciocinar sobre custo só leva até certo ponto. Um teste rápido: diminua o canvas ou a resolução de renderização. Se o tempo de quadro cai junto, o shader de fragmento é o gargalo. As engines têm profilers (o Visual Profiler do Godot mostra o tempo de GPU por etapa), e o WebGL 2 pode medir o trabalho da GPU com a extensão EXT_disjoint_timer_query_webgl2 quando o navegador a oferece.

Cada card deste site tem uma linha de custo: operações matemáticas, leituras de textura e passes. Elas são contadas a partir do código do efeito, não cronometradas, então leia-as como uma comparação entre cards, não como milissegundos. Leituras de textura e passes pesam mais que operações matemáticas.

Precisão

Os floats do GLSL vêm em três precisões. highp é um float de 32 bits na maioria do hardware. mediump só garante uma precisão relativa de cerca de 1 parte em 1000 e um alcance de cerca de ±16.000. GPUs de desktop calculam tudo em precisão total, seja lá o que você declarar, então um problema de mediump muitas vezes só aparece no celular. Declare precision highp float; a menos que você tenha medido um motivo para não fazer isso; o GLSL ES 3.00 exige que os shaders de fragmento o suportem.

Até o highp acaba. Um float de 32 bits tem 24 bits de mantissa, então a distância entre valores vizinhos cresce com o tamanho do número. Um uniform time contando segundos vai bem por minutos, mas depois de um dia (86.400 s) essa distância é de cerca de 0,008 s, e sin(time * 10.0) anda aos saltos de forma visível. Coordenadas de mundo grandes têm o mesmo problema no espaço. Faça o tempo dar a volta na CPU, num intervalo em que a sua animação se repete, e mantenha as coordenadas perto da origem.

NaN e infinito

Algumas operações não têm resposta real, e a GPU devolve NaN (not a number, “não é um número”) ou infinito em vez de falhar:

  • sqrt(x) e log(x) de um número negativo, pow(x, y) com x negativo
  • divisão por zero, incluindo o normalize de um vetor nulo
  • asin e acos fora de -1..1

O NaN se espalha por toda operação em que encosta. Num pixel isolado ele aparece como um pontinho preto ou branco; passe esse pixel por um blur ou um bloom e ele vira um bloco. Limite as entradas onde elas podem sair do intervalo, por exemplo sqrt(max(x, 0.0)) e pow(max(x, 0.0), k), e some um epsilon pequeno antes de dividir por algo que pode chegar a zero. Para achar onde um NaN começa, pinte-o de uma cor visível (veja mostrar valores como cor).

Leitura complementar