== causa ramificação no GLSL?


27

Tentando descobrir exatamente o que causa ramificação e o que não ocorre no GLSL.

Estou fazendo muito isso no meu shader:

float(a==b)

Eu uso para simular instruções if, sem ramificação condicional ... mas é eficaz? Não tenho declarações if em qualquer lugar do meu programa agora, nem tenho loops.

EDIT: Para esclarecer, eu faço coisas assim no meu código:

float isTint = float((renderflags & GK_TINT) > uint(0)); // 1 if true, 0 if false
    float isNotTint = 1-isTint;//swaps with the other value
    float isDarken = float((renderflags & GK_DARKEN) > uint(0));
    float isNotDarken = 1-isDarken;
    float isAverage = float((renderflags & GK_AVERAGE) > uint(0));
    float isNotAverage = 1-isAverage;
    //it is none of those if:
    //* More than one of them is true
    //* All of them are false
    float isNoneofThose = isTint * isDarken * isAverage + isNotTint * isAverage * isDarken + isTint * isNotAverage * isDarken + isTint * isAverage * isNotDarken + isNotTint * isNotAverage * isNotDarken;
    float isNotNoneofThose = 1-isNoneofThose;

    //Calc finalcolor;
    finalcolor = (primary_color + secondary_color) * isTint * isNotNoneofThose + (primary_color - secondary_color) * isDarken * isNotNoneofThose + vec3((primary_color.x + secondary_color.x)/2.0,(primary_color.y + secondary_color.y)/2.0,(primary_color.z + secondary_color.z)/2.0) * isAverage * isNotNoneofThose + primary_color * isNoneofThose;

EDIT: Eu sei por que não quero ramificação. Eu sei o que é ramificação. Estou feliz por você estar ensinando as crianças sobre ramificação, mas gostaria de me conhecer sobre operadores booleanos (e operações bit a bit, mas tenho certeza de que estão bem)

Respostas:


42

O que causa a ramificação no GLSL depende do modelo da GPU e da versão do driver OpenGL.

A maioria das GPUs parece ter uma forma de operação "selecione um dos dois valores" que não tem custo de ramificação:

n = (a==b) ? x : y;

e às vezes até coisas como:

if(a==b) { 
   n = x;
   m = y;
} else {
   n = y;
   m = x;
}

will be reduced to a few select-value operation with no branching penalty.

Alguns GPU / Drivers têm (tiveram?) Uma penalidade no operador de comparação entre dois valores, mas uma operação mais rápida na comparação contra zero.

Onde pode ser mais rápido:

gl_FragColor.xyz = ((tmp1 - tmp2) != vec3(0.0)) ? E : tmp1;

em vez de comparar (tmp1 != tmp2)diretamente, mas isso depende muito da GPU e do driver; portanto, a menos que você esteja direcionando uma GPU muito específica e nenhuma outra, recomendo usar a operação de comparação e deixar esse trabalho de otimização para o driver OpenGL, pois outro driver pode ter um problema com o formato mais longo e seja mais rápido com a maneira mais simples e legível.

"Ramos" nem sempre é uma coisa ruim. Por exemplo, na GPU SGX530 usada no OpenPandora, este shader scale2x (30ms):

    lowp vec3 E = texture2D(s_texture0, v_texCoord[0]).xyz;
    lowp vec3 D = texture2D(s_texture0, v_texCoord[1]).xyz;
    lowp vec3 F = texture2D(s_texture0, v_texCoord[2]).xyz;
    lowp vec3 H = texture2D(s_texture0, v_texCoord[3]).xyz;
    lowp vec3 B = texture2D(s_texture0, v_texCoord[4]).xyz;
    if ((D - F) * (H - B) == vec3(0.0)) {
            gl_FragColor.xyz = E;
    } else {
            lowp vec2 p = fract(pos);
            lowp vec3 tmp1 = p.x < 0.5 ? D : F;
            lowp vec3 tmp2 = p.y < 0.5 ? H : B;
            gl_FragColor.xyz = ((tmp1 - tmp2) != vec3(0.0)) ? E : tmp1;
    }

