¿== causa ramificación en GLSL?


27

Tratando de averiguar exactamente qué causa la ramificación y qué no en GLSL.

Estoy haciendo esto mucho en mi sombreador:

float(a==b)

Lo uso para simular sentencias if, sin ramificación condicional ... pero ¿es efectivo? No tengo declaraciones if en ninguna parte de mi programa ahora, ni tengo ningún bucle.

EDITAR: Para aclarar, hago cosas como esta en mi 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;

EDITAR: Sé por qué no quiero ramificar. Sé lo que es ramificar. Me alegra que estés enseñando a los niños sobre ramificaciones, pero me gustaría conocerme sobre operadores booleanos (y operaciones bit a bit, pero estoy bastante seguro de que están bien)

Respuestas:


42

Lo que causa la ramificación en GLSL depende del modelo de GPU y la versión del controlador OpenGL.

La mayoría de las GPU parecen tener una forma de operación de "seleccionar uno de dos valores" que no tiene costo de ramificación:

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

y a veces incluso cosas como:

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

se reducirá a unas pocas operaciones de valor de selección sin penalización de ramificación.

Algunas GPU / Drivers tienen (¿tuvieron?) Una pequeña penalización en el operador de comparación entre dos valores, pero una operación más rápida en comparación con cero.

Donde podría ser más rápido hacer:

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

en lugar de comparar (tmp1 != tmp2)directamente, pero esto depende mucho de la GPU y del controlador, por lo que, a menos que esté apuntando a una GPU muy específica y no a otras, recomiendo usar la operación de comparación y dejar ese trabajo de optimización al controlador OpenGL ya que otro controlador podría tener un problema con la forma más larga y sea más rápido con la forma más simple y legible.

Las "ramas" tampoco son siempre malas. Por ejemplo, en la GPU SGX530 utilizada en OpenPandora, este sombreador scale2x (30 ms):

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

Terminó dramáticamente más rápido que este sombreador equivalente (80 ms):

    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;

Nunca se sabe de antemano cómo funcionará un compilador GLSL específico o una GPU específica hasta que lo compare.


Para agregar el punto de referencia (incluso aunque no tenga números de sincronización reales y código de sombreador para presentarle esta parte), actualmente lo uso como mi hardware de prueba habitual:

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

Como una amplia gama de modelos de GPU diferentes y comunes para probar.

Probar cada uno de ellos con controladores OpenGL y OpenCL de código abierto de Windows, Linux y propietarios de Linux.

Y cada vez que intento micro-optimizar el sombreador GLSL (como en el ejemplo SGX530 anterior) o las operaciones de OpenCL para una combinación particular de GPU / Controlador , termino dañando el rendimiento en más de una de las otras GPU / Controladores.

Entonces, aparte de reducir claramente la complejidad matemática de alto nivel (por ejemplo: convertir 5 divisiones idénticas en un solo recíproco y 5 multiplicaciones en su lugar) y reducir las búsquedas de textura / ancho de banda, lo más probable es que sea una pérdida de tiempo.

Cada GPU es muy diferente de las demás.

Si estaría trabajando específicamente en (a) consolas de juegos con una GPU específica, esta sería una historia diferente.

El otro aspecto (menos significativo para los desarrolladores de juegos pequeños pero aún notable) es que los controladores de la GPU de la computadora podrían algún día reemplazar silenciosamente sus sombreadores ( si su juego se vuelve lo suficientemente popular ) con los reescritos personalizados optimizados para esa GPU en particular. Hacer que todo funcione para ti.

Lo harán para juegos populares que se usan con frecuencia como puntos de referencia.

O si le das a tus jugadores acceso a los sombreadores para que puedan editarlos ellos mismos fácilmente, algunos de ellos podrían exprimir algunos FPS adicionales para su propio beneficio.

Por ejemplo, hay paquetes de sombreado y textura hechos por los fanáticos para que Oblivion aumente drásticamente la velocidad de fotogramas en hardware que de otro modo apenas se podría reproducir.

Y, por último, una vez que su sombreador se vuelve lo suficientemente complejo, su juego casi se completa y comienza a probar en diferentes hardware, estará lo suficientemente ocupado simplemente arreglando sus sombreadores para que funcionen en una variedad de GPU, ya que se debe a varios errores que no podrá tener tiempo para optimizarlos a ese grado.


