La vectorisation est un mécanisme permettant d'exécuter la même instruction sur plusieurs données. Le terme anglais couramment utilisé pour qualifier la vectorisation est Single Instruction Multiple Data (SIMD). Comme il s'agit d'une instruction gérée directement par le processeur, les opérations possibles sont assez limitées. En général, il s'agit des opérations arithmétiques de base (addition, soustraction, ...) ainsi que les fonctions mathématiques classiques (minimum, maximum, valeur absolue, ...). Les opérations mathématiques complexes (comme le logarithme, l'exponentielle, ...) ne sont en général pas des instructions natives vectorielles.
Les processeurs récents permettent tous de faire de la vectorisation. Par contre, les tailles de vecteur et les opérations possibles sont différentes d'un processeur à l'autre.
Par exemple, la boucle simple suivante effectue n additions :
Avec un processeur scalaire, les registres ne contiennent qu'un seul réel et les instructions d'addition opèrent donc sur un seul réel. Il faudra n instructions d'addition pour faire ce calcul. Un processeur vectoriel dispose de registres contenant plusieurs réels. Pour des registres contenant P réels, le nombre d'instructions d'addition nécessaire est donc n/P. Si les instructions scalaires et vectorielles prennent le même temps, on a donc une accélération théorique d'un facteur P. Plus les registres sont grands, plus l'intéret potentiel de la vectorisation est important. Bien entendu, dans la pratique c'est souvent moins rose et le gain réel dépend d'autres facteurs comme la bande passante mémoire, le pipelining, ...
Pour exploiter la vectorisation, il existe deux possibilités (qui sont compatibles) :
La première solution est la plus simple car elle ne nécessite pas de changer le code. Elle est directement disponible via les bonnes options du compilateur. La contrepartie de cette simplicité est qu'il est souvent difficile pour le compilateur de détecter les endroits où la vectorisation est possible. Le code généré est donc rarement vectorisé. La seconde méthode garantit l'exploitation de la vectorisation mais elle nécessite de réécrire le code. Arcane propose un ensemble de classes pour exploiter cette seconde méthode.
Le principe est donc de fournir une classe vectorielle correspondant à une classe scalaire. La classe vectorielle contiendra donc N valeurs scalaires, avec N dépendant du type de vectorisation disponible.
Même si en théorie la vectorisation peut s'appliquer sur tous les types simples (short, int, long, float, ...), on se limite dans Arcane à fournir des classes gérant la vectorisation que pour les types Arcane::Real et dérivés (Arcane::Real2, Arcane::Real3).
Actuellement, Arcane fournit les types vectoriels suivants :
Type scalaire | Type vectoriel | Fichier de définition |
---|---|---|
Arcane::Real | Arcane::SimdReal |
#include "arcane/utils/Simd.h"
|
Arcane::Real2 | Arcane::SimdReal2 | |
Arcane::Real3 | Arcane::SimdReal3 | |
Arcane::Item, Arcane::Cell, Arcane::Face, ... | Arcane::SimdItem, Arcane::SimdCell, Arcane::SimdFace, ... |
#include "arcane/SimdItem.h"
|
L'utilisation des classes SIMD est similaire à l'usage scalaire. Il suffit en général de changer le nom des classes scalaires par le nom vectoriel correspondant.
L'exemple suivant montre comment passer d'une écriture scalaire à une écriture vectorielle :
La vectorisation fonctionne bien tant que tous les éléments du vecteur doivent effectuer la même opération. Les choses se compliquent lorsque cela n'est plus le cas. Notamment, tout ce qui dépend d'une condition est difficilement vectorisable. Il existe aussi des cas où on souhaite faire dans une boucle vectorielle des opérations spécifiques pour chacun des éléments. Pour gérer cette situation, il est possible d'ajouter des sections séquentiels en itérant sur les entités d'un Arcane::SimdItem via les macros ENUMERATE_*. Par exemple :
Enfin, il est possible de connaître le nombre de réels d'un registre vectoriel via la constante SimdReal::BLOCK_SIZE. Cela permet par exemple d'itérer sur les éléments d'un registre vectoriel :
En général, et c'est le cas pour les processeurs x64, l'utilisation de la vectorisation nécessite que les données en mémoires soient alignées d'une manière plus restrictive que pour types scalaires. Pour le SSE, l'AVX et l'AVX512 L'alignement minimal est égal à la taille en octet du vecteur Simd. Donc par exemple pour l'AVX avec des vecteurs de 256 bits, soit 32 octets, l'alignement minimal est de 32 octets. Pour simplifier la vectorisation Arcane garantit que les types suivants ont l'alignement minimal souhaité pour la vectorisation :
Le C++ ne permettant pas d'allouer via new/delete avec alignement, Arcane fournit la classe Arccore::AlignedMemoryAllocator qui peut être utilisée avec les classes Arcane::UniqueArray et Arcane::SharedArray pour garantir l'alignement. Par exemple :
La vectorisation fonctionne bien lorsque le nombre d'éléments de la boucle est un multiple de la taille du vecteur Simd. Si ce n'est pas le cas, il faut traiter la dernière partie de la boucle d'une certaine manière. Afin d'offrir un mécanisme identique pour tous les types de vectorisation, Arcane duplique dans le vecteur Simd la dernière valeur valide. Par exemple, on suppose le code suivant :
Avec cells un groupe de mailles qui contient 11 éléments. Si on suppose que la taille d'un vecteur est 8, alors la boucle précédente fera deux itérations. Pour la première on aura les valeurs suivantes de simd_cell
Pour la deuxième itération, comme cells ne contient que 11 éléments, on répète dans simd_cell la dernière valeur valide :
Ce mécanisme fonctionne partaitement tant que les opérations effectuées sont bien vectorielles. Si ce n'est pas le cas, il est possible d'itérer uniquement sur les valeurs valides comme suit :
Avec l'exemple précédent, la boucle interne ne fera que 3 itérations, (pour les mailles cells[8], cells[9] et cells[10]) pour la dernière partie de cells.
Les opérations mathématiquees supportés par les classes vectorielles de Arcane sont définies dans le fichier SimdMathUtils.h:
Arcane fournit pour les classes vectorielles Arcane::SimdReal, Arcane::SimdReal2 et Arcane::SimdReal3 les mêmes opérations que celles disponibles dans MathUtils.h pour la version scalaire à l'exception de min et max.
Dans la version 2.2, Arcane ne supporte que la vectorisation pour les processeurs d'architecture x86.
Pour ces processeurs, il existe (actuellement) trois générations de vectorisation :
Suivant la plateforme, plusieurs mécanismes peuvent être disponibles. Sur les processeurs Intel les processeurs ont une compatibilité ascendante et donc ceux qui supportent l'AVX512 supportent aussi l'AVX et le SSE. De même, les processeurs avec AVX supportent le SSE.
Arcane définit le mécanisme par défaut comme étant celui qui utilise la vectorisation la plus importante. Les types Arcane::SimdInfo, Arcane::SimdReal, Arcane::SimdReal3 sont donc des typedefs qui dépendent de la plateforme.
Arcane définit aussi des macros indiquant les mécanismes disponibles :