Evite as declarações if no DirectX 10 shaders?

14

Ouvi dizer que se as declarações devem ser evitadas em shaders, porque ambas as partes serão executadas e o errado será descartado (o que prejudica o desempenho).

Ainda existe um problema no DirectX 10? Alguém me disse que nele apenas o ramo certo será executado.

Para a ilustração, tenho o código:

float y1 = 5; float y2 = 6; float b1 = 2; float b2 = 3;

if(x>0.5){
    x = 10 * y1 + b1;
}else{
    x = 10 * y2 + b2;
}

Existe outra maneira de torná-lo mais rápido?

Se sim, como faz?

Os dois ramos são parecidos, a única diferença são os valores de "constantes" ( y1, y2, b1, b2são iguais para todos os pixels no Pixel Shader).

PolGraphic
fonte
1
Honestamente, isso é otimização muito prematura, só não os altere até você comparar seu código e estar 100% certo de que o shader é um gargalo.
Pwny

Respostas:

17

Muitas regras para shaders de otimização micro são as mesmas que para CPUs tradicionais com extensões de vetor. Aqui estão algumas dicas:

  • existem funções de teste integradas ( test, lerp/ mix)
  • adicionar dois vetores tem o mesmo custo que adicionar dois carros alegóricos
  • swizzling é grátis

É verdade que as filiais são mais baratas no hardware moderno do que costumavam ser, mas ainda é melhor evitá-las, se possível. Usando as funções swizzling e test, você pode reescrever seu shader sem testes:

/* y1, y2, b1, b2 */
float4 constants = float4(5, 6, 2, 3);

float2 tmp = 10 * constants.xy + constants.zw;
x = lerp(tmp[1], tmp[0], step(x, 0.5));

Usar stepe lerpé um idioma muito comum para escolher entre dois valores.

sam hocevar
fonte
6

Geralmente está tudo bem. Os sombreadores serão executados em grupos de vértices ou pixels (diferentes fornecedores têm uma terminologia diferente para isso, então estou mantendo isso distante) e se todos os vértices ou pixels de um grupo seguirem o mesmo caminho, o custo da ramificação será insignificante.

Você também precisa confiar no compilador de sombreador. O código HLSL que você escreve não deve ser visto como uma representação direta do bytecode ou mesmo da montagem que será compilada, e o compilador é perfeitamente livre para convertê-lo em algo equivalente, mas evita a ramificação (por exemplo, um lerp às vezes pode ser uma conversão preferida). Por outro lado, se o compilador determinar que a execução de uma ramificação é realmente o caminho mais rápido, ela será compilada em uma ramificação. Visualizar a montagem gerada no PIX ou uma ferramenta semelhante pode ser muito útil aqui.

Finalmente, a velha sabedoria ainda se mantém aqui - analise-a, determine se realmente é um problema de desempenho para você e resolva-o então, não antes. Assumindo que algo pode ser um problema de desempenho e agir de acordo com essa suposição, haverá um risco enorme de problemas maiores posteriormente.

Maximus Minimus
fonte
4

Citação do link / artigo postado por Robert Rouhani:

"Os códigos de condição (predicação) são usados ​​em arquiteturas mais antigas para emular a ramificação verdadeira. As instruções if-then compiladas nessas arquiteturas devem avaliar as instruções de ramificação obtidas e não realizadas em todos os fragmentos. A condição de ramificação é avaliada e um código de condição é definido. As instruções em cada parte da ramificação devem verificar o valor do código de condição antes de gravar seus resultados nos registros.Como resultado, apenas as instruções nas ramificações obtidas gravam sua saída. Portanto, nessas arquiteturas, todas as ramificações custam tanto quanto as duas partes do ramificação, mais o custo de avaliar a condição da ramificação. A ramificação deve ser usada com moderação nessas arquiteturas. As GPUs NVIDIA GeForce Série FX usam emulação de ramificação com código de condição em seus processadores de fragmento ".

Como mh01 sugeriu ("Visualizar o assembly gerado no PIX ou uma ferramenta semelhante pode ser muito útil aqui."), Você deve usar uma ferramenta de compilador para examinar a saída. Na minha experiência, a ferramenta Cg da nVidia (a Cg ainda é amplamente usada hoje por causa de suas capacidades de plataforma cruzada) deu uma ilustração perfeita do comportamento mencionado nos fragmentos de códigos de condição de parágrafo dos . Portanto, independentemente do valor do acionador, ambas as ramificações foram avaliadas por fragmento e, somente no final, a correta foi colocada no registro de saída. No entanto, o tempo de computação foi desperdiçado. Naquela época, eu pensava que a ramificação ajudaria o desempenho, principalmente porque todos gemas (predicação) da GPU naquele shader que contava com um valor uniforme para decidir sobre o ramo certo - isso não aconteceu como pretendido. Portanto, uma grande ressalva aqui (por exemplo, evite as trepadeiras - possivelmente a maior fonte do inferno).

teodron
fonte
2

Se você ainda não está tendo problemas de desempenho, tudo bem. O custo para comparação com uma constante ainda é extremamente barato. Aqui está uma boa leitura sobre a ramificação da GPU: http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter34.html

Independentemente disso, aqui está um trecho de código que será pré-formado muito pior que a instrução if (e é muito menos legível / sustentável), mas ainda assim se livra dela:

int fx = floor(x);
int y = (fx * y2) + ((1- fx) * y1);
int b = (fx * b2) + ((1 -fx) * b1);

x = 10 * y + b;

Note que estou assumindo que x é limitado ao intervalo [0, 1]. Isso não funcionará se x> = 2 ou x <0.

O que esse snippet faz é converter x em um 0ou 1e multiplicar o errado por 0 e o outro por 1.

Robert Rouhani
fonte
Como o teste original é if(x<0.5)o valor para fxdeve ser round(x)ou floor(x + 0.5).
Sam Hocevar
1

Existem várias instruções capazes de fazer condições sem ramificação;

vec4 when_eq(vec4 x, vec4 y) {
  return 1.0 - abs(sign(x - y));
}

vec4 when_neq(vec4 x, vec4 y) {
  return abs(sign(x - y));
}

vec4 when_gt(vec4 x, vec4 y) {
  return max(sign(x - y), 0.0);
}

vec4 when_lt(vec4 x, vec4 y) {
  return max(sign(y - x), 0.0);
}

vec4 when_ge(vec4 x, vec4 y) {
  return 1.0 - when_lt(x, y);
}

vec4 when_le(vec4 x, vec4 y) {
  return 1.0 - when_gt(x, y);
}

Além de alguns operadores lógicos;

vec4 and(vec4 a, vec4 b) {
  return a * b;
}

vec4 or(vec4 a, vec4 b) {
  return min(a + b, 1.0);
}

vec4 xor(vec4 a, vec4 b) {
  return (a + b) % 2.0;
}

vec4 not(vec4 a) {
  return 1.0 - a;
}

fonte: http://theorangeduck.com/page/avoiding-shader-conditionals

Alexis Paques
fonte