== provoque-t-il une ramification dans GLSL?


27

Essayer de comprendre exactement ce qui cause la ramification et ce qui ne le fait pas dans GLSL.

Je fais beaucoup ça dans mon shader:

float(a==b)

Je l'utilise pour simuler des instructions if, sans branchement conditionnel ... mais est-ce efficace? Je n'ai pas d'instructions if maintenant dans mon programme, ni de boucles.

EDIT: Pour clarifier, je fais des trucs comme ça dans mon code:

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: Je sais pourquoi je ne veux pas de branchement. Je sais ce qu'est la ramification. Je suis content que vous enseigniez aux enfants la ramification, mais j'aimerais me connaître sur les opérateurs booléens (et les opérations au niveau du bit, mais je suis presque sûr que ça va)

Réponses:


42

Les causes de branchement dans GLSL dépendent du modèle GPU et de la version du pilote OpenGL.

La plupart des GPU semblent avoir une forme d'opération "sélectionner l'une des deux valeurs" qui n'a pas de coût de branchement:

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

et parfois même des choses comme:

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

sera réduit à quelques opérations de sélection de valeur sans pénalité de branchement.

Certains GPU / Drivers ont (eu?) Une pénalité sur l'opérateur de comparaison entre deux valeurs mais une opération plus rapide sur la comparaison contre zéro.

Où il pourrait être plus rapide de le faire:

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

plutôt que de comparer (tmp1 != tmp2)directement, mais cela dépend beaucoup du GPU et du pilote, donc à moins que vous ne cibliez un GPU très spécifique et aucun autre, je recommande d'utiliser l'opération de comparaison et de laisser cette tâche d'optimisation au pilote OpenGL car un autre pilote pourrait avoir un problème avec le formulaire plus long et soyez plus rapide avec la manière la plus simple et la plus lisible.

Les "succursales" ne sont pas toujours une mauvaise chose non plus. Par exemple sur le GPU SGX530 utilisé dans OpenPandora, ce 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;
    }

A fini considérablement plus rapide que ce shader équivalent (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;

Vous ne savez jamais à l'avance comment un compilateur GLSL spécifique ou un GPU spécifique fonctionnera jusqu'à ce que vous le compariez.


Pour ajouter le point (même si je n'ai pas de numéros de synchronisation réels et de code de shader à vous présenter pour cette partie), j'utilise actuellement comme matériel de test régulier:

  • Intel HD Graphics 3000
  • Carte graphique Intel HD 405
  • nVidia GTX 560M
  • nVidia GTX 960
  • AMD Radeon R7 260X
  • nVidia GTX 1050

En tant que large éventail de modèles de GPU différents et courants à tester.

Tester chacun avec les pilotes OpenGL et OpenCL de Windows, Linux propriétaire et Linux open source.

Et chaque fois que j'essaie de micro-optimiser le shader GLSL (comme dans l'exemple SGX530 ci-dessus) ou les opérations OpenCL pour un combo GPU / Driver particulier, je finis par nuire également aux performances de plusieurs des autres GPU / Drivers.

Donc, à part réduire clairement la complexité mathématique de haut niveau (par exemple: convertir 5 divisions identiques en une seule réciproque et 5 multiplications à la place) et réduire les recherches de texture / bande passante, ce sera probablement une perte de temps.

Chaque GPU est trop différent des autres.

Si vous travailliez spécifiquement sur une (des) console (s) de jeu avec un GPU spécifique, ce serait une autre histoire.

L'autre aspect (moins important pour les développeurs de petits jeux mais toujours notable) est que les pilotes de GPU informatiques pourraient un jour remplacer silencieusement vos shaders ( si votre jeu devient assez populaire ) par des pilotes personnalisés réécrits optimisés pour ce GPU particulier. Faire tout cela fonctionne pour vous.

Ils le feront pour les jeux populaires qui sont fréquemment utilisés comme références.

Ou si vous donnez à vos joueurs l'accès aux shaders afin qu'ils puissent facilement les éditer eux-mêmes, certains d'entre eux pourraient serrer quelques FPS supplémentaires à leur propre avantage.

Par exemple, il existe des packs de shaders et de textures créés par des fans pour Oblivion afin d'augmenter considérablement la fréquence d'images sur du matériel autrement difficilement jouable.

Et enfin, une fois que votre shader est suffisamment complexe, votre jeu est presque terminé et que vous commencez à tester sur différents matériels, vous serez assez occupé à simplement réparer vos shaders pour qu'ils fonctionnent sur une variété de GPU car cela est dû à divers bogues que vous ne rencontrerez pas. avoir le temps de les optimiser à ce degré.


"Ou si vous donnez à vos joueurs accès aux shaders afin qu'ils puissent facilement les modifier eux-mêmes ..." Puisque vous l'avez mentionné, quelle pourrait être votre approche des shaders wallhack et autres? Système d'honneur, vérifié, rapports ...? J'aime l'idée de lobbies limités aux mêmes shaders / actifs, quels qu'ils soient, car les positions sur le réalisme max / min / évolutif, les exploits, etc. devraient rassembler les joueurs et les modders pour encourager la révision, la collaboration, etc. de se rappeler que c'est ainsi que Gary's Mod a fonctionné, mais je suis loin de la boucle.
John P

1
@JohnP En ce qui concerne la sécurité, tout ce qui suppose que le client n'est pas compromis ne fonctionne pas de toute façon. Bien sûr, si vous ne voulez pas que les gens modifient leurs shaders, cela ne sert à rien de les exposer, mais cela n'aide pas vraiment beaucoup en termes de sécurité. Votre stratégie pour détecter des choses comme les wallhacks devrait traiter le côté client avec des choses comme une première barrière basse, et il pourrait sans doute y avoir un avantage plus important à permettre le modding léger comme dans cette réponse si cela ne conduit pas à un avantage injuste détectable pour le joueur. .
Cubic

8
@JohnP Si vous ne voulez pas que les joueurs voient trop à travers les murs, ne laissez pas le serveur leur envoyer des informations sur ce qui se trouve derrière le mur.
Polygnome

1
C'est juste ça - je ne suis pas contre le piratage mural entre les joueurs qui l'aiment pour une raison quelconque. En tant que joueur, cependant, j'ai abandonné plusieurs titres AAA parce que - entre autres raisons - ils ont fait des exemples de moddeurs esthétiques tout en argent / XP / etc. Les pirates informatiques sont restés indemnes (qui ont fait de l'argent réel à ceux qui étaient assez frustrés pour payer), ont manqué de personnel et automatisé leur système de rapport et d'appel, et ont fait en sorte que les jeux vivent et meurent par le nombre de serveurs qu'ils tenaient à garder en vie. J'espérais qu'il pourrait y avoir une approche plus décentralisée en tant que développeur et joueur.
John P

