Je comprends que cette question peut sembler sans fondement, mais si quelqu'un sait quelque chose de théorique/a une expérience pratique sur ce sujet, ce serait bien si vous la partagez .
J'essaie d'optimiser l'un de mes anciens shaders, qui utilise beaucoup de recherches de texture.
J'ai des cartes diffuses, normales et spéculaires pour chacun des trois plans de cartographie possibles et pour certains visages qui sont proches de l'utilisateur, je dois également appliquer les techniques de mappage, qui apportent également beaucoup de recherches de texture (comme parallax occlusion mapping
).
Le profilage a montré que les recherches de texture sont le goulot d'étranglement du shader et je suis prêt à en supprimer certaines. Pour certains cas des paramètres d'entrée je sais déjà qu'une partie des recherches de texture serait inutile et la solution évidente consiste à faire quelque chose comme (pseudocode) :
if (part_actually_needed) {
perform lookups;
perform other steps specific for THIS PART;
}
// All other parts.
Maintenant - voici la question.
Je ne me souviens pas exactement (c'est pourquoi j'ai déclaré que la question pourrait être sans fondement), mais dans certains articles, j'ai récemment lu (malheureusement, je ne me souviens pas du nom) quelque chose de similaire à ce qui suit a été déclaré:
Les performances de la technique présentée dépendent de l'efficacité avec laquelle BRANCHEMENT CONDITIONNEL BASÉ SUR LE MATÉRIEL est implémenté.
Je me suis souvenu de ce genre de déclaration juste avant de commencer à refactoriser un grand nombre de shaders et à implémenter cette optimisation basée sur if
I parlait.
Donc - juste avant de commencer - quelqu'un sait-il quelque chose sur l'efficacité du branchement dans les shaders? Pourquoi la ramification pourrait-elle affecter gravement les performances des shaders?
Et est-il même possible que je ne puisse qu'aggraver les performances réelles avec la branche basée sur if
?
Vous pourriez dire - essayez de voir. Oui, c'est ce que je vais faire si personne ici ne m'aide :)
Mais encore, ce qui dans le cas if
peut être efficace pour les nouveaux GPU pourrait être un cauchemar pour les plus anciens. Et ce genre de problème est très difficile à prévoir, sauf si vous avez beaucoup de GPU différents (ce n'est pas mon cas)
Donc, si quelqu'un en sait quelque chose ou a une expérience de benchmarking pour ces types de shaders, j'apprécierais vraiment votre aide.
Peu de cellules cérébrales qui fonctionnent réellement continuent à me dire que la ramification sur les GPU pourrait être loin d'être aussi efficace que la ramification pour le CPU (qui a généralement des moyens extrêmement efficaces de prédire les branches et d'éliminer les échecs de cache) simplement parce que c'est un GPU (ou que pourrait être difficile/impossible à implémenter sur le GPU).
Malheureusement, je ne sais pas si cette déclaration a quelque chose en commun avec la situation réelle ...
Malheureusement, je pense que la vraie réponse ici est de faire des tests pratiques avec un analyseur de performances de votre cas spécifique, sur votre matériel cible. Particulièrement étant donné qu'il semble que vous soyez au stade de l'optimisation du projet; c'est la seule façon de prendre en compte le fait que le matériel change fréquemment et la nature du shader spécifique.
Sur un processeur, si vous obtenez une branche mal prédite, vous provoquerez un vidage du pipeline et puisque les pipelines du processeur sont si profonds, vous perdrez effectivement quelque chose de l'ordre de 20 cycles ou plus. Sur le GPU, les choses sont un peu différentes; le pipeline est probablement beaucoup moins profond, mais il n'y a pas de prédiction de branche et tout le code du shader sera en mémoire rapide - mais ce n'est pas la vraie différence.
Il est difficile de connaître les détails exacts de tout ce qui se passe, car nVidia et ATI sont relativement étroits, mais l'essentiel est que les GPU sont conçus pour une exécution massivement parallèle. Il existe de nombreux cœurs de shader asynchrones, mais chaque cœur est à nouveau conçu pour exécuter plusieurs threads. Ma compréhension est que chaque noyau s'attend à exécuter la même instruction sur tous ses threads sur un cycle donné (nVidia appelle cette collection de threads une "déformation").
Dans ce cas, un thread peut représenter un sommet, un élément de géométrie ou un pixel/fragment et une déformation est une collection d'environ 32 d'entre eux. Pour les pixels, ils sont susceptibles d'être des pixels proches les uns des autres à l'écran. Le problème est que si au sein d'une chaîne, différents threads prennent des décisions différentes lors du saut conditionnel, la chaîne a divergé et n'exécute plus la même instruction pour chaque thread. Le matériel peut gérer cela, mais il n'est pas entièrement clair (du moins pour moi) comment il le fait. Il est également susceptible d'être géré légèrement différemment pour chaque génération successive de cartes. Les nVidias les plus récents et les plus généraux compatibles CUDA/compute-shader pourraient avoir la meilleure implémentation; les anciennes cartes peuvent avoir une implémentation moins bonne. Le pire des cas est que de nombreux threads exécutent les deux côtés des instructions if/else.
L'une des grandes astuces avec les shaders est d'apprendre à tirer parti de ce paradigme massivement parallèle. Parfois, cela signifie utiliser des passes supplémentaires, des tampons temporaires hors écran et des tampons de pochoir pour pousser la logique hors des shaders et sur le CPU. Parfois, une optimisation peut sembler brûler plus de cycles, mais elle pourrait en fait réduire certains frais généraux cachés.
Notez également que vous pouvez explicitement marquer si les instructions dans les shaders DirectX comme [branche] ou [aplatir]. Le style aplati vous donne le bon résultat, mais exécute toujours tout dans les instructions. Si vous n'en choisissez pas explicitement, le compilateur peut en choisir un pour vous - et peut choisir [aplatir], ce qui n'est pas bon pour votre exemple.
Une chose à retenir est que si vous sautez par-dessus la première recherche de texture, cela déroutera les calculs de dérivée des coordonnées de texture du matériel. Vous obtiendrez des erreurs de compilation et il est préférable de ne pas le faire, sinon vous risquez de manquer une partie du meilleur support de texturation.
Si la condition est uniforme (c'est-à-dire constante pour toute la passe), alors la branche est essentiellement libre car le framework compilera essentiellement deux versions du shader (branche prise et non) et choisira l'une d'entre elles pour la passe entière en fonction de votre entrée variable. Dans ce cas, optez pour l'instruction if
car will accélérera votre shader.
Si la condition varie par sommet/pixel, cela peut en effet dégrader les performances et les anciens modèles de shaders ne prennent même pas en charge la ramification dynamique.
Dans de nombreux cas, les deux branches peuvent être calculées et mélangées par condition comme interpolateur. Cette approche fonctionne beaucoup plus rapidement que la branche. Peut également être utilisé sur le CPU. Par exemple:
...
vec3 c = vec3(1.0, 0.0, 0.0); if (a == b) c = vec3(0.0, 1.0, 0.0);
pourrait être remplacé par:
vec3 c = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), (a == b));
...
Voici une référence de performance réelle sur un Kindle Fire:
Dans le fragment shader ...
Cela fonctionne à 20fps:
lowp vec4 a = vec4(0.0, 0.0, 0.0, 0.0);
if (a.r == 0.0)
gl_FragColor = texture2D ( texture1, TextureCoordOut );
Cela fonctionne à 60fps:
gl_FragColor = texture2D ( texture1, TextureCoordOut );
Je ne sais pas pour les optimisations basées sur if, mais que diriez-vous de simplement créer toutes les permutations des recherches de texture dont vous pensez avoir besoin, chacune son propre shader, et utilisez simplement le bon shader pour la bonne situation (selon sur quelle texture recherche un modèle particulier, ou une partie de votre modèle, nécessaire). Je pense que nous avons fait quelque chose comme ça sur Bully pour Xbox 360.