Le supercalculateur Sequoia (prévu pour être le plus puissant supercalculateur lors de sa sortie) ne fera pas que battre des records de FLOPS, il utilisera aussi des processeurs BlueGene/Q d’IBM, les premiers processeurs commerciaux à utiliser une mémoire transactionnelle matérielle. Le processeur développé par Sun et annulé avec le rachat par Oracle, aurait également dû le prendre en charge.
C’est l’occasion d’expliquer ce qu’est la mémoire transactionnelle : une technique peu connue car elle pose des problèmes de performance lorsque plusieurs processus ou fils d’exécution (threads) doivent accéder à une valeur partagée.
N. D. A. : Merci à Nÿco, NeoX et Michel Barret pour leur aide lors de la rédaction de cette dépêche.
Le problème du multithread
Le multithread permet souvent d’améliorer les performances en exécutant plusieurs opérations en même temps. Cependant, à part quelques algorithmes qui peuvent s’exécuter en parallèle de manière totalement indépendante, il est souvent nécessaire de partager des ressources, comme une valeur, entre les différents threads. Et il faut absolument éviter qu’un thread lise une valeur obsolète ou incohérente.
Prenons l’exemple d’une mise à jour de compte en banque. Un thread va retirer de l’argent et un autre thread va en ajouter, tout ça sur le même compte. Les deux threads lisent la valeur du montant du compte. Ensuite, le thread qui ajoute de l’argent s’exécute plus vite (il y a plein de raisons possibles pour ça), il met à jour la valeur du compte par la nouvelle valeur qu’il a calculé (montant + valeur qu’il a ajoutée). Cependant, lorsque l’autre thread se termine, il met à jour la valeur, il écrit dans la mémoire la valeur qu’il a calculé (le montant lors de la lecture - la valeur qu’il retire), la valeur dans la mémoire ne tient donc pas compte de l’ajout qui a été fait.
Les mutex
Pour régler ce problème, on utilise, la plupart du temps, des mutex (mutual exclusion ou exclusion mutuelle). Le programmeur détermine quelles sont les zones sensibles, au début de celles‐ci, le programme demande l’acquisition du mutex, et si personne ne l’a demandé, il l’obtient et peut exécuter la suite du code. Quand il a fini, il libère le mutex. Si un thread a déjà le mutex, celui qui le demande est mis en attente jusqu’à ce qu’il soit libéré.
Cette technique permet d’être sûr que le thread est le seul à utiliser la ressource partagée. Cependant, elle pose d’autres problèmes. Premièrement, il faut être sûr de relâcher le mutex quand on a fini de l’utiliser, sous peine de bloquer tous les autres threads. Deuxièmement, il y a un risque de blocage complet quand on utilise plusieurs mutex. Par exemple, prenons deux threads avec deux mutex A et B (chacun pour une ressource partagée) ; le premier thread prend le mutex A, le second thread prend le mutex B. Le premier thread veut ensuite prendre le mutex B (sans avoir relâché le mutex A) et le second thread veut prendre le mutex A, on se retrouve dans une situation où les deux threads vont attendre indéfiniment que les mutex soient relâchés, sans que cela se produise. Cela n’arriverait pas si les threads prenaient les mutex dans le même ordre (d’abord le A puis le B), mais ce n’est pas toujours facile à mettre en place, ni même possible.
Les transactions
Pour résoudre les problèmes, on peut utiliser la mémoire transactionnelle. Elle peut être implémentée de manière logicielle ou matérielle. La technique est similaire à celle utilisée dans les transactions des bases de données. La méthode est optimiste, les threads s’exécutent sans verrou, et à la fin de l’opération (le commit), on vérifie que les données n’ont pas été modifiées par d’autres threads pendant le traitement, sinon, on recommence.
On peut l’exprimer dans le code de la manière suivante, le bloc « atomic »
définit la transaction :
// Insère atomiquenent un nœud dans une liste doublement liée
atomic {
newNode→prev = node;
newNode→next = node→next;
node→next→prev = newNode;
node→next = newNode;
}
Il y a un surcoût en mémoire et en temps processeur, car les données doivent être dupliquées pour chaque transaction et des instructions sont parfois exécutées dans le vide, puisque la transaction échouera si les données sont modifiées. D’après les défenseur des transactions, le surcoût processeur est récupéré par l’absence de gestion des mutex, qui n’est pas une opération négligeable. Dans le cas du BlueGene/Q, le surcoût en mémoire sera absorbé par les caches des processeurs, par contre, le surcoût processeur sera toujours présent.
Des implémentations logicielles existent pour beaucoup de langages, dont C++, Java, Python, Scala, Perl…