:: Enseignements :: ESIPE :: E4INFO :: 2020-2021 :: Collections Concurrentes ::
[LOGO]

Single instruction, Multiple Data (SIMD)


Vectorisation, Loop decomposition, Mask
Nous allons utiliser l'API jdk.incubator.vector intégré au JDK 16.
Vous devez donc installer le JDK 16 sur votre machine, il est disponible
Sur le site d'Oracle: https://www.oracle.com/java/technologies/javase-jdk16-downloads.html
Sur une Fedora, c'est
    sudo dnf install java-latest-openjdk.x86_64
  

Après, il faut mettre à jour votre IDE, pour IntelliJ IDEA, il faut une version 2021.x, pour Eclipse, une version 14.20+
IntelliJ 2021.1
Eclipse 4.20M1

Comme je suis gentil avec vous, je vous ai préparé un zip qui contient un projet Maven avec son POM, struct-conc-lab5.zip, et les fichiers de configs pour Eclipse et IntelliJ déjà écrits.

Exercice 1 - Vectorized Add / Vectorized Min

Le but de cet exercice est de calculer la somme et le minimum d'un tableau de façon vectorisée (en utilisant des vecteurs de valeurs). Nous allons pour cela créer une classe fr.umlv.structconc.VectorComputation (dans src/main/java) qui contriendra les différentes méthodes statiques.
Les tests unitaires correspondant s'appel fr.umlv.structconc.VectorComputationTest (dans src/test/java).
De plus la classe fr.umlv.structconc.VectorComputationBenchMark (dans src/main/java) contient les tests de perfomance JMH pour vérfier que le code que vous avez écrit est bien plus performant qu'une simple boucle sur les données du tableau.

  1. On cherche à écrire une fonction sum qui calcule la somme des entiers d'un tableau passé en paramètre. Pour cela, nous allons utiliser l'API de vectorisation pour calculer la somme sur des vecteurs.
    Quel est la classe qui represente des vecteurs d'entiers ?
    Qu'est ce qu'un VectorSpecies et quelle est la valeur de VectorSpecies que nous allons utiliser dans notre cas ?
    Comment créer un vecteur contenant des zeros et ayant un nombre préféré de lanes ?
    Comment calculer la taille de la boucle sur les vecteurs (LoopBound) ?
    Comment faire la somme de deux vecteurs d'entiers ?
    Comment faire la somme de toutes les lanes d'un vecteur d'entiers ?
    Si la longeur du tableau n'est pas un multiple du nombre de lanes, on va utiliser une post-loop, quelle doit être le code de la post-loop ?
    Une fois que vous avez répondu à toutes ces questions, écrire le code de sum et vérifier que les tests marqués "Q1" passent. De plus, vérifier en laissant les tests de performance dans VectorComputationBenchMark montrent que votre code est plus efficace qu'une simple boucle.
    Rappel: pour lancer les tests JMH, il suffit d'exécuter /path/to/jdk-16/bin/java -jar -jar target/benchmarks.jar dans un terminal.
  2. On souhaite écrire une méthode sumMask qui évite d'utiliser une post-loop et utlise un mask à la place.
    Comment doit on faire une addition de deux vecteurs avec un mask ?
    Comment faire pour créer un mask qui allume les bits entre i la variable de boucle et length la longueur du tableau ?
    Ecrire le code de la méthode sumMask et vérifier que les tests marqués "Q2" passent.
    Que pouvez dire en terme de performance entre sum et sumMask en utilisant les tests de performances JMH ?
  3. On souhaite maintenant écire une méthode min qui calcule le minimum des valeurs d'un tableau en utilisant des vecteurs et une post-loop.
    Contrairement à la somme qui a 0 comme élément nulle, le minimum n'a pas d'élément nulle, quelle doit être la valeur utilisé pour initialiser de toute les lanes du vecteur avant la boucle principale ?
    Ecrire le code de la méthode min, vérifier que les test marqués "Q3" passent et vérifier avec les tests JMH que votre code est plus efficace qu'une simple boucle sur les valeurs du tableau.
  4. On souhaite enfin écrire une méthode minMask qui au lieu d'utiliser une post-loop comme dans le code précédent, utilise un mask à la place.
    Attention, le minimum n'a pas d'élement nulle (non, toujours pas), donc on ne peut pas laisser des zeros "trainés" dans les lanes lorsque l'on fait un mimimum sur deux vecteurs.
    Ecrire le code de la méthode minMask et vérifier que les test marqués "Q4" passent.
    Que pouvez dire en terme de performance entre min et minMask en utilisant les tests de performances JMH ?

Exercice 2 - FizzBuzz

On cherche à écrire FizzBuzz avec des vecteurs. Pour rappel, FizzBuzz est un programme qui consiste à écrire sur la sortie standard
  • Les nombres de 1 à 100 en remplaçant les nombres
  • multiple de 3 par "Fizz"
  • multiple de 5 par "Buzz"
  • multiple de 15 par "FizzBuzz"

