web-dev-qa-db-fra.com

Les instructions conditionnelles ralentissent-elles les shaders?

Je veux savoir si les "if-statement" dans les shaders (vertex/fragment/pixel ...) ralentissent vraiment les performances du shader. Par exemple:

Est-il préférable d'utiliser ceci:

vec3 output;
output = input*enable + input2*(1-enable);

au lieu d'utiliser ceci:

vec3 output;
if(enable == 1)
{
    output = input;
}
else
{
    output = input2;
}

dans un autre forum, il a été question de cela (2013): http://answers.unity3d.com/questions/442688/shader-if-else-performance.html Ici, les gars disent que les déclarations If sont vraiment mauvaises pour la performance du shader.

Ici aussi, ils parlent de ce qui se trouve dans les déclarations if/else (2012): https://www.opengl.org/discussion_boards/showthread.php/177762-Performance-alternative-for-if- ( -)

peut-être que le matériel ou le shadercompiler sont meilleurs maintenant et qu'ils résolvent d'une manière ou d'une autre ce problème de performances (peut-être pas existant).

MODIFIER:

Dans ce cas, autorisons est une variable uniforme et elle est toujours définie sur 0:

if(enable == 1) //never happens
{
    output = vec4(0,0,0,0);
}
else  //always happens
{
    output = calcPhong(normal, lightDir);
}

Je pense qu'ici nous avons une branche à l'intérieur du shader qui ralentit le shader. Est-ce exact?

Est-il plus logique de créer 2 shaders différents, l’un pour l’autre et l’autre pour la partie if?

47
Thomas

Qu'est-ce qui rend potentiellement les problèmes de performances des instructions if dans les shaders? Cela concerne la manière dont les shaders sont exécutés et où les GPU tirent leurs énormes performances informatiques.

Les invocations de shader séparées sont généralement exécutées en parallèle, en exécutant les mêmes instructions au même moment. Ils les exécutent simplement sur différents ensembles de valeurs d'entrée; ils partagent des uniformes, mais ils ont des registres internes différents. Un terme pour un groupe de shaders exécutant tous la même séquence d'opérations est "front d'onde".

Le problème potentiel de toute forme de ramification conditionnelle est qu’elle peut tout gâcher. Cela entraîne différentes invocations au sein du front d’onde à exécuter différentes séquences de code. C’est un processus très coûteux, dans lequel un nouveau front d’onde doit être créé, les données copiées dessus, etc.

A moins que ... ce ne soit pas le cas.

Par exemple, si la condition est celle qui est prise par à chaque appel dans le front d'onde, aucune divergence d'exécution n'est nécessaire. En tant que tel, le coût de if représente uniquement le coût de la vérification d'une condition.

Supposons donc que vous ayez une branche conditionnelle et supposons que toutes les invocations du front d'onde suivront la même branche. Il existe trois possibilités pour la nature de l'expression dans cette condition:

  • Statique au moment de la compilation. L'expression conditionnelle est entièrement basée sur des constantes à la compilation. En regardant le code, vous savez quelles branches seront prises. Pratiquement tous les compilateurs traitent cela dans le cadre de l'optimisation de base.
  • Ramification statique uniforme. La condition est basée sur des expressions impliquant des choses connues pour être constantes au moment de la compilation (en particulier, les constantes et les valeurs uniform.). Mais la valeur de l'expression ne sera pas connue au moment de la compilation. Le compilateur peut donc être statiquement certain que les fronts d'onde ne seront jamais cassés par ce if, mais il ne peut pas savoir quelle branche sera prise.
  • Branchement dynamique. L'expression conditionnelle contient des termes autres que des constantes et des uniformes. Ici, un compilateur ne peut pas dire a priori si un front d'onde sera divisé ou non. Cela dépendra de l'évaluation au moment de l'exécution de l'expression de condition.

Différents matériels peuvent gérer différents types de branches sans divergence.

De plus, même si une condition est prise par différents fronts d'onde, le compilateur pourrait restructurer le code de manière à ne pas nécessiter de branchement réel . Vous avez donné un bon exemple: output = input*enable + input2*(1-enable); est fonctionnellement équivalent à l'instruction if. Un compilateur pourrait détecter qu'un if est utilisé pour définir une variable et exécuter ainsi les deux côtés. Ceci est souvent fait pour les cas de conditions dynamiques où les corps des branches sont petits.