Non, je ne fais pas en ligne si n'importe où. Je fais juste flotter (déclaration booléenne) * (quelque chose)
Geklmintendon't of Awesome

7

La réponse de @Stephane Hockenhull vous donne à peu près ce que vous devez savoir, cela dépendra entièrement du matériel.

Mais permettez - moi de vous donner quelques exemples de la façon dont il peut être dépendant du matériel, et pourquoi la ramification est même un problème du tout, qu'est-ce que le GPU fait dans les coulisses lors de branchement ne ont lieu.

Je me concentre principalement sur Nvidia, j'ai une certaine expérience de la programmation CUDA de bas niveau et je vois quel PTX ( IR pour les noyaux CUDA , comme SPIR-V mais juste pour Nvidia) est généré et je vois les repères pour effectuer certains changements.

Pourquoi la branche dans les architectures GPU est-elle si importante?

Pourquoi est-il mauvais de se ramifier en premier lieu? Pourquoi les GPU essaient-ils d'éviter de se ramifier en premier lieu? Parce que les GPU utilisent généralement un schéma dans lequel les threads partagent le même pointeur d'instruction . Les GPU suivent une architecture SIMDgénéralement, et bien que la granularité de cela puisse changer (par exemple 32 threads pour Nvidia, 64 pour AMD et autres), à un certain niveau, un groupe de threads partage le même pointeur d'instruction. Cela signifie que ces threads doivent regarder la même ligne de code afin de travailler ensemble sur le même problème. Vous pouvez demander comment ils peuvent utiliser les mêmes lignes de code et faire des choses différentes? Ils utilisent des valeurs différentes dans les registres, mais ces registres sont toujours utilisés dans les mêmes lignes de code dans l'ensemble du groupe. Que se passe-t-il lorsque cela cesse d'être le cas? (IE une branche?) Si le programme n'a vraiment aucun moyen de le contourner, il divise le groupe (Nvidia de tels faisceaux de 32 threads sont appelés Warp , pour AMD et le calcul parallèle), il est appelé front d'onde) dans deux ou plusieurs groupes différents.

