:: Enseignements :: Master :: M1 :: 2024-2025 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Single instruction, Multiple Data (SIMD)
|
Vectorisation, Décomposition de boucles, Opération binaires et Masques
Nous allons utiliser l'API jdk.incubator.vector intégré au JDK 23
(en incubation).
Exercice 1 - Maven
Comme pour les TPs précédents, nous allons utiliser Maven comme outil de build.
Dans ce TP, nous avons trois besoins spécifiques
-
Pour les tests, en plus de la bibliothèque JUnit habituelle,
nous allons utiliser la bibliothèque junit-jupiter-params qui permet de déclarer
des tests paramétrés, des tests qui peuvent prendre plusieurs algorithmes en entrée.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
-
On va utiliser la bibliothèque JMH qui permet de tester la performance de code écrit Java.
JMH fonctionne avec un ensemble d'annotation comme @Benchmark et un processeur d'annotation
qui transforme le code des benchmarks en tests exécutables isolés du reste du système
(donc les résultats seront reproductibles).
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>provided</scope>
</dependency>
Il faut configurer le maven-compiler-plugin pour qu'il utilise le processeur d'annotation de JMH
lors de la compilation.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>23</release>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Enfin, on utilise le plugin maven-shade-plugin qui prend un code et ses dépendances et créer
un seul jar exécutable nommé benchmarks.jar.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>benchmarks</finalName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>**/module-info.class</exclude>
<exclude>META-INF/MANIFEST.MF</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
-
De plus on utilise le module jdk.incubator.vector qui est un module en incubation
(l'API n'est pas finalisée), il faut l'activer explicitement avec --add-modules
à la compilation et à l'exécution des tests.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>23</release>
<compilerArgs>
<compilerArg>--add-modules</compilerArg>
<compilerArg>jdk.incubator.vector</compilerArg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<argLine>--add-modules jdk.incubator.vector</argLine>
</configuration>
</plugin>
Comme précédemment, créer un projet Maven en cochant
create simple project au niveau du premier écran,
puis passer à l'écran suivant en indiquant
Next.
Pour ce TP, le groupId est
fr.uge.simd , l'artefactId est
simd et
la version est
0.0.1-SNAPSHOT. Pour finir, cliquer sur
Finish.
Exercice 2 - 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.uge.simd.VectorComputation
(dans
src/main/java) qui contiendra les différentes méthodes statiques.
Les tests unitaires correspondants s'appellent
fr.uge.simd.VectorComputationTest
(dans
src/test/java).
Note : il est possible de lancer les tests directement, mais il faut penser à rajouter
--add-modules jdk.incubator.vector dans les options de la VM.
Pour Maven, sous Eclipse, clic droit sur le projet, "run as..." puis maven clean et maven install.
De plus la classe
fr.uge.simd.VectorComputationBenchmark
(dans
src/main/java)
contient les tests de performance JMH pour vérifier que le code que vous avez écrit est bel et bien plus
performant qu'une simple boucle sur les données du tableau.
Note : par défaut les benchmarks (les méthodes annotées avec
@Benchmark) sont commentés, il faut les dé-commenter si vous voulez exécuter un benchmark. Et il faut penser à produire le jar correspondant.
-
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.
-
Quelle est la classe qui représente 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 zéros 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 longueur du tableau n'est pas un multiple du nombre de lanes, on va utiliser une post-loop,
quel 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 le test
nommé "testSum" passe. De plus, vérifier avec les tests de performance dans
VectorComputationBenchMark (dé-commenter les annotations correspondantes) que votre code est plus efficace qu'une simple boucle.
Rappel : pour lancer les tests JMH, il suffit d'exécuter
java -jar target/benchmarks.jar dans un terminal (et arrêter tout les programmes qui tournent !).
-
On souhaite écrire une méthode sumMask qui évite d'utiliser une post-loop et
utilise un mask à la place.
-
Comment peut-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 ?
Écrire le code de la méthode sumMask et vérifier que le test "testSumMask" passe.
Que pouvez dire en termes de performance entre sum et sumMask en utilisant
les tests de performances JMH ?
-
On souhaite maintenant écrire 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 nul, le minimum n'a pas d'élément nul...
Quelle doit être la valeur utilisée pour initialiser toutes les lanes du vecteur avant la boucle principale ?
Écrire le code de la méthode min, vérifier que le test nommé "testMin" passe et
vérifier avec les tests JMH que votre code est plus efficace qu'une simple boucle sur les valeurs du tableau.
-
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’élément nul (non, toujours pas !), donc on ne peut pas laisser des zéros
"traîner" dans les lanes lorsque l'on fait un minimum sur deux vecteurs.
Écrire le code de la méthode minMask et vérifier que le test nommé "testMinMask" passe.
Que pouvez-vous dire en termes de performance entre min et minMask en utilisant
les tests de performances JMH ?
Exercice 3 - FastScanList
On souhaite améliorer la vitesse moyenne (pas le pire cas, pour cela il ne faut pas prendre une liste)
des recherches dans une liste.
L'idée est la suivante : chercher dans une liste demande à appeler la méthode
Object.equals() sur chaque élément de la liste. Or, on peut accélérer un peu les choses en comparant les
hashCode() AVANT d'appeler
equals()... En effet, cela peut permettre de gagner en performances : si, par exemple, les éléments sont
des chaînes de caractères, cela revient à comparer un entier au lieu de parcourir
la chaîne de caractères et comparer les caractères un à un (dans le cas où les chaines sont différentes).
On se propose de créer une classe
FastScanList qui, en plus du tableau des éléments,
maintient un tableau des hashCodes. Pour éviter d'utiliser trop d'espace mémoire (et pour pouvoir plus tard
utiliser l'API des vectors), nous n'allons pas stocker le hashCode complet sur un int mais seulement une portion sur un byte.
Pour la classe
FastScanList, on se propose d'utiliser le code suivant
(dans
src/main/java)
public class FastScanList<E> {
private E[] array;
private byte[] hashes;
private int size;
public FastScanList() {
@SuppressWarnings("unchecked")
var array = (E[]) new Object[16];
this.array = array;
this.hashes = new byte[16];
}
public int size() {
return size;
}
private void resize() {
var newLength = size << 1;
array = Arrays.copyOf(array, newLength);
hashes = Arrays.copyOf(hashes, newLength);
}
public void add(E element) {
Objects.requireNonNull(element);
if (array.length == size) {
resize();
}
array[size] = element;
hashes[size] = (byte) element.hashCode();
size++;
}
@Override
public String toString() {
return IntStream.range(0, size)
.mapToObj(i -> array[i] + " (" + (hashes[i] & 0xFF) + ")")
.collect(Collectors.joining(", ", "[", "]"));
}
public boolean contains(Object o) {
Objects.requireNonNull(o);
var hash = (byte) o.hashCode();
for(var i = 0; i < size; i++) {
if (o.equals(array[i])) {
return true;
}
}
return false;
}
}
Les tests unitaires correspondants s'appellent
fr.uge.simd.FastScanListTest
(dans
src/test/java).
De plus la classe
fr.uge.simd.FastScanListBenchmark
(dans
src/main/java)
contient les tests de performance JMH.
-
Vérifier que vous avez bien mis les fichiers au bon endroit et que les tests Q1 passent.
Lancer le benchmark de performance pour obtenir les temps si l'on cherche un élément
présent ou pas dans la liste.
-
L'implantation actuelle de contains n'utilise pas le tableau contenant
les valeurs de hachage.
Changer le code pour améliorer les performances.
Vérifier que les tests Q2 passent.
Relancer le benchmark de performance sur votre nouvelle implantation pour vérifier qu'elle
est bien plus rapide.
-
En fait, on peut améliorer le code en utilisant l'API de vectorisation, afin d'aller chercher
plus rapidement s'il y a la valeur de hachage recherchée. L'idée est de comparer
plusieurs bytes en une fois en utilisant la méthode Vector.eq(vector).
Sachant nous allons utiliser les méthodes VectorMask.anyTrue() et
mask.laneIsSet(index), écrire une nouvelle implantation de la méthode
contains utilisant l'API vectorisée.
Vérifier que les tests Q3 passent.
Lancer le benchmark de performance sur votre nouvelle implantation pour vérifier qu'elle
est bien plus rapide que la précédente.
© Université de Marne-la-Vallée