Acabou drasticamente mais rápido que esse shader equivalente (80ms):

    lowp vec3 E = texture2D(s_texture0, v_texCoord[0]).xyz;
    lowp vec3 D = texture2D(s_texture0, v_texCoord[1]).xyz;
    lowp vec3 F = texture2D(s_texture0, v_texCoord[2]).xyz;
    lowp vec3 H = texture2D(s_texture0, v_texCoord[3]).xyz;
    lowp vec3 B = texture2D(s_texture0, v_texCoord[4]).xyz;
    lowp vec2 p = fract(pos);

    lowp vec3 tmp1 = p.x < 0.5 ? D : F;
    lowp vec3 tmp2 = p.y < 0.5 ? H : B;
    lowp vec3 tmp3 = D == F || H == B ? E : tmp1;
    gl_FragColor.xyz = tmp1 == tmp2 ? tmp3 : E;

Você nunca sabe de antemão como um compilador GLSL específico ou uma GPU específica serão executados até que você faça o benchmark.


Para adicionar o ponto final (mesmo que eu não tenha números de tempo reais e código de sombreamento para apresentá-lo para esta parte), atualmente uso como meu hardware de teste regular:

  • Intel HD Graphics 3000
  • Gráficos Intel HD 405
  • nVidia GTX 560M
  • nVidia GTX 960
  • AMD Radeon R7 260X
  • nVidia GTX 1050

Como uma ampla variedade de modelos diferentes e comuns de GPU para testar.

Testando cada um com drivers OpenGL e OpenCL de código aberto do Windows, proprietários de Linux e Linux.

E toda vez que tento otimizar o sombreador GLSL (como no exemplo SGX530 acima) ou as operações OpenCL para uma combinação de GPU / Driver específica, acabo afetando igualmente o desempenho em mais de uma das outras GPUs / Drivers.

Portanto, além de reduzir claramente a complexidade matemática de alto nível (por exemplo, converter 5 divisões idênticas em uma única recíproca e 5 multiplicações) e reduzir as pesquisas de textura / largura de banda, provavelmente será uma perda de tempo.

Cada GPU é muito diferente dos outros.

Se você estivesse trabalhando especificamente em (a) console (s) de jogos com uma GPU específica, isso seria uma história diferente.

O outro aspecto (menos significativo para desenvolvedores de jogos pequenos, mas ainda notável) é que os drivers de GPU de computador podem um dia substituir silenciosamente seus shaders ( se o jogo se tornar popular o suficiente ) por reescritos personalizados otimizados para essa GPU em particular. Fazendo tudo isso funcionar para você.

Eles farão isso para jogos populares que são freqüentemente usados ​​como benchmarks.

Ou, se você der aos seus jogadores acesso aos shaders para que eles possam editá-los facilmente, alguns deles podem extrair alguns FPS extras para seu próprio benefício.

Por exemplo, existem pacotes de shader e textura feitos pelo ventilador para o Oblivion para aumentar drasticamente a taxa de quadros em um hardware que dificilmente seria reproduzido.

E, finalmente, quando seu shader ficar complexo o suficiente, seu jogo quase terminar, e você começar a testar em diferentes hardwares, estará ocupado o suficiente apenas consertando seus shaders para funcionar em uma variedade de GPUs, devido a vários erros que você não conseguirá. ter tempo para otimizá-los nesse nível.


"Ou se você der aos seus jogadores acesso aos shaders para que eles possam editá-los facilmente ..." Como você mencionou isso, qual pode ser a sua abordagem aos shaders de wallhack e coisas do gênero? Sistema de honra, verificado, relatórios ...? Gosto da ideia de lobbies restritos aos mesmos shaders / ativos, quaisquer que sejam, uma vez que posturas sobre realismo máximo / min / escalável, façanhas e assim por diante devem reunir jogadores e modders para incentivar a revisão, a colaboração etc. para lembrar que foi assim que o Gary's Mod funcionou, mas estou bem fora do circuito.
John P #

