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.
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:
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.
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
en otros sin una instrucción de elección, podría compilarse para
que podría verse así:
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.
fuente
Estoy de acuerdo con todo lo dicho en la respuesta de @Stephane Hockenhull. Para ampliar sobre el último punto:
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.
fuente