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é.
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:
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.
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
sur les autres sans instruction de choix, il peut être compilé pour
qui pourrait ressembler à:
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.
la source
Je suis d'accord avec tout ce qui a été dit dans la réponse de @Stephane Hockenhull. Pour développer le dernier point:
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.
la source