11
@JohnP Security qualquer coisa que presuma que o cliente não está comprometido não funciona de qualquer maneira. É claro que se você não quiser que as pessoas editem seus shaders, não há motivo para expô-los, mas isso não ajuda muito em segurança. Sua estratégia para detectar coisas como wallhacks deve tratar o lado do cliente mexer com as coisas como uma primeira barreira baixa e, sem dúvida, pode haver um benefício maior para permitir modificações leves, como nesta resposta, se não levar a uma vantagem injusta detectável para o jogador .
Cubic

8
@JohnP Se você não quiser que os jogadores também vejam através dos muros, não deixe o servidor enviar nenhuma informação sobre o que está por trás do muro.
Polygnome

11
É isso mesmo - não sou contra hackers entre jogadores que gostam por qualquer motivo. Como jogador, porém, abandonei vários títulos AAA porque - entre outras razões - eles fizeram exemplos de modders estéticos enquanto dinheiro / XP / etc. os hackers ficaram ilesos (que ganharam dinheiro com os que estavam frustrados o suficiente para pagar), sobrecarregaram e automatizaram seu sistema de denúncia e apelação e garantiram que os jogos vivessem e morressem pelo número de servidores que eles queriam manter vivos. Eu esperava que houvesse uma abordagem mais descentralizada, tanto como desenvolvedor quanto como jogador.
John P

Não, eu não faço inline se em qualquer lugar. Eu só faço float (boolean declaração) * (algo)
Geklmintendon't de impressionante

7

A resposta de @Stephane Hockenhull praticamente fornece o que você precisa saber, será totalmente dependente de hardware.

Mas deixe-me dar alguns exemplos de como isso pode depender do hardware e por que a ramificação é um problema, o que a GPU faz nos bastidores quando ocorre a ramificação .

Meu foco é principalmente na Nvidia, tenho alguma experiência com programação CUDA de baixo nível e vejo o que é gerado PTX ( IR para kernels CUDA , como SPIR-V, mas apenas para Nvidia) e vejo os parâmetros de referência para fazer determinadas alterações.

Por que a ramificação nas arquiteturas GPU é tão importante?

Por que é ruim ramificar em primeiro lugar? Por que as GPUs tentam evitar ramificações em primeiro lugar? Como as GPUs geralmente usam um esquema no qual os threads compartilham o mesmo ponteiro de instrução . GPUs seguem uma arquitetura SIMDnormalmente, e embora a granularidade disso possa mudar (ou seja, 32 threads para Nvidia, 64 para AMD e outros), em algum nível, um grupo de threads compartilha o mesmo ponteiro de instrução. Isso significa que esses segmentos precisam estar olhando para a mesma linha de código para trabalharem juntos no mesmo problema. Você pode perguntar como eles são capazes de usar as mesmas linhas de código e fazer coisas diferentes? Eles usam valores diferentes nos registros, mas esses registros ainda são usados ​​nas mesmas linhas de código em todo o grupo. O que acontece quando isso deixa de ser o caso? (Ou seja, uma filial?) Se o programa realmente não tem como contornar isso, ele divide o grupo (Nvidia, esses pacotes de 32 threads são chamados de Warp , para a AMD e a academia de computação paralela, é chamado de frente de onda) em dois ou mais grupos diferentes.

Se houver apenas duas linhas de código diferentes nas quais você terminará, os segmentos de trabalho serão divididos entre dois grupos (a partir daqui, eu os chamarei de warps). Vamos assumir a arquitetura da Nvidia, onde o tamanho do warp é 32, se metade desses threads divergir, então você terá 2 warps ocupados por 32 threads ativos, o que torna as coisas metade mais eficientes do ponto de vista computacional até o final. Em muitas arquiteturas a GPU vai tentar remediar esta convergindo tópicos backup em um único warp uma vez que alcançam o mesmo cargo instrução de desvio, ou o compilador irá colocar explicitamente um ponto de sincronização que conta a GPU para tópicos Converge para trás, ou tentar.