Pour résoudre le problème avec des vecteurs, on va le décomposer en deux parties, on va résoudre le problème en remplissant un tableau d'entiers avec -1 pour Fizz, -2 pour Buzz et -3 pour FizzBuzz puis on utilisera le code suivant pour afficher les chaines de caractères correspondantes
Arrays.stream(fizzBuzz(19)).mapToObj(i -> switch (i) { case -1 -> "Fizz"; case -2 -> "Buzz"; case -3 -> "FizzBuzz"; default -> "" + i; }).forEach(System.out::println);
Donc on cherche a générer la suite d'entiers correspondant à FizzBuzz, pour les première valeur, cela donne
    -3  1  2  -1  4  -2  -1  7  8  -1  -2  11  -1  13  14  -3  16  17 -1 19 -2 -1 22 23 -1 -2 26  -1 28 29 ...
   

On peut remarquer qu'une fois passé les 15 premières valeurs la suite se répète pour les valeurs négatives (c'est normal avec le module 15).
Les mêmes valeurs avec un saut de ligne à la 15ième valeur:
     -3  1  2  -1  4  -2  -1  7  8  -1  -2 11  -1 13 14
     -3 16 17  -1 19  -2  -1 22 23  -1  -2 26  -1 28 29 ...
   
En fait, on peut facilement calculer les valeurs suivantes à partir des 15 premières valeurs, soit la valeur est negative et elle reste la même soit elle est positive et il faut l'augmenter de 15.
Reste à savoir comment mettre 15 valeurs dans un vecteur d'entiers, si on utilise des CPUs AVX2, chaque vecteur fait 256 bites, donc 8 valeurs entières, on a donc besoin de 2 registres, et il faudra faire attention à ne pas prendre en compte la 16ième valeur, la dernière du deuxième registre.

  1. On souhaite écrire dans la classe FizzBuzz une méthode fizzbuzzWithMask qui prend en paramètre une longueur et renvoie un tableau d'entiers de taille longueur en contenant les valeurs de FizzBuzz.
    L'idée est de créer deux vecteurs v1 et v2 contenant les valeurs respectivement de 0 à 7 et de 8 à 16, et deux autres vecteurs delta1 et delta2 qui contiennent pour chaque lane soit 0 si la valeur est négative soit 15 si la valeur est positive. Pour calculer les nouvelles valeurs de v1 et v2 si suffira d'ajouter respectivelent delta1 et delta2.
    Il faut stocker les valeurs de v1 et v2 dans le tableau avant de calculer les nouvelles valeurs sinon on aura pas les 15 premières valeurs dans le tableau. Il faut aussi utiliser un mask pour ne pas stocker la 16ième valeur dans le tableau.
    Sachant que VALUES contient les 16 premières valeurs de FizzBuzz et que DELTAS contient les 16 valeurs de delta (0 ou 15 suivant la valeur correspondante de VALUES). Ecrire la méthode fizzBuzzWithMask en utilisant une post-loop, vérifier que les tests correspondant à "Q1" passent et vérifier avec le test JMH que votre solutuon est plus efficace que la méthode fizzbuzz qui fait une boucle classique.
    Note: ici, on fait une boucle sur 15 valeurs, pas 16 !
  2. En fait, comme nous l'avons vu précédemment, lire ou écrire avec un mask n'a pas encore été optimisé, donc si on trouve une façon de ne pas utiliser de mask l'implantation va être plus efficace.
    L'astuce ici, consiste à voir que si l'on écrit la 16ième valeur, ce n'est pas très grave car celle-ci sera ré-écrit au tour suivant.
    Dupliquer votre code est écrire une nouvelle version fizzBuzzOverwrite qui n'utilise pas de mask, vérifier que les tests marqués "Q2" passent et que votre solution est plus efficace en utilisant le test JMH.
    Attention, il faut faire attention que lorsque vous écrivez dans le tableau vous ne dépasser pas la taille est écrivant la 16ième valeur, il faut pour cela calculer loopBound correctement !
  3. Enfin, dernière façon de faire, il existe une version de add sur un vecteur d'entier qui prend en paramètre une constant et un mask donc au lieu de calculer delta1 et delta2, on peut toujours ajouter 15 avec un mask que sera à faux pour les valeurs négatives de VALUES.
    Définisser une constante MASK_BITS qui contient un tableau de booléen permettant de créer le mask nécessaire (vrai pour 15 et false pour 0). Puis initialiser deux VectorMask m1 et m2 que vous pourrez utiliser avec la méthode IntVector.add(int, VectorMask). Ecrire le reste de la méthode fizzBuzzAddWithMask, vérifier que les tests unitaires marqués "Q3" passent et regarder les performances de implantation avec le test JMH.
    Note: essayer pas de calculer MASK_BITS avec un Stream, les BooleanStream cela n'existe pas !