"O si le das acceso a tus jugadores a los sombreadores para que puedan editarlos fácilmente ellos mismos ..." Ya que has mencionado esto, ¿cuál podría ser tu enfoque para los sombreadores de wallhack y similares? Sistema de honor, verificado, informes ...? Me gusta la idea de que los lobbies se restrinjan a los mismos sombreadores / activos, sean los que sean, ya que las posturas sobre el realismo máximo / mínimo / escalable, las vulnerabilidades, etc., deberían unir a jugadores y modders para fomentar la revisión, la colaboración, etc. recordar que esta fue la forma en que funcionó Gary's Mod, pero estoy fuera del circuito.
John P

1
@JohnP Seguridad sabio todo lo que se supone que el cliente no está comprometido no funciona de todos modos. Por supuesto, si no quieres que la gente edite sus sombreadores, no tiene sentido exponerlos, pero realmente no ayuda mucho con la seguridad. Su estrategia para detectar cosas como wallhacks debería tratar el desorden del lado del cliente con las cosas como una primera barrera baja, y podría decirse que podría haber un beneficio mayor para permitir la modificación de la luz como en esta respuesta si no conduce a una ventaja injusta detectable para el jugador .
Cubic

8
@JohnP Si no quieres que los jugadores también vean a través de las paredes, no dejes que el servidor les envíe información sobre lo que hay detrás de la pared.
Polygnome

1
Eso es todo: no estoy en contra de la piratería de muros entre jugadores que les gusta por cualquier razón. Sin embargo, como jugador, abandoné varios títulos AAA porque, entre otras razones, hicieron ejemplos de modificadores estéticos mientras dinero / XP / etc. los piratas informáticos quedaron ilesos (que ganaban dinero real con los que estaban lo suficientemente frustrados como para pagar), carecieron de personal y automatizaron su sistema de informes y apelaciones, y se aseguraron de que los juegos vivieran y murieran por la cantidad de servidores que querían mantener con vida. Esperaba que pudiera haber un enfoque más descentralizado como desarrollador y como jugador.
John P

No, no hago en línea si en cualquier lugar. Acabo de flotar (declaración booleana) * (algo)
Geklmintendon't of Awesome

7

La respuesta de @Stephane Hockenhull le da más o menos lo que necesita saber, dependerá completamente del hardware.

Pero déjame darte algunos ejemplos de cómo puede depender del hardware, y por qué la ramificación es incluso un problema, qué hace la GPU detrás de escena cuando se realiza la ramificación .

Mi enfoque es principalmente con Nvidia, tengo algo de experiencia con la programación CUDA de bajo nivel, y veo qué PTX ( IR para los núcleos CUDA , como SPIR-V pero solo para Nvidia) se genera y veo los puntos de referencia para realizar ciertos cambios.

¿Por qué es tan importante la ramificación en arquitecturas GPU?

¿Por qué es malo ramificarse en primer lugar? ¿Por qué las GPU intentan evitar ramificarse en primer lugar? Debido a que las GPU generalmente usan un esquema donde los hilos comparten el mismo puntero de instrucción . Las GPU siguen una arquitectura SIMDtípicamente, y aunque la granularidad de eso puede cambiar (es decir, 32 hilos para Nvidia, 64 para AMD y otros), en algún nivel un grupo de hilos comparte el mismo puntero de instrucción. Esto significa que esos hilos deben estar mirando la misma línea de código para poder trabajar juntos en el mismo problema. Puede preguntar cómo pueden usar las mismas líneas de código y hacer cosas diferentes. Usan diferentes valores en los registros, pero esos registros todavía se usan en las mismas líneas de código en todo el grupo. ¿Qué sucede cuando eso deja de ser el caso? (¿Es una rama?) Si el programa realmente no tiene forma de evitarlo, divide el grupo (Nvidia, tales paquetes de 32 hilos se llaman Warp , para AMD y la academia de computación paralela, se lo conoce como frente de onda) en dos o más grupos diferentes.

