Les cartes graphiques/Les processeurs de shaders
Les shaders sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de shaders. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
- L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.
En conséquence, il est possible de traiter chaque instance d'un shader en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes warps processor, ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le Thread Execution Control Unit en jaune, qui répartit les différentes instances du shader sur les différents processeurs. Elle est aussi appelée le processeur de commandes, comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.

Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de shaders, la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
Les registres des processeurs de shaders
[modifier | modifier le wikicode]Un processeur de shaders contient beaucoup de registres généraux, qui servent un peu à tout. Le programmeur de shader peut les utiliser à loisir. Tout processeur digne de ce nom possède des registres généraux, mais un processeur de shader dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de shaders. Ils servent à l'interfacer avec le reste du pipeline graphique. Par exemple, des registres pour les textures, d'autres pour recevoir des données du rastériseur, etc.
Les registres d'interface avec le pipeline graphique
[modifier | modifier le wikicode]Les processeurs de vertex shader reçoivent des sommets provenant de l'input assembler et envoient leur résultat au rastériseur. Les processeurs de pixel shader reçoivent des données de l'unité de rastérisation, et envoient un pixel éclairé aux ROPs. Et pour cela, le processeur de shader a des registres dédiés, qui servent d'interface avec le reste du pipeline graphique.
Les registres de sortie sont là où le processeur stocke les résultats à envoyer, soit au rastériseur pour un vertex shader, soit aux ROP pour un pixel shader. Les registres de sorties sont en écriture seule. Pour donner un exemple, les vertex shaders ont au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
| Vertex shader | Pixel shader |
|---|---|
| Couleur du pixel | Couleur du sommet |
| Profondeur du pixel | Position du sommet |
| Coordonnées de texture du sommet | |
| Couleur de brouillard. |
Les registres d'entrée, aussi appelés registres d'attributs, réceptionnent soit les sommets provenant de l'input assembler, soit les pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, ils sont initialisés avant l'exécution de l'instance du shader.
Les registres de constantes mémorisent des constantes utiles pour le shader. Par exemple, pour les vertex shaders, ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. La différence avec les registres d'attribut est qu'ils mémorisent des données constantes pour un objet/modèle 3D (pour un draw call, pour être plus précis) : des matrices de transformation, les adresses de texture, et bien d'autres. À l'opposé, les registres d'attributs mémorisent des sommets/pixels qui varient d'une instance de shader à l'autre.
Les pixel/vertex shaders 1.0 ne géraient que des constantes flottantes pour les vertex shaders, entières pour les pixel shaders. Les pixel/vertex shaders 2.0 et 3.0 ont ajouté des registres de constantes pour les nombres entiers et des opérandes booléennes. Il y en avait 16, comparé aux centaines de registres de constantes flottants. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Avec les pixel/vertex shaders 4.0 et plus, les registres de constante n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de shaders modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un local store un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce local store en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce local store est dans un registre, séparé du reste, appelé le registre d'adresse de constante.
Les registres spécialisés internes
[modifier | modifier le wikicode]D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Depuis les pixel/vertex shaders 3.0, les shaders sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un registre compteur de boucle, qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
Certains processeurs de shader ont aussi des registres de texture , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec multitexturing, et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du shader, mais la plupart sont initialisé quand le shader termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
Les processeurs de shaders modernes : les processeurs SIMD
[modifier | modifier le wikicode]Maintenant, voyons quelles sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
- Graphics Core Next 1 instruction set ;
- Graphics Core Next 2 instruction set ;
- Graphics Core Next 3 and 4 instruction sets ;
- "Vega" 7nm instruction set architecture (also referred to as Graphics Core Next 5.1) ;
- Jeu d'instruction des GPU de type RDNA3 d'AMD.
Les instructions SIMD
[modifier | modifier le wikicode]Les instructions SIMD manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des vecteurs, des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.

Les vecteurs sont stockés dans des registres vectoriels, aussi appelés registres SIMD. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.

Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits, avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
Les instruction scalaires entières, typiques des CPU
[modifier | modifier le wikicode]Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des instruction scalaires. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d'instructions entières, agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des instructions flottantes scalaires, à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de shaders disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des instructions transcendantales, car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
Les instructions en co-issue
[modifier | modifier le wikicode]Beaucoup de cartes graphiques récentes comme anciennes incorporent des instructions de co-issue qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de co-issue regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à co-issue en plus des instructions normales. Les instructions à co-issue sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à co-issue : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de co-issue est la co-issue entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
Un exemple : le jeu d’instruction du GPU de la Geforce 3
[modifier | modifier le wikicode]La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article "A user programmable vertex engine", disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
| OpCode | Nom | Description |
|---|---|---|
| Opérations mémoire | ||
| MOV | Move | vector -> vector |
| ARL | Address register load | miscellaneous |
| Opérations arithmétiques | ||
| ADD | Add | vector -> vector |
| MUL | Multiply | vector -> vector |
| MAD | Multiply and add | vector -> vector |
| MIN | Minimum | vector -> vector |
| MAX | Maximum | vector -> vector |
| SLT | Set on less than | vector -> vector |
| SGE | Set on greater or equal | vector -> vector |
| LOG | Log base 2 | miscellaneous |
| EXP | Exp base 2 | miscellaneous |
| RCP | Reciprocal | scalar-> replicated scalar |
| RSQ | Reciprocal square root | scalar-> replicated scalar |
| Opérations trigonométriques | ||
| DP3 | 3 term dot product | vector-> replicated scalar |
| DP4 | 4 term dot product | vector-> replicated scalar |
| DST | Distance | vector -> vector |
| Opérations d'éclairage géométrique | ||
| LIT | Phong lighting | Calcule l'éclairage de Gouraud |
L'instruction la plus intéressante est clairement la dernière : elle éclaire un sommet, en utilisant un éclairage de Phong. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais cette forme d'éclairage est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de vertex shader ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
La prédication et le SIMT
[modifier | modifier le wikicode]Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de prédication. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
- une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
- suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le Vector Mask Register. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le Vector Mask Register stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.

La prédication avec une pile SIMT
[modifier | modifier le wikicode]Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La pile de masques remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
- Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
- Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
Les compteurs d'activité
[modifier | modifier le wikicode]Une variante de la technique précédente remplace la pile de masques par des compteurs d'activité. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
| masque 1 | 1 | 1 | 1 | 1 |
|---|---|---|---|---|
| masque 2 | 0 | 1 | 1 | 1 |
| masque 3 | 0 | 1 | 1 | 1 |
| masque 4 | 0 | 0 | 0 | 1 |
| masque 1 | vide | |||
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
| masque 1 | 3 | 1 | 1 | 0 |
|---|
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.