por exemplo:

if(a)
    x += z * w;
    q >>= p;
else if(c)
    y -= 3;
r += t;

O encadeamento tem um forte potencial de divergir (caminhos de instruções diferentes), portanto, nesse caso, pode ocorrer convergência r += t;onde os ponteiros de instruções seriam os mesmos novamente. A divergência também pode ocorrer com mais de duas ramificações, resultando em uma utilização de urdidura ainda mais baixa; quatro ramificações significam que 32 threads são divididos em 4 urdiduras, utilização de throughput de 25%. Entretanto, a convergência pode ocultar alguns desses problemas, pois 25% não mantém a taxa de transferência durante todo o programa.

Em GPUs menos sofisticadas, outros problemas podem ocorrer. Em vez de divergir, eles simplesmente calculam todas as ramificações e, em seguida, selecionam a saída no final. Isso pode parecer o mesmo que divergência (ambos têm 1 / n utilização de taxa de transferência), mas existem alguns problemas importantes com a abordagem de duplicação.

Um é o uso de energia, você está usando muito mais energia sempre que um ramo acontece, isso seria ruim para os gpus móveis. A segunda é que a divergência só acontece na Nvidia gpus quando os segmentos da mesma urdidura seguem caminhos diferentes e, portanto, têm um ponteiro de instrução diferente (que é compartilhado a partir do pascal). Portanto, você ainda pode ter ramificações e não ter problemas de taxa de transferência nas GPUs da Nvidia se elas ocorrerem em múltiplos de 32 ou ocorrerem apenas em um único warp de dezenas. se é provável que uma ramificação ocorra, é provável que haja menos threads divergentes e você não terá nenhum problema de ramificação.

Outro problema menor é que, quando você compara GPUs com CPUs, elas geralmente não possuem mecanismos de previsão e outros mecanismos de ramificação robustos, devido à quantidade de hardware que esse mecanismo ocupa; geralmente, você pode ver o preenchimento não operacional nas GPUs modernas por causa disso.

Exemplo prático de diferença arquitetônica da GPU

Agora vamos dar o exemplo de Stephanes e ver como seria a montagem para soluções sem ramificação em duas arquiteturas teóricas.

n = (a==b) ? x : y;

Como Stephane disse, quando o compilador de dispositivos encontra uma ramificação, ele pode decidir usar uma instrução para "escolher" o elemento que acabaria sem penalidade por ramificação. Isso significa que em alguns dispositivos isso seria compilado para algo como

cmpeq rega, regb
// implicit setting of comparison bit used in next part
choose regn, regx, regy

em outras pessoas sem uma instrução de escolha, ela pode ser compilada para

n = ((a==b))* x + (!(a==b))* y

que pode se parecer com:

cmpeq rega regb
// implicit setting of comparison bit used in next part
mul regn regcmp regx
xor regcmp regcmp 1
mul regresult regcmp regy
mul regn regn regresult

que é sem ramificação e equivalente, mas tem muito mais instruções. Como o exemplo de Stephanes provavelmente será compilado para seus respectivos sistemas, não faz muito sentido tentar descobrir manualmente a matemática para remover a ramificação, pois o compilador da primeira arquitetura pode decidir compilar para a segunda forma em vez de a forma mais rápida.


5

Concordo com tudo o que foi dito na resposta de Stephanie Hockenhull. Para expandir o último ponto:

Você nunca sabe de antemão como um compilador GLSL específico ou uma GPU específica serão executados até que você faça o benchmark.

Absolutamente verdadeiro. Além disso, vejo esse tipo de pergunta surgir com bastante frequência. Mas, na prática, raramente vi um shader de fragmento sendo a fonte de um problema de desempenho. É muito mais comum que outros fatores estejam causando problemas, como muitas leituras de estado da GPU, trocando muitos buffers, muito trabalho em uma única chamada de empate etc.

Em outras palavras, antes de se preocupar em otimizar um sombreador, crie um perfil de todo o aplicativo e verifique se os sombreadores estão causando a desaceleração.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.