Pourquoi est-ce si conditionnel dans mon fragment shader si lent?

19

J'ai mis en place un code de mesure FPS dans WebGL (basé sur cette réponse SO ) et j'ai découvert quelques bizarreries avec les performances de mon fragment shader. Le code rend juste un seul quadruple (ou plutôt deux triangles) sur une toile 1024x1024, donc toute la magie se produit dans le shader de fragment.

Considérez ce shader simple (GLSL; le vertex shader est juste un pass-through):

// some definitions

void main() {
    float seed = uSeed;
    float x = vPos.x;
    float y = vPos.y;

    float value = 1.0;

    // Nothing to see here...

    gl_FragColor = vec4(value, value, value, 1.0);
}

Donc, cela rend juste une toile blanche. Il fait en moyenne environ 30 fps sur ma machine.

Maintenant, augmentons le nombre de calculs et calculons chaque fragment en fonction de quelques octaves de bruit dépendant de la position:

void main() {
    float seed = uSeed;
    float x = vPos.x;
    float y = vPos.y;

    float value = 1.0;

      float noise;
      for ( int j=0; j<10; ++j)
      {
        noise = 0.0;
        for ( int i=4; i>0; i-- )
        {
            float oct = pow(2.0,float(i));
            noise += snoise(vec2(mod(seed,13.0)+x*oct,mod(seed*seed,11.0)+y*oct))/oct*4.0;
        }
      }

      value = noise/2.0+0.5;

    gl_FragColor = vec4(value, value, value, 1.0);
}

Si vous souhaitez exécuter le code ci-dessus, j'ai utilisé cette implémentation desnoise .

Cela ramène le fps à quelque chose comme 7. Cela a du sens.

Maintenant, la partie bizarre ... calculons seulement un fragment sur 16 en tant que bruit et laissons les autres blancs, en enveloppant le calcul du bruit dans le conditionnel suivant:

if (int(mod(x*512.0,4.0)) == 0 && int(mod(y*512.0,4.0)) == 0)) {
    // same noise computation
}

Vous vous attendriez à ce que ce soit beaucoup plus rapide, mais ce n'est toujours que de 7 images par seconde.

Pour un autre test, filtrons plutôt les pixels avec la condition suivante:

if (x > 0.5 && y > 0.5) {
    // same noise computation
}

Cela donne exactement le même nombre de pixels de bruit qu'auparavant, mais nous sommes maintenant de retour à près de 30 ips.

Qu'est-ce qui se passe ici? Les deux façons de filtrer un 16e des pixels ne devraient-elles pas donner exactement le même nombre de cycles? Et pourquoi le plus lent est-il aussi lent que le rendu de tous les pixels sous forme de bruit?

Question bonus: que puis-je faire à ce sujet? Existe-t-il un moyen de contourner l'horrible performance si je veux vraiment tacher ma toile avec seulement quelques fragments coûteux?

(Juste pour être sûr, j'ai confirmé que le calcul modulo réel n'affecte pas du tout la fréquence d'images, en rendant chaque 16e pixel noir au lieu de blanc.)

Martin Ender
la source

Réponses:

22

Les pixels sont regroupés en petits carrés (la taille dépend du matériel) et calculés ensemble dans un seul pipeline SIMD . (type de structure de tableaux de SIMD)

Ce pipeline (qui a plusieurs noms différents selon le fournisseur: warps, fronts d'ondes) exécutera des opérations pour chaque pixel / fragment en lockstep. Cela signifie que si 1 pixel a besoin d'un calcul, tous les pixels le calculeront et ceux qui n'ont pas besoin du résultat le jetteront.

Si tous les fragments suivent le même chemin à travers un shader, les autres branches ne seront pas exécutées.

Cela signifie que votre première méthode de calcul tous les 16 pixels sera la pire des branches.

Si vous souhaitez toujours réduire la taille de votre image, effectuez un rendu sur une texture plus petite, puis augmentez-la.

monstre à cliquet
la source
5
Le rendu à une texture plus petite et le suréchantillonnage est un bon moyen de le faire. Mais si, pour une raison quelconque, vous avez vraiment besoin d'écrire sur chaque 16e pixel de la grande texture, l'utilisation d'un shader de calcul avec une invocation pour chaque 16e pixel plus la charge / stockage d'image pour disperser les écritures dans la cible de rendu peut être une bonne option.
Nathan Reed