¿Evitar si las declaraciones en sombreadores DirectX 10?

14

He escuchado que si las declaraciones deben evitarse en los sombreadores, porque ambas partes de las declaraciones se ejecutarán y luego se eliminará el error (lo que perjudica el rendimiento).

¿Sigue siendo un problema en DirectX 10? Alguien me dijo que en él solo se ejecutará la rama correcta.

Para la ilustración tengo el 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;
}

¿Hay alguna otra forma de hacerlo más rápido?

Si es así, ¿cómo hacerlo?

Ambas ramas se ven similares, la única diferencia son los valores de "constantes" ( y1, y2, b1, b2son los mismos para todos los píxeles en Pixel Shader).

PolGraphic
fuente
1
Honestamente, esa es una optimización muy prematura, simplemente no los cambie hasta que haya comparado su código y esté 100% ese sombreador es un cuello de botella.
pwny

Respuestas:

17

Muchas reglas para sombreadores de microoptimización son las mismas que para las CPU tradicionales con extensiones vectoriales. Aquí hay algunos consejos:

  • hay funciones de prueba incorporadas ( test, lerp/ mix)
  • agregar dos vectores tiene el mismo costo que agregar dos flotadores
  • swizzling es gratis

Es cierto que las sucursales son más baratas en hardware moderno de lo que solían ser, pero es mejor evitarlas si es posible. Mediante el uso de funciones de swizzling y prueba, puede volver a escribir su sombreador sin pruebas:

/* 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 stepy lerpes un idioma muy común para elegir entre dos valores.

sam hocevar
fuente
6

En general está bien. Los sombreadores se ejecutarán en grupos de vértices o píxeles (diferentes proveedores tienen una terminología diferente para estos, así que me mantengo alejado de eso) y si todos los vértices o píxeles en un grupo toman la misma ruta, el costo de ramificación es insignificante.

También debe confiar en el compilador de sombreadores. El código HLSL que escriba no debe verse como una representación directa del bytecode o incluso el ensamblaje en el que se compilará, y el compilador es perfectamente libre de convertirlo en algo equivalente pero que evita la ramificación (por ejemplo, un lerp a veces puede ser una conversión preferida) Por otro lado, si el compilador determina que realizar una rama es realmente la ruta más rápida, la compilará en una rama. Ver el ensamblaje generado en PIX o una herramienta similar puede ser muy útil aquí.

Finalmente, la vieja sabiduría aún se mantiene aquí: perfílela, determine si en realidad es un problema de rendimiento para usted, y enfréntela entonces, no antes. Asumir que algo puede ser un problema de rendimiento y actuar de acuerdo con esa suposición solo generará un gran riesgo de mayores problemas más adelante.

Maximus Minimus
fuente
4

Cita del enlace / artículo publicado por Robert Rouhani:

"Los códigos de condición (predication) se usan en arquitecturas más antiguas para emular una verdadera ramificación. Las instrucciones if-then compiladas en estas arquitecturas deben evaluar las instrucciones de ramificación tomadas y no tomadas en todos los fragmentos. La condición de ramificación se evalúa y se establece un código de condición. las instrucciones en cada parte de la rama deben verificar el valor del código de condición antes de escribir sus resultados en los registros. Como resultado, solo las instrucciones en las ramas tomadas escriben su salida. Por lo tanto, en estas arquitecturas todas las ramas cuestan tanto como ambas partes ramificación, más el costo de evaluar la condición de la ramificación. La ramificación se debe usar con moderación en tales arquitecturas. Las GPU NVIDIA GeForce FX Series usan emulación de ramificación de código de condición en sus procesadores de fragmentos ".

Como sugirió mh01 ("Ver el ensamblado generado en PIX o una herramienta similar puede ser muy útil aquí"), debe usar una herramienta de compilación para examinar la salida. En mi experiencia, la herramienta Cg de nVidia (Cg todavía se usa ampliamente hoy en día debido a sus capacidades multiplataforma) dio una ilustración perfecta del comportamiento mencionado en el párrafo de códigos de condición (predicción) de gemas de GPU . Por lo tanto, independientemente del valor de activación, ambas ramas se evaluaron por fragmento y solo al final, la correcta se colocó en el registro de salida. Sin embargo, se perdió el tiempo de cálculo. En aquel entonces, pensé que la ramificación ayudaría al rendimiento, especialmente porque todosLos fragmentos en ese sombreador dependían de un valor uniforme para decidir la rama correcta, eso no sucedió como se esperaba. Entonces, una advertencia importante aquí (por ejemplo, evitar ubershaders, posiblemente la mayor fuente de infierno ramificado).

teodron
fuente
2

Si aún no tiene problemas de rendimiento, está bien. El costo de la comparación contra una constante sigue siendo extremadamente barato. Aquí hay una buena lectura sobre la ramificación de GPU: http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter34.html

De todos modos, aquí hay un fragmento de código que va a ser mucho peor que la instrucción if (y es mucho menos legible / mantenible), pero aún así se deshace de él:

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

x = 10 * y + b;

Tenga en cuenta que estoy asumiendo que x está limitado al rango [0, 1]. Esto no funcionará si x> = 2 o x <0.

Lo que corta es convertir x en uno 0o dos 1y multiplicar el incorrecto por 0 y el otro por 1.

Robert Rouhani
fuente
Como la prueba original es if(x<0.5)el valor para fxdebería ser round(x)o floor(x + 0.5).
sam hocevar
1

Hay múltiples instrucciones capaces de hacer condiciones sin ramificación;

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);
}

Además de algunos 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;
}

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

Alexis Paques
fuente