Presque tout le matériel peut gérer var = bool ? val1 : val2 Sans avoir à diverger. C'était possible en 2002.

Étant donné que cela dépend beaucoup du matériel, cela ... dépend du matériel. Il existe cependant certaines époques matérielles sur lesquelles on peut se pencher:

Ordinateur de bureau, pré-D3D10

Là, c'est un peu l'ouest sauvage. Le compilateur de NVIDIA pour ce type de matériel était bien connu pour détecter de telles conditions et en fait recompiler votre shader chaque fois que vous changiez les uniformes qui les affectaient.

En général, environ 80% des "déclarations inutilisées if" de cette époque proviennent de cette époque. Mais même ici, ce n'est pas nécessairement vrai.

Vous pouvez vous attendre à une optimisation des branches statiques. Vous pouvez espérer que la création de branches statiquement uniformes ne provoquera pas de ralentissement supplémentaire (bien que le fait que NVIDIA pense que la recompilation serait plus rapide que son exécution rend peu probable moins pour leur matériel). Mais le branchement dynamique va vous coûter quelque chose, même si toutes les invocations prennent le même branchement.

Les compilateurs de cette époque font de leur mieux pour optimiser les shaders afin que des conditions simples puissent être exécutées simplement. Par exemple, votre output = input*enable + input2*(1-enable); est quelque chose qu'un compilateur correct pourrait générer à partir de votre déclaration équivalente if.

Bureau, post-D3D10

Le matériel de cette époque est généralement capable de traiter des déclarations de branches statiquement uniformes avec un faible ralentissement. Pour les branches dynamiques, vous pouvez ou non rencontrer un ralentissement.

Bureau, D3D11 +

Le matériel de cette époque est quasiment garanti pour pouvoir gérer des conditions dynamiquement uniformes avec peu de problèmes de performances. En effet, il n'est même pas nécessaire qu'il soit uniformément dynamique; tant que tous les appels dans le même front d'onde empruntent le même chemin, vous ne constaterez aucune perte de performances significative.

Notez que certains matériels de l'époque précédente pourraient probablement le faire également. Mais c’est celui où il est presque certain que ce sera le cas.

Mobile, ES 2.0

Bienvenue dans l'ouest sauvage. Contrairement aux ordinateurs de bureau Pre-D3D10, cela est principalement dû à la grande variance du matériel de calibre ES 2.0. Il existe une quantité énorme de choses pouvant gérer ES 2.0, et elles fonctionnent toutes très différemment les unes des autres.

Le branchement statique sera probablement optimisé. Toutefois, le fait d’obtenir de bonnes performances avec des branches statiquement uniformes est très dépendant du matériel.

Mobile, ES 3.0+

Le matériel ici est un peu plus mature et capable que l’ES 2.0. En tant que tel, vous pouvez vous attendre à ce que les branches statiquement uniformes s’exécutent raisonnablement bien. Et certains matériels peuvent probablement gérer des branches dynamiques de la même manière que le matériel de bureau moderne.

99
Nicol Bolas

Cela dépend fortement du matériel et de la condition.

Si votre condition est un uniforme: ne vous embêtez pas, laissez le compilateur s'en charger. Si votre condition est quelque chose de dynamique (comme une valeur calculée à partir d'attribut ou extraite à partir de texture ou quelque chose), alors c'est plus compliqué.

Pour ce dernier cas, vous devrez à peu près tester et évaluer, car cela dépend de la complexité du code dans chaque branche et de la "cohérence" de la décision de la branche.

Par exemple, si l'une des branches prend 99% du cas et rejette le fragment, il est fort probable que vous souhaitiez conserver le conditionnel. Mais OTOH dans votre exemple simple ci-dessus si enable est une condition dynamique, la sélection arithmétique pourrait être meilleure.

À moins que vous n'ayez un cas précis comme ci-dessus, ou que vous n'optimisiez pas pour une architecture connue fixe, vous feriez probablement mieux que le compilateur le découvre pour vous.

6
246tNt