S'il n'y a que deux lignes de code différentes sur lesquelles vous vous retrouveriez, les threads de travail sont divisés en deux groupes (à partir d'ici, je les appellerai des déformations). Supposons l'architecture Nvidia, où la taille de la chaîne est de 32, si la moitié de ces threads divergent, vous aurez alors 2 warps occupés par 32 threads actifs, ce qui rend les choses à moitié aussi efficaces d'un calcul à une fin de mise. Sur de nombreuses architectures, le GPU tentera de remédier à cela en reconvertissant les threads en une seule chaîne une fois qu'ils auront atteint la même branche de poste d'instructions, ou le compilateur mettra explicitement un point de synchronisation qui indique au GPU de reconvertir les threads, ou d'essayer de le faire.

par exemple:

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

Le thread a un fort potentiel de divergence (chemins d'instruction différents), dans ce cas, vous pourriez avoir une convergence dans r += t;laquelle les pointeurs d'instruction seraient à nouveau les mêmes. La divergence peut également se produire avec plus de deux branches, ce qui entraîne une utilisation encore plus faible de la chaîne, quatre branches signifie que 32 threads sont divisés en 4 chaînes, 25% d'utilisation du débit. La convergence peut cependant masquer certains de ces problèmes, car 25% ne maintiennent pas le débit tout au long du programme.

Sur les GPU moins sophistiqués, d'autres problèmes peuvent survenir. Au lieu de diverger, ils calculent simplement toutes les branches puis sélectionnent la sortie à la fin. Cela peut sembler identique à la divergence (les deux ont une utilisation de débit de 1 / n), mais il y a quelques problèmes majeurs avec l'approche de duplication.

L'un est la consommation d'énergie, vous utilisez beaucoup plus d'énergie chaque fois qu'une branche se produit, ce serait mauvais pour les GPU mobiles. La seconde est que la divergence ne se produit sur les gpus Nvidia que lorsque les threads de la même chaîne prennent des chemins différents et ont donc un pointeur d'instruction différent (qui est partagé à partir de pascal). Ainsi, vous pouvez toujours avoir des branchements et ne pas avoir de problèmes de débit sur les GPU Nvidia s'ils se produisent par multiples de 32 ou ne se produisent que dans une seule chaîne sur des dizaines. si une branche est susceptible de se produire, il est plus probable que moins de threads divergent et vous n'aurez pas de problème de branchement de toute façon.

Un autre petit problème est que lorsque vous comparez des GPU à des CPU, ils n'ont souvent pas de mécanismes de prédiction et d'autres mécanismes de branche robustes en raison de la quantité de matériel que ce mécanisme utilise, vous pouvez souvent voir aucun remplissage sur les GPU modernes à cause de cela.

Exemple pratique de différence d'architecture GPU

Prenons maintenant l'exemple de Stephanes et voyons à quoi ressemblerait l'assemblage pour des solutions sans branche sur deux architectures théoriques.

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

Comme Stephane l'a dit, lorsque le compilateur de périphérique rencontre une branche, il peut décider d'utiliser une instruction pour «choisir» un élément qui finira par ne pas avoir de pénalité de branche. Cela signifie que sur certains appareils, cela serait compilé en quelque chose comme

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

sur les autres sans instruction de choix, il peut être compilé pour

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

qui pourrait ressembler à:

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

qui est sans branche et équivalent, mais prend beaucoup plus d'instructions. Étant donné que l'exemple de Stephanes sera probablement compilé sur l'un ou l'autre sur leurs systèmes respectifs, il n'est pas très logique d'essayer de comprendre manuellement les calculs pour supprimer la ramification nous-mêmes, car le premier compilateur de l'architecture peut décider de compiler vers le second formulaire au lieu de la forme la plus rapide.


5

Je suis d'accord avec tout ce qui a été dit dans la réponse de @Stephane Hockenhull. Pour développer le dernier point:

Vous ne savez jamais à l'avance comment un compilateur GLSL spécifique ou un GPU spécifique fonctionnera jusqu'à ce que vous le compariez.

Absolument vrai. De plus, je vois ce genre de question se poser assez fréquemment. Mais dans la pratique, j'ai rarement vu un shader de fragment être la source d'un problème de performance. Il est beaucoup plus courant que d'autres facteurs provoquent des problèmes tels que trop de lectures d'état du GPU, l'échange de trop de tampons, trop de travail en un seul appel, etc.

En d'autres termes, avant de vous inquiéter de la micro-optimisation d'un shader, profilez l'ensemble de votre application et assurez-vous que les shaders sont à l'origine de votre ralentissement.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.