vignette|Processeur vectoriel d'un supercalculateur Cray-1. Un processeur vectoriel est un processeur possédant diverses fonctionnalités architecturales lui permettant d'améliorer l’exécution de programmes utilisant massivement des tableaux, des matrices, et qui permet de profiter du parallélisme inhérent à l'usage de ces derniers.
Développé pour des applications scientifiques et exploité par les machines Cray et les supercalculateurs qui lui feront suite, ce type d'architecture a rapidement montré ses avantages pour des applications grand public (on peut citer la manipulation d'images). Elle est implémentée en partie dans les processeurs grand public par des instructions SIMD, soit grâce à une unité de calcul vectoriel dédiée (AltiVec), soit simulée par des instructions bas niveau de type vectoriel (MMX/SSE). Contrairement au SIMD de type MMX, où il faut charger les vecteurs dans les registres en plusieurs opérations puis exécuter une opération supplémentaire sur ces registres, dans une unité vectorielle on charge l'adresse d'une table de vecteurs, la largeur des vecteurs ou du pas et la longueur de la table à traiter par instruction dans un registre, l'instruction vectorielle enchaîne ensuite son calcul sur l'ensemble des vecteurs de cette table.
Jeu d'instructions Les processeurs vectoriels peuvent être vus comme des processeurs normaux, auxquels on a ajouté un certain nombre d'instructions optimisées pour la gestion des tableaux. Ces instructions optimisées pour les tableaux peuvent être vues comme des variantes d'instructions normales, mais optimisées pour traiter de plus grandes données (pour les accès mémoires), ou capables d'effectuer des opérations en parallèle. Ces instructions sont appelées des instructions vectorielles. Il en existe plusieurs types, qu'on va présenter dans ce qui suit.
Instructions de calcul vectoriel vignette|Exemple d'exécution d'une instruction vectorielle d'addition Les instructions vectorielles sont des instructions qui effectuent plusieurs opérations en parallèle, sur des données différentes. Ces instructions de calcul vectoriel travaillent sur un ensemble de données de même taille et de même type, l'ensemble formant ce qu'on appelle un vecteur. Habituellement, ces vecteurs contiennent plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres. Une instruction de calcul vectoriel va traiter chaque donnée du vecteur en parallèle, à savoir en même temps et 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. Quand on exécute une instruction sur un vecteur, les données présentes dans ce vecteur sont traitées simultanément.
Les opérations en question peuvent être : des opérations bit à bit, comme des ET, OU, NOT ; des additions ; des soustractions ; des multiplications ; éventuellement des divisions ; ou des opérations mathématiques plus complexes.
Certaines instructions similaires sont disponibles sur certains processeurs non vectoriels. Par exemple, tous les processeurs x86 modernes contiennent des extensions à leur jeu d'instructions, comme le MMX ou le SSE, qui fournissent de telles instructions de calcul vectoriel.
Instructions d'accès mémoire Tout processeur vectoriel possède des instructions d'accès mémoire diverses, dont au moins une instruction de lecture de vecteurs et une autre pour l'écriture. Sur les anciens processeurs vectoriels, les vecteurs sont lus ou écrits directement en mémoire. Un tel processeur vectoriel est qualifié de mémoire-mémoire. Cette appellation souligne le fait que les vecteurs sont lus et écrits directement dans la mémoire RAM de l'ordinateur, sans stockage intermédiaire visible dans le jeu d'instructions. Mais ce mode de fonctionnement pose quelques problèmes de performances, compte tenu de la lenteur des accès mémoire et de l'inutilité des caches sur ce genre d'architecture. Les processeurs vectoriels de type ont été inventés pour résoudre ce problème. Ceux-ci possèdent des registres vectoriels, qui permettent de stocker des vecteurs dans leur intégralité. Un programmeur peut décider de placer certains vecteurs dans ces registres : les résultats intermédiaires de calculs sont enregistrés et accédés depuis ces registres, ce qui est plus rapide que d'aller les enregistrer et les manipuler en mémoire RAM. Pour utiliser ces registres vectoriels efficacement, le processeur doit pouvoir échanger des vecteurs entre la mémoire RAM et ces registres.
Les processeurs vectoriels load-store disposent d'instructions capables de transférer des vecteurs entre la RAM et les registres vectoriels. Ces instructions sont des instructions d'accès mémoire. Sur les processeurs vectoriels, seules ces instructions peuvent aller lire ou écrire en mémoire : toutes les autres instructions vectorielles manipulent des vecteurs placés dans des registres vectoriels. Ces instructions ont des modes d'adressage spécialisés, de par la nature même des vecteurs. Le mode d'adressage principal est le mode d'adressage absolu, à savoir que l'adresse du vecteur manipulé est intégrée directement dans le code machine de l'instruction, mais d'autres modes d'adressage inspirés des modes d'adressage sur architectures non-vectorielles sont aussi disponibles.
Accès mémoires contigus Avec le mode d'adressage absolu, les instructions peuvent préciser l'adresse mémoire d'un vecteur, qui n'est alors qu'un paquet de données contigües en mémoire. Par exemple, si le processeur manipule des vecteurs de , chaque instruction d'accès mémoire utilisant le mode d'adressage absolu va lire ou écrire dans des blocs de . L'adresse de départ de ces blocs n'est soumise à aucune contrainte d'alignement, ce qui n'est pas le cas sur les processeurs modernes utilisant des jeux d'instructions comme le SSE, le MMX, etc. La raison à cela est que gérer des accès non alignés en mémoire rend les circuits de lecture/écriture en mémoire plus complexes. En contrepartie, ces contraintes compliquent l'utilisation des instructions vectorielles. Par exemple, un compilateur aura plus de mal à utiliser des instructions de calcul vectorielles en présence de contraintes d'alignement.
Accès en stride et en Scatter-Gather vignette|Illustration des modes d'accès en stride Sur un processeur vectoriel, d'autres modes de chargement et d'enregistrement des vecteurs sont disponibles. On peut notamment citer l'existence d'accès mémoires en stride et en scatter-gather. Ces accès permettent à une instruction de charger des données dispersées en mémoire pour les rassembler dans un vecteur.
L'accès en stride permet de charger ou d'enregistrer les données d'un vecteur qui sont séparées par un intervalle régulier d'adresses.. Une instruction d'accès mémoire voulant utiliser ce mode d'accès a besoin de connaitre l'adresse initiale, celle du premier élément du vecteur, et la distance entre deux données en mémoire. Ce mode d'accès permet aux instructions de mieux gérer les tableaux de structures, ainsi que les tableaux multi-dimensionnels. Lorsqu'on utilise de tels tableaux, il arrive aussi assez souvent que l'on n'accède qu'à certains éléments tous séparés par une même distance. Par exemple, si on fait des calculs de géométrie dans l'espace, on peut très bien ne vouloir traiter que les coordonnées sur l'axe des x, sans accès sur l'axe des y ou des z. Les instructions d'accès mémoire en stride permettent au processeur de gérer de tels cas efficacement.
Dernier type d'accès : le Scatter-Gather. Cet accès sert à mieux gérer les matrices creuses. Dans ces matrices, une grande partie des éléments sont nuls. Dans un souci d'optimisation, seuls les éléments non nuls de la matrice sont stockés en mémoire. Avec ce genre d'organisation, les instructions vectorielles ne seraient pas utilisables sur ce genre de matrices, sans Scatter-Gather. Les accès en Scatter-Gather peuvent être vus comme une généralisation de l'adressage indirect à registre aux vecteurs. Avec cet accès, les adresses de chaque élément du vecteur sont stockées dans un registre vectoriel. L'accès en Scatter-Gather va permettre d'aller lire ou écrire aux diverses adresses rassemblées dans ce vecteur.
Registres des processeurs vectoriels vignette|Exemple d'utilisation de registre vectoriel pour stocker des entiers et flottants. Comme décrit plus haut, sur certains processeurs, les vecteurs sont stockés dans des registres vectoriels pour plus d’efficacité. Ces registres ont tous une taille fixe. Ces registres ont une taille qui varie entre , pour les tailles les plus courantes.
Généralement, ces registres ne sont pas spécialisés : ils peuvent stocker indifféremment des entiers et des flottants. Et leur contenu s’adapte suivant leur taille. C'est-à-dire qu'un registre de pourra stocker indifféremment : 8 entiers 16 bits ; 8 flottants 16 bits ; 4 entiers de 32 bits ; 4 flottants de 32 bits ; 2 entiers de 64 bits ; 2 flottants de 64 bits ; etc.
Un processeur vectoriel peut aussi incorporer d'autres registres pour faciliter le traitement des diverses instructions. Ces registres vont permettre au compilateur de mieux utiliser les instructions vectorielles pour compiler certaines structures logicielles. Parmi ces registres, on peut citer le Vector Length Register, et le Vector Mask Register.
Vectorisation
L'utilisation des instructions vectorielles permet de faciliter certains traitements sur les tableaux. De nos jours, ces instructions sont difficilement utilisables dans des langages de haut niveau, et c'est au compilateur de traduire certains traitements sur les tableaux en instructions vectorielles. Ces transformations permettant de traduire des morceaux de programmes en instructions vectorielles s'appelle la vectorisation.
Déroulage de boucles La transformation la plus basique est ce qu'on appelle le déroulage de boucles. Il s'agit d'une optimisation qui vise à réduire le nombre d'itérations d'une boucle en dupliquant son corps en plusieurs exemplaires. Elle est utilisée pour réduire le coût d’exécution des branchements et des autres opérations de comparaison de la boucle. Sur les processeurs SIMD, elle peut s'utiliser afin de réduire le temps d’exécution de boucles qui agissent sur des tableaux. D'ordinaire, les programmeurs utilisent une boucle pour répéter un traitement sur tous les éléments d'un tableau, la boucle traitant un élément à la fois. Les instructions vectorielles permettent de traiter plusieurs éléments à la fois, ce qui fait que plusieurs tours de boucles peuvent être rassemblés en une seule instruction SIMD. Mais cela ne fonctionne que si les éléments du tableau sont traités indépendamment, ou d'une façon assez simple, auquel cas la boucle peut être vectorisée. Pour cela, le compilateur va répliquer le corps de la boucle (les instructions à répéter) en plusieurs exemplaires dans cette boucle.
Exemple, prenons cette boucle, écrite dans le langage C :
int i; for (i = 0; i < 100; ++i) { a[i] = b[i] * 7 ;
Celle-ci peut être déroulée comme suit :
int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ;
Si le compilateur réplique ces instructions en autant de fois qu'une instruction peut traiter d’éléments simultanément, vectoriser la boucle devient trivial. Dans cet exemple, si jamais le processeur dispose d'une instruction de multiplication capable de traiter 4 éléments du tableau a ou b en une seule fois, la boucle déroulée peut être vectorisée assez simplement.
Strip mining