Si solo hay dos líneas de código diferentes en las que terminaría, entonces los hilos de trabajo se dividen en dos grupos (desde aquí uno los llamaré warps). Supongamos que la arquitectura de Nvidia, donde el tamaño de la urdimbre es 32, si la mitad de estos hilos divergen, entonces tendrás 2 urdimbres ocupados por 32 hilos activos, lo que hace que las cosas sean la mitad de eficientes desde un extremo computacional hasta el final. En muchas arquitecturas, la GPU intentará remediar esto mediante la convergencia de subprocesos en una sola urdimbre una vez que lleguen a la misma rama de instrucción, o el compilador pondrá explícitamente un punto de sincronización que le indica a la GPU que converja los subprocesos o lo intente.

por ejemplo:

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

El hilo tiene un gran potencial para divergir (rutas de instrucción diferentes), por lo que en tal caso es posible que ocurra una convergencia en la r += t;que los punteros de instrucción volverían a ser los mismos. La divergencia también puede ocurrir con más de dos ramificaciones, lo que resulta en una utilización de urdimbre aún menor, cuatro ramificaciones significan que 32 hilos se dividen en 4 urdimbres, 25% de utilización de rendimiento. Sin embargo, la convergencia puede ocultar algunos de estos problemas, ya que el 25% no mantiene el rendimiento en todo el programa.

En GPU menos sofisticadas, pueden ocurrir otros problemas. En lugar de divergir, simplemente calculan todas las ramas y luego seleccionan la salida al final. Esto puede parecer lo mismo que la divergencia (ambos tienen una utilización de rendimiento de 1 / n), pero hay algunos problemas importantes con el enfoque de duplicación.

Uno es el uso de energía, está usando mucha más energía cuando ocurre una rama, esto sería malo para gpus móviles. En segundo lugar, esa divergencia solo ocurre en Nvidia gpus cuando los hilos de la misma urdimbre toman caminos diferentes y, por lo tanto, tienen un puntero de instrucción diferente (que se comparte a partir de pascal). Por lo tanto, aún puede tener ramificaciones y no tener problemas de rendimiento en las GPU Nvidia si se producen en múltiplos de 32 o solo ocurren en una sola deformación de docenas. Si es probable que se produzca una ramificación, es más probable que se diferencien menos hilos y, de todos modos, no tendrá un problema de ramificación.

Otro problema menor es cuando se comparan las GPU con las CPU, a menudo no tienen mecanismos de predicción y otros mecanismos de rama robustos debido a la cantidad de hardware que ocupan esos mecanismos, a menudo se puede ver el llenado sin operación en las GPU modernas debido a esto.

Ejemplo práctico de diferencia arquitectónica de GPU

Ahora tomemos el ejemplo de Stephanes y veamos cómo se vería el ensamblaje para soluciones sin ramificación en dos arquitecturas teóricas.

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

Como dijo Stephane, cuando el compilador del dispositivo encuentra una rama, puede decidir usar una instrucción para "elegir" el elemento que terminaría sin penalización de rama. Esto significa que en algunos dispositivos esto se compilaría en algo como

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

en otros sin una instrucción de elección, podría compilarse para

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

que podría verse así:

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 no tiene ramificaciones y es equivalente, pero requiere más instrucciones. Debido a que el ejemplo de Stephanes probablemente se compilará en cualquiera de sus respectivos sistemas, no tiene mucho sentido tratar de resolver manualmente las matemáticas para eliminar la ramificación, ya que el compilador de la primera arquitectura puede decidir compilar a la segunda forma en lugar de La forma más rápida.


5

Estoy de acuerdo con todo lo dicho en la respuesta de @Stephane Hockenhull. Para ampliar sobre el último punto:

Nunca se sabe de antemano cómo funcionará un compilador GLSL específico o una GPU específica hasta que lo compare.

Totalmente cierto. Además, veo que este tipo de preguntas surgen con bastante frecuencia. Pero en la práctica, rara vez he visto que un sombreador de fragmentos sea la fuente de un problema de rendimiento. Es mucho más común que otros factores estén causando problemas como demasiadas lecturas de estado de la GPU, intercambiando demasiados búferes, demasiado trabajo en una sola llamada de sorteo, etc.

En otras palabras, antes de preocuparse por la microoptimización de un sombreador, perfile toda su aplicación y asegúrese de que los sombreadores sean lo que está causando su desaceleración.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.