A éviter si les instructions dans les shaders DirectX 10?

14

J'ai entendu dire que si les instructions devaient être évitées dans les shaders, parce que les deux parties des instructions seront exécutées et que le mauvais sera supprimé (ce qui nuit à la performance).

C'est toujours un problème dans DirectX 10? Quelqu'un m'a dit que seule la bonne branche serait exécutée.

Pour l'illustration j'ai le code:

float y1 = 5; float y2 = 6; float b1 = 2; float b2 = 3;

if(x>0.5){
    x = 10 * y1 + b1;
}else{
    x = 10 * y2 + b2;
}

Existe-t-il un autre moyen de le rendre plus rapide?

Si oui, comment faire?

Les deux branches se ressemblent, la seule différence est les valeurs des "constantes" ( y1, y2, b1, b2sont les mêmes pour tous les pixels dans Pixel Shader).

PolGraphic
la source
1
Honnêtement, c'est une optimisation très prématurée, ne les changez pas tant que vous n'avez pas évalué votre code et que le shader est à 100% un goulot d'étranglement.
pwny

Réponses:

17

De nombreuses règles de micro-optimisation des shaders sont les mêmes que pour les CPU traditionnels avec des extensions vectorielles. Voici quelques conseils:

  • il y a des fonctions de test intégrées ( test, lerp/ mix)
  • l'ajout de deux vecteurs a le même coût que l'ajout de deux flotteurs
  • swizzling est gratuit

Il est vrai que les succursales sont moins chères sur le matériel moderne qu'elles ne l'étaient auparavant, mais il est toujours préférable de les éviter si possible. En utilisant le swizzling et les fonctions de test, vous pouvez réécrire votre shader sans tests:

/* y1, y2, b1, b2 */
float4 constants = float4(5, 6, 2, 3);

float2 tmp = 10 * constants.xy + constants.zw;
x = lerp(tmp[1], tmp[0], step(x, 0.5));

L'utilisation de stepet lerpest un idiome très courant pour choisir entre deux valeurs.

sam hocevar
la source
6

En général, ça va. Les shaders s'exécuteront en groupes de sommets ou pixels (différents fournisseurs ont une terminologie différente pour ceux-ci, donc je m'en tiens à l'écart) et si tous les sommets ou pixels d'un groupe empruntent le même chemin, le coût de branchement est négligeable.

Vous devez également faire confiance au compilateur de shader. Le code HLSL que vous écrivez ne doit pas être considéré comme une représentation directe du bytecode ou même de l'assembly vers lequel il sera compilé, et le compilateur est parfaitement libre de le convertir en quelque chose d'équivalent mais évite la branche (par exemple, un lerp peut parfois être une conversion préférée). D'un autre côté, si le compilateur détermine que l'exécution d'une branche est en fait le chemin le plus rapide, il la compilera jusqu'à une branche. La visualisation de l'assemblage généré dans PIX ou un outil similaire peut être très utile ici.

Enfin, la vieille sagesse tient toujours ici - profilez-la, déterminez si c'est réellement un problème de performances pour vous, et abordez-la ensuite, pas avant. En supposant que quelque chose peut être un problème de performances et en agissant selon cette hypothèse, vous n'encourrez qu'un risque énorme de problèmes plus importants par la suite.

Maximus Minimus
la source
4

Citation du lien / article publié par Robert Rouhani:

"Les codes de condition (prédication) sont utilisés dans les anciennes architectures pour émuler la vraie branche. Les instructions if-then compilées pour ces architectures doivent évaluer les instructions de branche prises et non prises sur tous les fragments. La condition de branche est évaluée et un code de condition est défini. La les instructions de chaque partie de la branche doivent vérifier la valeur du code de condition avant d'écrire leurs résultats dans les registres. Par conséquent, seules les instructions des branches prises écrivent leur sortie. Ainsi, dans ces architectures, toutes les branches coûtent autant que les deux parties de la branche, plus le coût de l'évaluation de l'état de la branche. La branche doit être utilisée avec parcimonie sur de telles architectures. Les GPU NVIDIA GeForce FX Series utilisent l'émulation de branche de code de condition dans leurs processeurs de fragments. "

Comme l'a suggéré mh01 ("L'affichage de l'assembly généré dans PIX ou un outil similaire peut être très utile ici."), Vous devez utiliser un outil de compilation pour examiner la sortie. D'après mon expérience, l'outil Cg de nVidia (Cg est encore largement utilisé aujourd'hui en raison de ses capacités multiplateformes) a donné une illustration parfaite du comportement mentionné dans le paragraphe des codes de condition des gemmes du GPU (prédication) . Ainsi, quelle que soit la valeur de déclenchement, les deux branches ont été évaluées par fragment, et seulement à la fin, la bonne a été placée dans le registre de sortie. Néanmoins, le temps de calcul a été perdu. À l'époque, je pensais que la ramification améliorerait les performances, en particulier parce que tousles fragments dans ce shader se sont appuyés sur une valeur uniforme pour décider de la bonne branche - cela ne s'est pas produit comme prévu. Donc, une mise en garde majeure ici (par exemple éviter les ubershaders - peut-être la plus grande source d'enfer de ramification).

teodron
la source
2

Si vous ne rencontrez pas déjà de problèmes de performances, c'est très bien. Le coût de comparaison avec une constante est toujours extrêmement bon marché. Voici une bonne lecture de la branche GPU: http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter34.html

Quoi qu'il en soit, voici un extrait de code qui va être bien pire que l'instruction if (et qui est beaucoup moins lisible / maintenable), mais qui s'en débarrasse toujours:

int fx = floor(x);
int y = (fx * y2) + ((1- fx) * y1);
int b = (fx * b2) + ((1 -fx) * b1);

x = 10 * y + b;

Notez que je fais l'hypothèse que x est limité à la plage [0, 1]. Cela ne fonctionnera pas si x> = 2 ou x <0.

Ce qui est coupé, c'est convertir x en ou 0ou 1et multiplier le mauvais par 0 et l'autre par 1.

Robert Rouhani
la source
Étant donné que le test d'origine est if(x<0.5)la valeur de fxdoit être round(x)ou floor(x + 0.5).
sam hocevar
1

Il existe plusieurs instructions capables de faire des conditions sans ramification;

vec4 when_eq(vec4 x, vec4 y) {
  return 1.0 - abs(sign(x - y));
}

vec4 when_neq(vec4 x, vec4 y) {
  return abs(sign(x - y));
}

vec4 when_gt(vec4 x, vec4 y) {
  return max(sign(x - y), 0.0);
}

vec4 when_lt(vec4 x, vec4 y) {
  return max(sign(y - x), 0.0);
}

vec4 when_ge(vec4 x, vec4 y) {
  return 1.0 - when_lt(x, y);
}

vec4 when_le(vec4 x, vec4 y) {
  return 1.0 - when_gt(x, y);
}

Plus certains opérateurs logiques;

vec4 and(vec4 a, vec4 b) {
  return a * b;
}

vec4 or(vec4 a, vec4 b) {
  return min(a + b, 1.0);
}

vec4 xor(vec4 a, vec4 b) {
  return (a + b) % 2.0;
}

vec4 not(vec4 a) {
  return 1.0 - a;
}

source: http://theorangeduck.com/page/avoiding-shader-conditionals

Alexis Paques
la source