:: Enseignements :: ESIPE :: E4INFO :: 2025-2026 :: Collections Concurrentes ::
![[LOGO]](http://monge.univ-eiffel.fr/ens/resources/mlv.png) |
Examen de Collection Concurrente 2026 - Session 1
|
À lire absolument
Tout ce que vous devez rendre devra obligatoirement être placé dans
le répertoire EXAM à la racine de votre compte ; sinon, ce n'est pas récupéré
et vous aurez 0.
Les deux exercices de ce TP noté sont indépendants.
Exercice 1 - TreiberStack
Le but de cet exercice est d'implanter une classe
TreiberStack
qui est une pile qui stocke les éléments dans une liste chainée de
Node.
Cette pile est intéressante historiquement, car c'est la première structure de données
thread-safe et
lock-free à avoir été implantée en 1986.
Note: si vous pouvez aller regarder l'algo sur wikipedia (en anglais), le code Java donné est faux,
car il a un problème de publication.
public final class TreiberStack<E> {
private record Node<E>(E element, Node<E> next) {}
private Node<E> head;
public void push(E element) {
throw new UnsupportedOperationException("TODO");
}
public E pop() {
throw new UnsupportedOperationException("TODO");
}
}
La classe
TreiberStack doit être
thread-safe et
lock-free.
La méthode
push permet d'ajouter un élément en sommet de pile.
La méthode
pop permet de retirer un élément en sommet de pile.
Elle renvoie
null si la pile est vide.
Voici un exemple d'utilisation :
var stack = new TreiberStack<Integer>();
stack.push(100);
stack.push(1024);
IO.println(stack.pop()); // 1024
IO.println(stack.pop()); // 100
IO.println(stack.pop()); // null
Des tests unitaires correspondant à l'implantation sont ici :
TreiberStackTest.java.
Attention, ces tests ne couvrent que le cas le plus simple à chaque fois,
avoir le test qui passe n'indique en rien si votre code est bon.
Note : comme on utilise les tests unitaires JUnit sans Maven, dans la configuration de votre projet, il faut ajouter
la librairie JUnit 5, soit à partir du fichier
TreiberStackTest.java, en cliquant sur l'annotation
@Test et en sélectionnant le quickfix "Fixup project ...", soit en sélectionnant les "Properties" du projet
(avec le bouton droit de la souris sur le projet) puis en ajoutant la librairie JUnit 5 (jupiter) au ClassPath.
-
Écrire les méthodes push et pop en utilisant l'API des VarHandle.
Vérifier que les deux premiers tests unitaires passent.
-
On veut maintenant ajouter une méthode pushAll(list) qui prend en paramètre
une liste d'élément et ajoute ceux-ci dans la liste en une opération atomique.
Vérifier que le dernier test passent.
Note: ne m'oubliez pas les wildcards.
Exercice 2 - SimdBitSet
On souhaite écrire une classe gérant un ensemble de bits
utilisant les opérations SIMD (vectorisées) du CPU.
Des tests unitaires correspondant à l'implantation sont ici :
SimdBitSetTest.java
Note : comme on utilise les tests unitaires JUnit sans Maven, dans la configuration de votre projet, il faut ajouter
la librairie JUnit 5, soit à partir du fichier
SimdBitSet.java, en cliquant sur l'annotation
@Test et en sélectionnant le quickfix "Fixup project ...", soit en sélectionnant les "Properties" du projet
(avec le bouton droit de la souris sur le projet) puis en ajoutant la librairie JUnit 5 (jupiter) au ClassPath.
L'idée de l'implantation est de stocker les bits dans un tableau de
int.
Comme un
int est de 32 bits en Java, on peut stocker 32 bits par
int.
Donc si on cherche si le ith bit est vrai, on peut le faire en divisant cherchant
d'abord dans quelle case du tableau se trouve le bit (index / 32) puis dans l'entier,
où se trouve le bit (index % 32).
Pour être un poil plus efficace, diviser par 32 est équivalent à faire un décalage de 5 bits
vers la droite et le reste de la division par 32 est un mask (un &) avec 31.
public final class SimdBitSet {
private int[] words;
private SimdBitSet(int[] words) {
this.words = words;
super();
}
public SimdBitSet() {
this(new int[0]);
}
public void set(int bit) {
if (bit < 0) {
throw new IndexOutOfBoundsException("bit index out of range: " + bit);
}
var wordIndex = bit >> 5;
if (wordIndex >= words.length) { // resize if necessary
words = Arrays.copyOf(words, wordIndex + 1);
}
words[wordIndex] |= 1 << (bit & 31);
}
public boolean get(int bit) {
if (bit < 0) {
throw new IndexOutOfBoundsException("bit index out of range: " + bit);
}
if (bit >= words.length << 5) {
return false;
}
var mask = 1 << (bit & 31);
return (words[bit >> 5] & mask) != 0;
}
@Override
public boolean equals(Object o) {
return o instanceof SimdBitSet other &&
words.length == other.words.length &&
Arrays.equals(words, other.words);
}
@Override
public int hashCode() {
return Arrays.hashCode(words);
}
public int bitCount() {
var count = 0;
for (var word : words) {
count += Integer.bitCount(word);
}
return count;
}
@Override
public String toString() {
var builder = new StringBuilder().append('{');
var separator = "";
for (var i = 0; i < words.length; i++) {
var word = words[i];
while (word != 0) {
// Isolate the lowest set bit and find its position
var lowestSetBitPos = Integer.numberOfTrailingZeros(word);
builder.append(separator).append((i << 5) + lowestSetBitPos);
separator = ", ";
word &= word - 1; // clear the lowest set bit
}
}
return builder.append('}').toString();
}
public SimdBitSet and(SimdBitSet other) {
Objects.requireNonNull(other);
var length = Math.min(this.words.length, other.words.length);
var words = new int[length];
for (var i = 0; i < length; i++) {
words[i] = this.words[i] & other.words[i];
}
return new SimdBitSet(words);
}
}
Pour cet exercice, on vous demande d'utiliser le module jdk.incubator.vector.
Eclipse ou IntelliJ ne font pas bien les imports automatiques
donc un import jdk.incubator.vector.* peut aider.
Pour exécuter le code des tests, il faut ajouter --add-modules jdk.incubator.vector aux paramètres
de la VM (en plus du -ea).
-
On va, dans un premier temps, implanter une nouvelle version de la méthode and
qui utilise les opérations vectorisées.
Écrire la nouvelle version de la méthode and et
vérifier que les tests unitaires marqués "Q1" passent.
Note: ici, on souhaite une implantation avec une post-loop.
-
On souhaite maintenant ajouter une méthode parallelAnd qui marche comme
and mais effectue le calcul sur plusieurs cœurs (en utilisant le principe
fork/join).
Doit-on utiliser une RecursiveTask ou une RecursiveAction ici ?
On va créer pour cela une classe interne ParallelAnd.
En prenant le temps de réfléchir, quelles sont les champs de cette classe ?
Dans la classe interne ParallelAnd, on va écrire une méthode
sequentialAnd qui effectue le calcul séquentiel si le nombre
de cases du tableau est trop petit.
Écrire le code de cette méthode.
Sachant que l'on va considérer que les tableaux de 32 cases ou moins doivent
être traité séquentiellement, écrire le reste de la classe ParallelAnd.
Enfin, écrire le code de la méthode parallelAnd.
Vérifier que les tests unitaires marqués "Q2" passent.
Note: pour être efficace, vous ne devriez y avoir qu'une seule allocation de tableau
(le tableau contenant les bits résultant), pas 25 !
Note2: on ne vous demande pas pour l'instant que l'implantation de sequentialAnd
utilise des instructions vectorisées.
-
Changer le code de sequentialAnd (commenter l'ancien code),
pour utiliser les instructions vectorisées.
Vérifier que les tests unitaires marqués "Q2" continue de passer.
-
Sachant qu'il existe une opération VectorOperators.BIT_COUNT,
modifier le code de bitCount pour utiliser des opérations vectorisées.
Vérifier que les tests unitaires marqués "Q4" passent.
Note: si vous faites bien les choses, il devrait y avoir deux raisons pour lesquelles
on a des gains de performance
-
Enfin, on souhaite ajouter une méthode or qui marche comme and
mais fait un "ou bit-à-bit" ("bitwise or" en anglais) utilisant aussi les
opérations vectorisées.
Vérifier que les tests unitaires marqués "Q5" passent.
© Université de Marne-la-Vallée