Telechargé par Elkhiati Fouad

Architectures et Syst`emes des Calculateurs Parall`eles - ENSEIRB ...

publicité
Architectures et Systèmes des
Calculateurs Parallèles
François PELLEGRINI
ENSEIRB-MATMÉCA
[email protected]
30 septembre 2010
2
Ce document est copiable et distribuable librement et gratuitement à la condition
expresse que son contenu ne soit modifié en aucune façon, et en particulier que le
nom de son auteur et de son institution d’origine continuent à y figurer, de même
que le présent texte.
Cours d’architectures et systèmes des calculateurs parallèles
Avant-propos
La loi de Moore, énoncée en 1975, et encore vérifiée jusqu’il y a quelques années,
stipulait que la puissance des ordinateurs, à prix égal, doublait en moyenne tous les
18 mois. Cette amélioration constante en puissance, sur près de trente ans, ne peut
s’expliquer par la simple augmentation de la fréquence des processeurs, car celle-ci
n’a pas suivi la même évolution. Elle est le fruit d’une intense recherche qui porte
tout à la fois sur l’architecture générale du processeur, l’optimisation du câblage de
ses opérations, les stratégies efficaces de prédiction de branchement, la définition
de hiérarchies mémoire, les techniques de compilation avancées, l’optimisation des
ressources disques, et l’amélioration des systèmes d’exploitation.
La situation a cependant radicalement changé à partir du milieu de la décennie
2000. À cette période, les fabricants de processeurs ont été à cours de solutions
pour employer les transistors dont la densité d’intégration toujours croissante, encore pour une autre décennie au moins, leur permettait de disposer. Toutes les
techniques mises en œuvre jusqu’alors pour augmenter la puissance individuelle des
cœurs ont atteint leurs limites, dont la tristement célèbre « barrière de la chaleur »,
qui limite la puissance thermique pouvant être évacuée par unité de surface. La voie
employée par les fabricants a donc été de multiplier le nombre de cœurs des processeurs, sans que le débit mémoire augmente en proportion, laissant les utilisateurs
faire face seuls au problème de leur utilisation.
L’objectif de ce cours est de faire un tour d’ensemble des techniques matérielles
et logicielles mises en œuvre au sein des architectures des processeurs hautes performances, afin d’en tirer parti au maximum lors de l’écriture de programmes faisant
un usage intensif du processeur et de la mémoire.
3
4
Cours d’architectures et systèmes des calculateurs parallèles
Ouvrages de référence
– Highly Parallel Computing – Second edition, G. S. Almasi et A. Gottlieb.
Benjamin Cummings.
– Advanced Computer Architecture : Parallelism, Scalability, Programmability,
K. Hwang. McGraw-Hill.
– Designing and Building Parallel Programs, I. Foster. Addison-Wesley, http:
//www.mcs.anl.gov/dbpp/.
– Practical Parallel Computing, H. S. Morse. AP Professional.
– Algorithmes et Architectures Parallèles,M. Cosnard et D. Trystram.InterÉditions.
– CPU Info Center, http://infopad.eecs.berkeley.edu/CIC/.
– Journal of Parallel and Distributed Computing, . . .
– Page d’accueil des conférences HPCA (« High Performance Computer Architecture »), http://www.hpcaconf.org/
5
6
Cours d’architectures et systèmes des calculateurs parallèles
Chapitre 1
Introduction
1.1
Un aperçu du parallélisme
Depuis les débuts de l’informatique s’est posée la question de résoudre rapidement des problèmes (le plus souvent numériques) coûteux en temps de calcul :
simulations numériques, cryptographie, imagerie, S.G.B.D., etc.
Pour résoudre plus rapidement un problème donné, une idée naturelle consiste à
faire coopérer simultanément plusieurs agents à sa résolution, qui travailleront donc
en parallèle.
À titre d’illustration, on peut se représenter le travail d’un maçon en train de
monter un mur de briques. S’il est seul, il procède rangée par rangée (figure 1.1).
13
7
14
8
15
9
1
2
16
10
3
Tas de briques
17
11
4
18
12
5
6
Mur
Fig. 1.1 – Séquencement du travail d’un maçon travaillant seul.
Si l’on veut monter le mur plus rapidement, on peut faire appel à deux maçons,
qui peuvent organiser leur travail de plusieurs manières différentes.
a) Chacun pose une brique, l’un après l’autre (figure 1.2). Dans ce cas, ils
risquent de se gêner mutuellement, tant pour prendre les briques dans le tas
que pour les mettre en place.
7a
4a
7b
4b
1a
5a
1b
Tas de briques
8a
8b
5b
2a
9a
6a
2b
9b
6b
3a
3b
Mur
Fig. 1.2 – Séquencement du travail de deux maçons travaillant brique par brique.
b) Chacun s’attribue une portion de mur pour travailler (figure 1.3). Ils ne se
gênent plus, mais le maçon le plus éloigné du tas a plus de chemin à faire, et
7
8
CHAPITRE 1. INTRODUCTION
sa partie du mur avancera moins vite. Remarquons également qu’ils se gênent
toujours pour prendre les briques. Une variante possible consiste pour le maçon
de gauche à travailler de droite à gauche et non plus de gauche à droite. Cette
astuce permet plus de flexibilité dans le montage du mur, car même si l’un
des maçons a un peu de retard, le deuxième peut quand même démarrer un
nouveau rang alors que le rang du premier n’est pas terminé. L’inconvénient
de cette variante est que les maçons ont une plus forte probabilité de se gêner à
la frontière. Dans ce cas, une légère désynchronisation sera en fait souhaitable.
7a
4a
5a
1a
Tas de briques
8a
9a
6a
2a
7b
4b
3a
8b
5b
1b
9b
6b
2b
3b
Mur
Fig. 1.3 – Séquencement du travail de deux maçons travaillant sur deux portions
de mur séparées.
c) Chacun s’attribue une portion de mur pour travailler, mais le maçon le plus
près du tas lance une brique à l’autre chaque fois qu’il en prend une pour lui.
Dans ce cas, ils ne se gênent plus ni pour prendre les briques, ni dans leur
travail. Cependant, ils doivent bien savoir viser et attraper. . .
Le montage du mur en parallèle est plus rapide que le montage par un seul maçon,
mais la quantité totale de travail est nécessairement plus importante, car il faut que
les maçons s’organisent entre eux.
Cet exemple impose plusieurs réflexions.
– Pour que la résolution parallèle soit possible, il faut que le problème puisse être
décomposé en sous-problèmes suffisamment indépendants les uns des autres
pour que chaque agent puisse travailler sans perturber les autres.
– Il faut pouvoir organiser efficacement le travail à répartir. En plus du coût de
calcul intrinsèque du problème, on génère un surcoût dû aux calculs annexes
et à la communication entre agents de l’information nécessaire à sa résolution.
– Lorsque cela est possible, il est souvent profitable de reformuler les algorithmes
afin de supprimer des séquentialités et des dépendances qui ne sont en fait pas
inhérentes au traitement considéré.
Les problèmes réels sont parallélisables à des degrés différents. Parfois, il est même
plus intéressant d’éviter le parallélisme si le surcoût engendré par celui-ci est trop important. C’est tout-à-fait regrettable, mais il existe des algorithmes intrinsèquement
séquentiels.
L’obtention d’une version parallèle efficace d’un algorithme peut conduire à
une formulation très différente de l’algorithme séquentiel équivalent. En fait, un
problème a souvent plusieurs formulations parallèles différentes, dont les performances peuvent elles aussi être très différentes, et dépendent de la taille des données
manipulées et de l’architecture cible (hiérarchie mémoire, mécanisme de communication inter-processus, etc).
1.2
Le parallélisme est-il nécessaire ?
La puissance des ordinateurs séquentiels augmentant de manière régulière (en
gros, elle est multipliée par deux tous les dix-huit mois), on pourrait croire qu’elles
Cours d’architectures et systèmes des calculateurs parallèles
1.2. LE PARALLÉLISME EST-IL NÉCESSAIRE ?
9
sera toujours suffisante, et que les machines parallèles (ordinateurs multi-processeurs)
sont inutiles. C’est faux, pour plusieurs raisons.
– Plus on en a, plus on en veut. À mesure que la puissance des machines augmente, on introduit l’outil informatique dans des disciplines où il ne pouvait jusqu’alors pénétrer, et on cherche à intégrer de plus en plus de paramètres dans les modèles numériques : météorologie, synthèse et reconstruction d’images, simulations numériques, repliement de protéines, etc.
Quelques applications, telles que les calculs de chimie quantique, sont extrêmement coûteuses en terme de calcul, et requièrent des machines toujours
plus puissantes. Ces applications sont appelées « applications pétaflopiques »
parce qu’elles nécessitent pour leur exécution, en ordre de grandeur :
– plusieurs Péta1 flops (« floating operation per second »), en double précision ;
– plusieurs Téra octets de mémoire centrale ;
– plusieurs Téra-octets par seconde de bande passante pour produire les
résultats.
À l’heure actuelle, ces applications ne peuvent être réalisées qu’en ayant recours au parallélisme massif, sur des machines à plus de 8000 processeurs
hautes performances [27], qui peuvent atteindre le Péta-flops de puissance
soutenue.
– La vitesse de la lumière est (actuellement) une limitation intrinsèque à la
vitesse des processeurs. Supposons en effet que l’on veuille construire une
machine entièrement séquentielle disposant d’une puissance de 1 Tflops et de
1 To de mémoire.
Soit d la distance maximale entre la mémoire et le micro-processeur. Cette
distance doit pouvoir être parcourue 1012 fois par seconde à la vitesse de la
lumière, c ≈ 3.108 m.s−1 , d’où :
d≤
3.108
= 0, 3mm .
1012
L’ordinateur devrait donc tenir dans une sphère de 0, 3 mm de rayon. Avec
cette contrainte de distance, si l’on considère la mémoire comme une grille
◦
carrée de 106 × 106 octets, alors chaque octet doit occuper une cellule de 3A
de côté, c’est à dire la surface occupée par un petit atome. On ne tient ici
pas compte de l’espace nécessaire à l’acheminement de l’information et de
l’énergie, ainsi qu’à l’extraction de la chaleur.
Cette argumentation est biaisée en ce que la mise en œuvre d’une hiérarchie
mémoire (voir section 4.1) permet d’augmenter la distance entre la mémoire
de masse et l’unité de traitement. Néanmoins, elle reste globalement valable.
– La fréquence des processeurs stagne depuis plusieurs années, et le parallélisme
à grain fin qu’il est possible d’extraire d’un unique flot d’instructions (« instruction-level parallelism ») est déjà exploité. La seule utilisation rationnelle
du nombre croissant de transistors gravables sur une puce, dont la densité
d’intégration continue toujours à augmenter suivant la loi de Moore, consiste
donc en l’intégration de plusieurs unités d’exécution (« hyper-threading ») ou
de processeurs complets (« multi-core », traduit en « multi-cœurs »). Les processeurs quadri-cœurs sont déjà disponibles au sein de machines de bureau, et
les constructeurs annoncent des processeurs à 80 cœurs, voire plus. La mise en
œuvre simultanée de ces multiples unités de traitement ne peut se faire qu’en
ayant recours au parallélisme. De ce fait, les techniques de programmation parallèle, jusqu’il y a peu réservées au monde du calcul scientifique, commencent
à se démocratiser, car toutes les grandes applications informatiques doivent
1 Dans
la nomenclature internationale, Kilo est synonyme de 103 , Méga de 106 , Giga de 109 ,
Téra de 1012 , Péta de 1015 et Exa de 1018 . En langage scientifique, on ne dit donc pas « Mille
milliards de mille sabords », mais « Un péta-sabord ».
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
10
CHAPITRE 1. INTRODUCTION
être repensées en terme de tâches concurrentes pour exploiter au mieux la
performance des processeurs à venir.
1.3
La recherche en parallélisme
L’utilisation efficace de machines parallèles nécessite de travailler sur :
– l’architecture des machines. Il faut assurer que :
– la machine est extensible (« scalable ») : on peut (facilement) augmenter la
taille de la machine sans que les performances s’écroulent ;
– les échanges de données entre processeurs sont rapides, pour éviter leur
famine ;
– les entrées/sorties ne sont pas pénalisantes.
– les modèles d’expression du parallélisme : chaque algorithme possède un modèle
de parallélisme avec lequel il s’exprime mieux ;
– les langages parallèles : il faut choisir le langage le plus adapté au problème ;
– l’algorithmique proprement dite : de nombreux problèmes pour lesquels il
existe un algorithme séquentiel optimal ne possèdent encore pas de contrepartie parallèle efficace ;
– l’environnement de développement : débogueurs, profileurs, bibliothèques portables, etc. ;
– la parallélisation automatique : compilateurs « data-parallèles » ou « multithreads » avec directives dans le cas de HPF [16] et d’OpenMP [21], parallélisation automatique de boucles, etc.
Cours d’architectures et systèmes des calculateurs parallèles
Chapitre 2
Modèles de calculateurs
parallèles
Afin de définir et comparer les architectures de machines, plusieurs classifications
ont été développées.
2.1
Classification de Flynn
La classification la plus connue est celle de Flynn [6], qui caractérise les machines
suivant leurs flots de données et d’instructions, ce qui donne quatre catégories :
– SISD (« Single Instruction stream, Single Data stream »). Cette catégorie correspond aux machines séquentielles conventionnelles, pour lesquelles chaque
opération s’effectue sur une donnée à la fois (figure 2.1) ;
FI
UC
E/S
FI
UT
FD
UM
Fig. 2.1 – Architecture SISD. L’unité de contrôle (UC), recevant son flot d’instructions (FI) de l’unité mémoire (UM), envoie les instructions à l’unité de traitement
(UT), qui effectue ses opérations sur le flot de données (FD) provenant de l’unité
mémoire.
– MISD (« Multiple Instruction stream, Single Data stream »). Cette catégorie
regroupe les machines spécialisées de type « systolique », dont les processeurs,
arrangés selon une topologie fixe, sont fortement synchronisés (figure 2.2) ;
FI
UM
UC
E/S
UC
FI
FD
UT
FI
FD
UT
Fig. 2.2 – Architecture MISD.
11
UC
FI
FD
UT
12
CHAPITRE 2. MODÈLES DE CALCULATEURS PARALLÈLES
– SIMD (« Single Instruction stream, Multiple Data stream »). Dans cette classe
d’architectures, les processeurs sont fortement synchronisés, et exécutent au
même instant la même instruction, chacun sur des données différentes (figure 2.3). Des informations de contexte (bits de masquage) permettent d’inhiber l’exécution d’une instruction sur une partie des processeurs.
FI
UT
FI
E/S
UC
UT
UT
FD
FD
FD
UM
UM
UM
FD
FD
UM
FD
Fig. 2.3 – Architecture SIMD.
Ces machines sont adaptées aux traitements réguliers, comme le calcul matriciel sur matrices pleines ou le traitement d’images. Elles perdent en revanche
toute efficacité lorsque les traitements à effectuer sont irréguliers et dépendent
fortement des données locales, car dans ce cas les processeurs sont inactifs la
majorité du temps.
Ainsi, pour exécuter une instruction conditionnelle de type if. . . then. . . else
(figure 2.4), l’ensemble des instructions des deux branches doit être présenté
aux processeurs, qui décident ou non de les exécuter en fonction de leur bit
local d’activité, positionné en fonction des valeurs de leurs variables locales.
Chacun des processeurs n’exécutera effectivement que les instructions de l’une
des branches.
Code source
Code compilé
Exécution,
cond=VRAI
Exécution,
cond=FAUX
blocA
if (cond)
blocV;
else
blocF;
blocA;
ACTIF = (cond);
blocV;
ACTIF = ~ACTIF;
blocF;
ACTIF = VRAI
blocB
blocA;
ACTIF = (cond);
blocV;
ACTIF = ~ACTIF;
-ACTIF = VRAI
blocB
blocA;
ACTIF = (cond);
-ACTIF = ~ACTIF;
blocF;
ACTIF = VRAI
blocB
blocB
Fig. 2.4 – Exécution d’une expression conditionnelle if. . . then. . . else sur une
architecture SIMD.
– MIMD (« Multiple Instruction stream, Multiple Data stream »). Cette classe
comprend les machines multi-processeurs, où chaque processeur exécute son
propre code de manière asynchrone et indépendante. On distingue habituellement deux sous-classes, selon que les processeurs de la machine ont accès à
une mémoire commune (on parle alors de MIMD à mémoire partagée, « multiprocessor », figure 2.5), ou disposent chacun d’une mémoire propre (MIMD
à mémoire distribuée, « multicomputer », figure 2.6). Dans ce dernier cas, un
réseau d’interconnexion est nécessaire pour échanger les informations entre
processeurs.
Cette classification est trop simple, car elle ne prend en compte ni les machines
vectorielles (qu’il faut ranger dans la catégorie SISD et non pas SIMD, car elles ne
Cours d’architectures et systèmes des calculateurs parallèles
13
2.2. CLASSIFICATION DE RAINA
FI
E/S
UC
E/S
UC
FI
FI
FI
UC
FD
UT
FD
UT
UM
FD
UT
Fig. 2.5 – Architecture MIMD à mémoire partagée.
E/S
E/S
UC
UC
UC
FI
FI
FI
UT
UT
UT
FD
FD
FD
UM
UM
FI
RI
UM
Fig. 2.6 – Architecture MIMD à mémoire distribuée. Les échanges d’informations
passent par un réseau d’interconnexion (RI) spécifique.
disposent que d’un seul flot mémoire, voir section 3.7), ni les différences d’architecture mémoire.
2.2
Classification de Raina
Une sous-classification étendue des machines MIMD, due à Raina [24], et illustrée
en figure 2.7, permet de prendre en compte de manière fine les architectures mémoire,
selon deux critères :
– l’organisation de l’espace d’adressage :
– SASM (« Single Address space, Shared Memory ») : mémoire partagée ;
– DADM (« Distributed Address space, Distributed Memory ») : mémoire
distribuée, sans accès aux données distantes. L’échange de données entre
processeurs s’effectue nécessairement par passage de messages, au moyen
d’un réseau de communication ;
– SADM (« Single Address space, Distributed Memory ») : mémoire distribuée, avec espace d’adressage global, autorisant éventuellement l’accès
aux données situées sur d’autres processeurs.
– le type d’accès mémoire mis en œuvre :
– NORMA (« No Remote Memory Access ») : pas de moyen d’accès aux
données distantes, nécessitant le passage de messages ;
– UMA (« Uniform Memory Access ») : accès symétrique à la mémoire, de
coût identique pour tous les processeurs ;
– NUMA (« Non-Uniform Memory Access ») : les performances d’accès dépendent
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
14
CHAPITRE 2. MODÈLES DE CALCULATEURS PARALLÈLES
de la localisation des données ;
– CC-NUMA (« Cache-Coherent NUMA ») : type d’architecture NUMA
intégrant des caches ;
– OSMA (« Operating System Memory Access ») : les accès aux données
distantes sont gérées par le système d’exploitation, qui traite les défauts de
page au niveau logiciel et gère les requêtes d’envoi/copie de pages distantes ;
– COMA (« Cache Only Memory Access ») : les mémoires locales se comportent comme des caches, de telle sorte qu’une donnée n’a pas de processeur propriétaire ni d’emplacement déterminé en mémoire.
MIMD
DADM
SASM
SADM
NORMA
UMA
NUMA
CRAY XTx
Sequent Symmetry
CRAY T3D,E,F
Dash
Munin
DDM
IBM Blue Gene
CRAY X,Y, C
Flash
Ivy
KSR 1,2
SUN Constellation
SGI Power Challenge
SGI Origin
Koan
SGI NUMAflex
Myoan
CC−NUMA
OSMA
COMA
Fig. 2.7 – Classification MIMD de Raina.
Cours d’architectures et systèmes des calculateurs parallèles
Chapitre 3
Architecture des processeurs
L’obtention de performances élevées sur une architecture parallèle nécessite l’obtention de performances de calcul élevées sur chacun des nœuds qui la composent.
Pour ce faire, il est nécessaire de connaı̂tre les principes architecturaux mis en œuvre
dans les processeurs haut-de-gamme actuels, afin d’en tirer pleinement parti lors de
l’écriture des logiciels, tout en conservant à ceux-ci une portabilité maximale.
Ces principes architecturaux portent tant sur la façon de câbler les fonctions
élémentaires des processeurs, que sur les mécanismes avancés permettant d’extraire
le parallélisme du flot séquentiel d’instructions à exécuter, et que l’on désigne par
le terme d’« Instruction-Level Parallelism »(ILP).
3.1
Horloge
La vitesse d’un processeur dépend en premier lieu de la durée de son cycle
d’horloge, qui cadence le système. Plus cette période est courte, plus le processeur est
rapide. Cependant, disposer d’un processeur rapide ne sert à rien si les composants
annexes (bus, mémoire) sont trop lents : le processeur passera son temps à les
attendre.
La fréquence d’horloge est le nombre de cycles d’horloge par seconde, mesurée
en Hertz (Hz). La relation entre le cycle d’horloge τ et la fréquence d’horloge f est
donnée par la relation f = τ1 .
Actuellement, selon la technologie utilisée (bipolaire, CMOS, etc.), les temps de
cycle vont de 200 ps à 10 ns, ce qui correspond à des fréquences de 100 MHz à
5 GHz. On ne peut augmenter la fréquence à l’infini, et l’on pense que les limites de
la technologie actuelle seront bientôt atteintes, aux alentours de 10 GHz. Cependant,
les fréquences actuelles plafonnent bien en deçà de cette limite, aux alentours des
2, 5 GHz, à cause d’une autre contrainte, la « barrière thermique ».
En effet, l’augmentation de la densité en transistors (gravures avec des pas de
masque de 65 nm, puis de 40 nm), couplée à ces fréquences déjà hautes, induit sur
la surface des processeurs une concentration d’énergie très élevée, au point que la
densité d’énergie thermique rayonnée par effet Joule n’est actuellement plus que
d’un ordre de grandeur inférieure à celle présente au sein d’un réacteur nucléaire
(mais sur un volume bien moindre !). Cette énergie thermique doit absolument être
évacuée, faute de quoi le processeur serait endommagé. Les températures mesurées
au niveau des cœurs des puces dépassent très rapidement la centaine de degrés
en fonctionnement soutenu, et la réduction des déperditions thermiques est un sujet d’études très actif chez les fabricants, qui cherchent à mettre en œuvre des
mécanismes de coupure de l’alimentation des unités fonctionnelles lorsque celles-ci
ne sont pas actives. Cependant, cela ne résout pas le problème pour les processeurs
15
16
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
haute performance, dont on cherche à ce qu’ils soient utilisés au plus près de leur
puissance de crête.
La solution naturelle pour contourner la barrière thermique consiste à éclater
les zones de forte dissipation sur la puce, c’est-à-dire à s’orienter vers une conception multi-cœurs. Comme on le verra par la suite, cette solution est aussi la seule
permettant de tirer parti des transistors disponibles alors que l’« instruction-level
parallelism »ne peut plus apporter de gains significatifs.
Il faut également noter qu’il y a un lien direct entre la fréquence d’horloge et
le prix du processeur (c’est d’ailleurs un argument commercial). Le parallélisme
semble donc intéressant en ce qu’il permet de tirer parti de processeurs moins
puissants, mais plus nombreux. Cela devient même un argument commercial, sous
la dénomination de « green computing », désignant des machines dissipant moins
d’énergie et nécessitant donc une infrastructure et des coûts d’exploitation moindres.
3.2
Câblage
Un premier moyen d’accélérer le traitement des opérations par le processeur
consiste à exhiber le parallélisme au niveau des bits. Nous allons illustrer cette
approche en étudiant un additionneur et un multiplicateur en arithmétique entière.
3.2.1
Additionneur
On peut réaliser l’addition de deux bits au moyen du circuit présenté en figure 3.1, appelé « demi-additionneur » (« half-adder », ou HA), et constitué de
deux niveaux de portes logiques « et » et « ou ». Le bit s correspond à la somme,
et c à la retenue.
y
0
1
1
c
1
2
1
x
0
2
s
x y
0 0
0 1
1 0
1 1
c
0
0
0
1
s
0
1
1
0
si
ci
= xi ⊕ yi
= xi yi
= xi yi + xi yi
Fig. 3.1 – Schéma, table, et équations logiques d’un demi-additionneur binaire
(HA).
En combinant deux HA, on peut réaliser une tranche d’additionneur complet
(« full adder », ou FA), représentée en figure 3.2.
En chaı̂nant ensemble des FA, on peut alors construire un additionneur par
propagation de retenue (« Ripple Carry Adder », ou RCA), montré en figure 3.3.
Le temps de calcul d’une addition sur n bits est donc :
tRCA (n) = 2n + 2 .
L’additionneur ci-dessus présente une très forte séquentialité, qui dérive de la
nécessité de connaı̂tre la retenue cin du bit i pour calculer celle du bit i + 1. Plus la
valeur des retenues partielles sera connue tôt, et plus le calcul des bits de la somme
pourra être accéléré. Le circuit FA peut nous apporter des informations, au prix
d’une légère modification.
Le circuit modifié FA’ de la figure 3.4 possède deux sorties supplémentaires, g et p,
qui indiquent respectivement si une retenue a été générée au sein de l’additionneur,
et si une retenue cin éventuelle sera propagée.
Cours d’architectures et systèmes des calculateurs parallèles
17
3.2. CÂBLAGE
x y
s
0,0
4,t+2
x
y
HA
c
s
1,1
2,2
c out
c in
4,t+2
0,t
x
y
HA
c
s
3,t+1
4,t+2
Fig. 3.2 – Schéma d’une tranche d’additionneur binaire (FA).
x4 y4 s 4
x3 y3 s 3
x y s
c
12
FA
x2 y2 s 2
x y s
c
c
10
FA
x1 y1 s 1
x y s
c
c
8
FA
x0 y0 s 0
x y s
c
c
6
FA
x y s
c
c
4
FA
c
0
Fig. 3.3 – Schéma d’un additionneur binaire à propagation de retenue (RCA).
Si l’on considère deux additionneurs FA’, et que l’on cherche à calculer les valeurs
globales de g et p, on trouve :
xh yh s h
xl yl s l
x y s
c out
c
g
g
p
cout
x y s
FA’c
c
p
FA’c
g
gh ph
c in
p
= gh + gl ph
= ph pl
= pcin + g
gl pl
Les formules logiques de p, g, et cout peuvent être câblées dans le circuit combineur de retenue (« Carry Merger », ou CM) présenté en figure 3.5, page 18.
On peut alors réaliser un additionneur complet par pré-calcul de retenue (« Carry
Lookahead Adder », ou CLA) selon le schéma de la figure 3.6, page 19.
Le temps de calcul sur n = 2k bits de cet additionneur est donc :
tCLA (n)
3 + 2 (log2 (n) − 2) + 2 + 2
= |{z}
2 + 2 (log2 (n) − 2) + |{z}
|
{z
}
{z
} | {z }
|
virage
descente FA’ descente
remontée remontée FA’
=
4 ⌈log2 (n)⌉ + 1
Le tableau ci-dessous permet de constater les gains réalisés.
n
RCA
CLA
3.2.2
8
18
13
16
34
17
32
66
21
64 128
130 258
25
29
Multiplicateur
En arithmétique binaire, la multiplication s’exprime simplement au moyen de
décalages et d’additions. Par exemple, si l’on considère la multiplication de deux
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
18
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
x y
s
0,0
4,t+2
x
y
HA
c
s
1,1
2,2
c out
c in
x
y
HA
c
2,2
s
2,2
g p
Fig. 3.4 – Schéma d’une tranche d’additionneur binaire modifiée (FA’).
gh ph gl pl
2,u
2,u
3,u+1
4,u+2
3,u+1
c out
0,t
5,max(t+2,u+3)
c in
4,max(t+1,u+2)
4,u+2
4,u+2
g
p
Fig. 3.5 – Schéma d’un combineur de retenues (CM).
nombres A et B codés chacun sur 8 bits :
∗
+
+
+
+
+
+
+
A =
B =
1
1 0
0
1 0
0 1
0 1
1
0
1
1
0
0
0
0
1
0
0
0
0
1
0
0
0
0
1
0
0
1
0
0
0
1
1
1
1
0
0
0
0
0
0
1
1
0
1
0
1
0
0
0
0
1
1
0
1
1
0
0
0
0
1 1
1
1
1
0
0
0
1
0
0
0
0
0
0
0 0
0
0
0
0
0
0
1
0
1
1
1
1
0 1
1
multiplier A par B revient à sommer les huit produits partiels obtenus en décalant
de i bits le mot obtenu par un « et » logique entre chacun des bits de A et le ième
bit de B.
La réalisation d’un multiplicateur efficace est bien plus délicate que celle d’un
additionneur, en ce que l’opération de multiplication nécessite de nombreuses additions. Le goulet d’étranglement d’un additionneur étant la propagation de la reteCours d’architectures et systèmes des calculateurs parallèles
19
3.2. CÂBLAGE
x7 y7 s 7
x6 y6 s 6
13
11
x y s
c
g
2
h
c
g
2
l
h
9
p
c
c
p
2
CM c
g
g
2
l
h
g
c
p
h
5
c
0
2
l
CM c 0
g
p
4
4
virage
l
h
7
p
p
2
p
CM c
g
g
l
remontée
h
FA’c
c
p
2
4
c
g
CM c
g
x y s
FA’c
c
p
2
CM c
4
g
4
x y s
FA’c
c
p
x0 y0 s 0
6
x y s
FA’c
g
x1 y1 s 1
7
x y s
FA’c
c
p
x2 y2 s 2
9
x y s
FA’c
c
p
x3 y3 s 3
9
x y s
FA’c
c
p
x4 y4 s 4
11
x y s
FA’c
g
x5 y5 s 5
c
descente
l
CM c 0
g
6
p
6
h
9
c
l
CM c 0
g
p
Fig. 3.6 – Schéma d’un additionneur binaire à pré-calcul de retenue (CLA).
nue, qui sérialise les calculs, il faut limiter leur nombre autant que possible dans le
multiplicateur afin d’obtenir un parallélisme maximum.
L’additionneur « à conservation de retenue » (« Carry Save Adder », ou CSA)
permet d’effectuer l’addition de trois nombres binaires, en conservant les retenues
de chaque bit dans un vecteur auxiliaire. Ainsi, si X, Y , et Z sont trois nombres
codés sur 8 bits, on aura :
+
+
avec
sbi
cbi
X
Y
Z
Sb
Cb
=
=
=
=
=
1
0
1
1
0
1
1
1
1
1
0
1
0
0
1
1
1
1
0
0
1
0
1
1
0
0
0
0
1
1
0
1
0
0
1
0
0
1
0
1
= xi ⊕ yi ⊕ zi = xi yi zi + xi yi zi + xi yi zi + xi yi zi
= xi−1 yi−1 + xi−1 zi−1 + yi−1 zi−1 ,
où S b est le vecteur somme bit à bit, toujours sur 8 bits, et C b est le vecteur retenue
bit à bit, sur neuf bits mais tel que le bit de poids le plus faible soit toujours zéro.
Le résultat produit vérifie bien S b + C b = X + Y + Z, et tous les bits de S b et C b
ont été calculés en parallèle.
En combinant entre eux les additionneurs CSA pour former un arbre de Wallace,
et en intercalant des tampons (« latches »), on construit un multiplicateur pipeliné (voir section 3.4 pour de plus amples informations sur les pipe-lines), présenté
en figure 3.7, dont le dernier étage est un additionneur à propagation de retenue
(« Carry Propagate Adder », ou CPA) comme l’additionneur CLA décrit plus haut.
En utilisant autant que possible des additionneurs CSA, on a considérablement
réduit le nombre de propagations de retenues à calculer (il n’en reste qu’une seule,
inévitable). Pour un résultat sur 16 bits, l’additionneur CLA a une profondeur
de quatre niveaux, chaque CSA nécessite deux niveaux de portes logiques, et la
génération des produits partiels (au moyen de « et » logiques) nécessite également
deux niveaux. Le pipe-line est donc relativement équilibré.
Il est à remarquer que, par le passé, plusieurs méthodes de multiplication ont
été proposées pour réduire la complexité des calculs de retenues. On peut citer par
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
20
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
E1
8
8
8
8
Calcul des produits partiels
15
14
15
13
12
11
10
9
8
13
12
11
10
9
8
14
CSA
CSA
13
E2
13
CSA
15
10
10
CSA
13
15
15
13
15
13
13
CSA
15
E3
15
CSA
16
16
16
16
CPA
E4
16
16
Fig. 3.7 – Schéma d’un multiplicateur binaire.
exemple la méthode dite « à jalousie », mise au point par les mathématiciens arabes
du Moyen-âge, illustrée en figure 3.8.
Dans les deux exemples d’opérateurs arithmétiques présentés dans cette section,
à chaque fois, on a gagné en temps au prix d’un surcoût de calcul (mesuré ici en
nombre de portes logiques).
3.3
Jeu d’instructions
On oppose conceptuellement deux types de jeux d’instructions :
– RISC (« Reduced Instruction Set Computer ») : jeu d’instructions réduit ;
– CISC (« Complex Instruction Set Computer ») : jeu d’instructions complexe.
Dans les faits, ces concepts impliquent d’autres choix technologiques, que nous allons présenter dans cette section.
Les premiers processeurs étaient RISC par nature, puisqu’ils possédaient un jeu
d’instructions très réduit. Dans les années 1960-1970, on s’est orienté vers une complexification des jeux d’instructions, afin de simplifier l’écriture des compilateurs
et d’économiser la mémoire en réduisant la taille des programmes. L’amélioration
des techniques d’intégration et l’augmentation des fréquences d’horloge compenCours d’architectures et systèmes des calculateurs parallèles
21
3.3. JEU D’INSTRUCTIONS
2
7
8
2
4
3
2
4
8
9 2
0
0
0
0
0
0
0
9
7
2
1
2
8
4
2
1
2
0 3
6
0 4
8
1 6
2
2
Fig. 3.8 – Représentation et calcul par « jalousie » de la multiplication 8072 × 346.
Chaque produit partiel des chiffres de chacun des nombres est stocké sous la forme
de deux chiffres, dizaine et unité, dans chaque demie-case de la jalousie (par exemple,
8 × 3 = 2|4, en haut à gauche). Ensuite, les chiffres contenus dans chaque diagonale
sont sommés, diagonale après diagonale, de droite à gauche, et la retenue éventuelle
est propagée à la diagonale suivante, pour donner le résultat final.
saient largement le surcoût induit par cette complexification. Le plus souvent, les
instructions complexes n’étaient pas câblées, mais micro-codées.
À titre d’exemple, le jeu d’instructions de la famille VAX de DEC possédait 304
instructions, dont certaines étaient de très haut niveau : l’instruction POLY servait
à évaluer les polynômes !
Des études statistiques ont alors montré que la plupart des instructions n’étaient
en fait pas utilisées, car trop spécialisées et de trop haut niveau pour qu’un compilateur puisse les générer à partir d’un code source. On s’est donc naturellement orienté
vers une simplification des jeux d’instructions, marquée par la naissance en 1972
du premier processeur délibérément RISC, le RISC I de l’Université de Berkeley,
qui ne possédait que 32 instructions. On peut remarquer que cette date charnière
coı̈ncide avec celle du développement de la théorie de la compilation, qui a permis
la réalisation de compilateurs efficaces.
Réduire la taille du jeu d’instructions permet de gagner :
– en temps de décodage des instructions, du fait de la plus grande simplicité de
celles-ci, ce qui permet de réduire le nombre de niveaux de portes logiques à
traverser pour exécuter une instruction ;
– en surface d’intégration, de par la réduction de la circuiterie de décodage et
l’absence de la gestion du micro-code, ce qui diminue la longueur maximale
des pistes à l’intérieur du processeur.
La combinaison de ces deux gains, en termes de nombre de niveaux logiques et de
longueur de pistes, permet une augmentation significative de la fréquence d’horloge
dans les architectures RISC, par rapport aux processeurs CISC. Un rapport de deux
à quatre est courant à l’heure actuelle.
Les caractéristiques généralement admises des architectures RISC sont les suivantes :
– toutes les instructions ont le même format et la même taille. Ceci simplifie leur
décodage, mais aussi les accès mémoire, car dans la plupart des processeurs
RISC les instructions doivent être alignées sur la taille d’un mot machine.
Cette caractéristique est également essentielle pour l’optimisation des architectures pipe-linées ;
– le jeu d’instruction est de type « load-store ». Les seules instructions pouvant
accéder à la mémoire sont les opérations « load » et « store », les autres
opérations n’opérant que sur les registres du processeur. Les processeurs RISC
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
22
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
disposent donc d’un grand nombre de registres, afin de stocker les valeurs des
opérandes qui ne peuvent plus être directement accédées en mémoire ;
– l’architecture est « orthogonale ». Chaque instruction peut utiliser indifféremment toutes les opérandes des types autorisés. On n’a donc pas de registres
spécialisés, comme c’est par exemple le cas pour l’architecture IA-32, où seul le
registre AX sert aux opérations arithmétiques, le registre CX comme compteur
de répétition, etc. ;
– la plupart des instructions s’exécutent en un cycle d’horloge. Dans les architectures RISC « pures », toutes les instructions s’exécutent en un cycle
d’horloge, à part les accès mémoire qui peuvent prendre plus de temps. Cependant, pour des raisons d’efficacité, on tend actuellement à inclure dans des
processeurs dits RISC des instructions (le plus souvent arithmétiques : multiplication, division) s’exécutant en quelques cycles d’horloge, bien plus efficaces
que leur contrepartie logicielle car câblées de façon optimisée ; ces processeurs
évolués sont parfois appelés CRISC (« Complexified RISC »). Dans tous les
cas cependant, on n’a jamais recours à un micro-code ;
– le jeu d’instructions est limité uniquement aux instructions nécessaires en
terme de compromis performance/place/pipe-line. Ainsi, les premiers processeurs SPARC ne possédaient pas de multiplication câblée (ce qui a été rajouté
assez rapidement, il est vrai. . . ).
Ces choix architecturaux ont un effet certain sur l’écriture des compilateurs. Si
leur écriture peut sembler plus compliquée au premier abord (nécessité d’émuler les
modes d’adressage étendus, les opérations arithmétiques complexes, etc.), on peut
gagner par rapport aux processeurs CISC pour lesquels :
– les instructions de trop haut niveau (comme le POLY du VAX) ne peuvent
être générées à partir d’un langage de bas niveau (comme le C) ;
– l’architecture non-orthogonale complique la gestion des registres (sauvegardes
et restauration perpétuelles).
3.4
Pipe-line
3.4.1
Principe
Le pipe-line est une technique permettant d’exploiter le parallélisme induit par
l’exécution répétée d’une même opération sur des données distinctes. On peut très
simplement comparer un pipe-line à une chaı̂ne de montage dans une usine. On
décompose l’unité de traitement de l’opération en sous-unités indépendantes, qui
travaillent en parallèle sur les données qui se présentent séquentiellement à elles,
chaque sous-unité travaillant à un instant donné sur une donnée différente.
Trois conditions sont nécessaires à l’élaboration d’un pipe-line :
– une opération de base doit être répétée dans le temps ;
– cette opération doit pouvoir être décomposée en étapes (étages) indépendants ;
– la complexité de ces étages doit être à peu près la même. Si ce n’est pas le
cas, on peut multiplexer les étages les plus coûteux pour augmenter le débit
du pipe-line, comme illustré en figure 3.9.
Ainsi, typiquement, le traitement des instructions par le processeur peut se
décomposer en cinq étapes :
– « fetch » : recherche de la prochaine instruction à exécuter ;
– « decode » : décodage de l’instruction, avec calculs éventuels des adresses ;
– « read » : chargement des opérandes dans l’unité d’exécution, par lecture à
partir des registres ou de la mémoire ;
Cours d’architectures et systèmes des calculateurs parallèles
23
3.4. PIPE-LINE
t3
t6
t5
t4
t1
t0
t2
Fig. 3.9 – Utilisation du multiplexage pour augmenter le débit d’un pipe-line. Les
commutateurs basculent à chaque cycle.
– « execute » : exécution proprement dite de l’instruction ;
– « write » : écriture du résultat vers les registres ou la mémoire.
Le nombre d’étages composant un pipe-line est appelé profondeur du pipe-line.
Dans le cas général, un pipe-line de profondeur p peut exécuter n opérations en
p + n − 1 étapes, s’il n’y a pas de bulles. Sans pipe-line, le temps d’exécution serait
np
. Quand n ≫ p, n + p − 1 ≃ n, ce
de n p, d’où un facteur d’accélération de n+p−1
qui donne une accélération de p, ce qui suggère d’augmenter le nombre d’étages afin
de bénéficier de l’accélération la plus grande possible. Cependant, plus le nombre
d’étages augmente, et plus le risque d’apparition de bulles augmente, ce qui réduit
l’efficacité du pipe-line.
En règle générale, le nombre d’étages des pipe-lines d’instructions est donc
compris entre 5 et 15. Ainsi, si le Pentium d’Intel avait 5 étages, les Pentium II
et III en avaient 12, et le Pentium IV en avait 20, ce qui lui a permis d’augmenter
considérablement sa fréquence de fonctionnement par rapport aux précédents.
Les figures 3.10 et 3.11 représentent l’exécution dans le temps d’une séquence
d’instructions. Sans pipe-line, l’exécution de chaque instruction ne peut se faire que
lorsque la précédente a été entièrement traitée. Avec pipe-line, les différents étages
de traitement peuvent travailler en parallèle, si les instructions ne présentent pas
de dépendances. Sinon, des bulles peuvent apparaı̂tre à l’exécution.
F
D
R
E
Temps
W
F
D
R
E
W
F
D
R
E
W
Instructions
Fig. 3.10 – Exécution d’une séquence d’instructions sur une machine non pipelinée.
Un autre domaine d’application classique des pipe-line est l’unité arithmétique
et logique, dont les nombreuses fonctions sont susceptibles d’être pipe-linées. Le
calcul d’une addition en virgule flottante peut ainsi se décomposer en cinq étapes :
– comparaison des exposants et calcul de leur différence (soustraction entière) ;
– alignement des mantisses en conséquence (décalage) ;
– addition des mantisses (addition entière) ;
– calcul du facteur de renormalisation (comptage de bits à zéro) ;
– normalisation du résultat (décalage).
Le mode de représentation des nombres à virgule flottante doit inciter à énormément de prudence lorsqu’on manipule ces derniers, pour éviter les accumulations
d’erreurs d’arrondi. Celles-ci arrivent lorsqu’on soustrait deux quantités de même
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
24
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
F
D
R
E
W
F
D
R
E
F
D
F
1111
0000
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
En cas de conflit (ici, RAW),
des ‘‘bulles’’ apparaissent
W
R
E
W
D
R
E
W
F
D
R
E
Temps
W
Instructions
Fig. 3.11 – Exécution d’une séquence d’instructions sur une machine pipelinée à
cinq étages. La troisième instruction nécessitant le résultat calculé par la première
(conflit « Read After Write », ou « RAW »), une bulle apparaı̂t dans le pipe-line.
signe et de même ordre de grandeur. Il faut donc veiller à ordonner les calculs
de façon à ce que cela arrive le moins possible. Par exemple, plutôt que de multiplier par un grand nombre la soustraction entre deux nombres de même ordre
de grandeur, il est préférable de soustraire le résultat de la multiplication de chacun des deux nombres, afin de ne pas multiplier l’erreur éventuelle. On gagne ainsi
en stabilité numérique ce que l’on perd en efficacité (pour l’exemple ci-dessus, le
temps d’exécution serait cependant le même sur une architecture superscalaire de
degré 2). De plus amples informations sur le codage des nombres à virgule flottante
sont disponibles en annexe A, page 73.
3.4.2
Pipe-lines non linéaires
Afin d’implémenter des opérations complexes tout en économisant de la place,
il est souhaitable de câbler plusieurs fonctions au sein de la même unité pipe-line,
ou de réutiliser la même unité plusieurs fois de suite. Dans ce cas, en plus des liens
directs entre étages voisins, on trouvera des connexions avant (« feedforward ») et
arrière (« feedback »), ainsi que plusieurs sorties, qui seront activées ou non suivant
la configuration dynamique du pipe-line, comme illustré en figure 3.12.
Y
E1
E2
E3
X
Fig. 3.12 – Exemple de pipe-line non linéaire.
Ces connexions non-linéaires compliquent beaucoup l’utilisation du pipe-line,
et en particulier la réservation des différents étages en fonction des opérations
demandées. L’enchaı̂nement des opérations dans le pipe-line est habituellement
représenté au moyen d’une table de réservation (« reservation table »), dont les lignes
représentent les étages du pipe-line, et les colonnes les pas de temps nécessaires à
l’évaluation de la fonction associée. Dans le cas d’un pipe-line linéaire, cette table
est triviale, puisque les étages sont traversés dans l’ordre. Dans le cas d’un pipe-line
non-linéaire, les tables sont plus complexes, et à une structure de pipe-line donnée
peuvent correspondre plusieurs tables, définissant chacune une fonction différente,
comme par exemples celles définies en figure 3.13.
Le fonctionnement du pipe-line peut lui aussi être représenté sous forme de table,
avec un format dérivant de celui des tables de réservation. Les cases non vides de
la table d’exécution sont alors indicées par le numéro d’instance de la fonction en
train de s’exécuter à partir du temps de référence, comme illustré en figure 3.14.
Cours d’architectures et systèmes des calculateurs parallèles
25
3.4. PIPE-LINE
E1
X
X
E2
X
X
E3
X
X
E1
Y
X
X
Y
Y
E2
Y
Y
E3
Y
Fig. 3.13 – Tables de réservation pour le pipe-line non linéaire de la figure 3.12.
E1
X2
X1
X1
E3
X2
X1
X1
X1
E2
X2
X1
X2
X1
X2
X1
X3
X3
X3
X3
X3
X2
X2
X3
X3
X3
X2
Fig. 3.14 – Table d’utilisation du pipe-line non linéaire de la figure 3.12 pour calculer
la fonction X définie en figure 3.13. L’exécution de X2 est lancée trois cycles après
celle de X1 , et celle de X3 est lancée 6 cycles après celle de X2 .
Le nombre de pas de temps séparant deux exécutions d’une fonction dans le
pipe-line est appelé « latence ». Lorsque deux instances de fonctions nécessitent un
même étage du pipe-line en même temps, il y a collision. Les collisions se produisent
pour des valeurs de latence particulières, qui sont appelées « latences interdites ».
Ainsi, pour la fonction X définie en figure 3.13, les latences 2, 4, et 5 sont-elles
interdites.
E1
X2
X1
E3
X2
X1
X2
X2
X 1X 2
X1
X1
X2
X2
X 1X 2
X1
X1
E2
Fig. 3.15 – Collision entre X1 et X2 lorsque l’exécution de celle-ci est lancée 4 cycles
après celle de X1 .
Les latences interdites se déduisent simplement des tables de réservation. Elles
correspondent aux distances entre cases occupées appartenant aux mêmes lignes.
Une séquence de latences est une séquence de latences autorisées entre exécutions
successives. Un cycle de latences est une séquence de latences répétant indéfiniment
le même motif. Il peut y en avoir plusieurs, comme illustré en figures 3.16 et 3.17.
E1
E2
E3
X1
X1
X2
X2
X1
X2
X1
X2
X1
X2
X1
X2
X1
X2
X1
X3
X2
X3
X4
X4
X3
X4
X3
X4
X3
X4
X3
X4
X3
X4
X3
X5
X6
X5
X4
Fig. 3.16 – Table d’utilisation du pipe-line pour calculer la fonction X, avec le cycle
de latences { 1, 6 }.
La latence moyenne d’un cycle de latences est obtenue en divisant la somme
de toutes les latences du cycle par le nombre de latences contenues dans le cycle.
Un cycle de latences constant est un cycle ne contenant qu’une unique valeur de
latence. Du point de vue de l’efficacité, on souhaite déterminer le cycle donnant le
débit le plus élevé, c’est-à-dire correspondant la latence moyenne minimale (« Minimal Average Latency », ou MAL).
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
26
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
E1
E2
E3
X1
X2
X1
X1
X1
X1
X3
X2
X4
X3
X5
X4
X6
X2
X1
X2
X3
X2
X3
X4
X3
X4
X5
X4
X5
X1
X2
X1
X2
X3
X2
X3
X4
X3
X4
X5
X4
Fig. 3.17 – Table d’utilisation du pipe-line pour calculer la fonction X, avec le cycle
de latences { 3 }.
En examinant la table de réservation, il est possible de déterminer l’ensemble des
latences autorisées, à partir des latences interdites. Si p est le nombre de colonnes
de la table de réservation, et m la plus grande latence interdite, avec m < p, on
souhaite déterminer la plus petite latence autorisée a, dans le domaine 1 ≤ a < m.
L’ensemble décrivant les états autorisés et interdits peut être représenté sous la
forme d’un vecteur de collisions, qui est un vecteur binaire C = (cm cm−1 . . . c1 ) sur
m bits, où la valeur de ci est 1 si une latence de i provoque une collision et 0 si elle
est autorisée.
À partir du vecteur de collisions, il est possible de créer un diagramme d’états
spécifiant les transitions d’états autorisées entre exécutions successives dans le pipeline. Le vecteur de collisions C correspond à l’état initial du pipe-line au temps 1,
et est donc appelé vecteur initial de collision. Si a est une latence autorisée à partir
d’un état E, l’état obtenu à partir de E par lancement d’une exécution après une
latence de a cycles s’obtient en décalant le vecteur de collision de E de a bits sur
la droite, et en additionnant C au vecteur décalé. Quand la latence est supérieure
à m + 1, toutes les transitions sont redirigées vers l’état initial, et la transition est
notée « (m + 1)+ ». Ainsi, le diagramme d’état de la fonction X est représenté en
figure 3.18.
6+
1 1 0 1 0
1*
1 1 1 1 1
6+
3
6+
1 1 0 1 1
3*
Fig. 3.18 – Diagramme d’état de la fonction X.
À partir du diagramme d’états, il est possible de déterminer les cycles permettant
d’obtenir la MAL. Il existe une infinité de cycles obtensibles à partir d’un état donné
du diagramme d’états. Cependant, seuls les cycles simples, c’est-à-dire les cycles ne
passant qu’une fois au plus par un état donné, sont intéressants.
Certains cycles simples sont dits « gloutons ». Ce sont les cycles tels que les
arêtes empruntées pour sortir de chaque état traversé du cycle ont les plus petites
étiquettes possibles. Le cycle donnant la MAL est le cycle glouton dont la latence
moyenne est inférieure à celle de tous les autres cycles gloutons.
3.4.3
Dépendances
Dans les pipe-line d’instructions des processeurs, les dépendances entre instructions constituent la plus grande source de bulles, qui peuvent parfois être évitées par
une réorganisation du code, qui est réalisée par les compilateurs. À titre d’exemple,
Cours d’architectures et systèmes des calculateurs parallèles
27
3.4. PIPE-LINE
supposons que l’on veuille calculer l’expression de la figure 3.19, sous sa forme langage machine.
A = B * C + D * E;
F = G * H + I * J;
mul
mul
add
mul
mul
add
r1, [B],
r2, [D],
[A], r1,
r1, [G],
r2, [I],
[F], r1,
[C]
[E]
r2
[H]
[J]
r2
Fig. 3.19 – Fragment de langage machine (à droite) correspondant directement à
l’expression à calculer (à gauche).
Si les instructions sont séquencées dans cet ordre sur une machine pipe-linée à
cinq étages, le temps d’exécution est de t = 14 cycles, comme illustré en figure 3.20,
ce qui est deux fois plus rapide que sur une machine non pipe-linée, qui aurait
nécessité 6 × 5 = 30 cycles élémentaires.
1
2
F
D
R
F
D
R
F
D
3
E
F
4
5
6
Temps
W
E
W
1111
0000
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
0000
00001111
1111
0000
1111
R
E
W
D
R
E
F
D
R
F
D
W
E
W
1111
0000
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
00001111
1111
0000
R
E
W
Instructions
Fig. 3.20 – Exécution sur une machine pipe-linée à cinq étages du fragment de
langage machine de la figure 3.19.
Si l’on réordonne les instructions en fonction du pipe-line selon le schéma de la
figure 3.21, le nombre de cycles nécessaire tombe à 11, comme illustré en figure 3.22.
Cette diminution demande une augmentation du nombre de registres mis en œuvre,
ce qui est une caractéristique des architectures RISC.
mul
mul
mul
mul
add
add
r1, [B],
r2, [D],
r3, [G],
r4, [I],
[A], r1,
[F], r3,
[C]
[E]
[H]
[J]
r2
r4
Fig. 3.21 – Fragment de langage machine réordonné, sémantiquement équivalent à
celui de la figure 3.19
On distingue habituellement dans la littérature quatre types de dépendances,
illustrées en figure 3.23. Certaines sont réelles, et reflètent le schéma d’exécution ;
d’autres sont de fausses dépendances, qui résultent d’accidents dans la génération
du code ou du manque d’informations sur le schéma d’exécution1 . Deux instructions ont une dépendance réelle de données si le résultat de la première est un
1 Afin de ne pas induire le compilateur en erreur, il est donc souhaitable de ne pas réutiliser les
mêmes variables temporaires dans des blocs de code différents, mais au contraire de les déclarer,
autant de fois que nécessaire, dans le bloc de portée immédiatement supérieure à celle de leur
utilisation.
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
28
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
1
2
F
Temps
D
R
E
W
F
D
R
E
W
F
D
R
E
F
D
R
E
F
D
R
F
D
4
5
3
6
W
W
E
1111
0000
0000
1111
0000
1111
W
R
E
W
Instructions
Fig. 3.22 – Exécution sur une machine pipe-linée à cinq étages du fragment de
langage machine de la figure 3.21.
opérande de la seconde (on parlera alors de dépendance « Read-After-Write », ou
RAW). Deux instructions sont anti-dépendantes si la première utilise la valeur d’un
opérande qui est modifié par la deuxième (dépendance WAR). Deux instructions ont
une dépendance de résultat si toutes deux modifient le même opérande (dépendance
WAW). Enfin, il existe une dépendance de contrôle entre un branchement et une
instruction dont l’exécution est conditionnée par ce branchement.
mov
...
...
...
add
add
...
...
...
mov
r1, [A]
r3, r1, r2
r1, [A]
Anti-dépendance
Dépendance réelle
add
...
...
...
mov
r2, r1, r4
r1, r2, r3
bz
...
div
...
L:
r1, [A]
Dépendance de résultat
r4, L
r1, r1, r4
Dépendance de contrôle
Fig. 3.23 – Exemples des quatre types de dépendances.
Une source de dépendances spécifique aux processeurs CISC est le mot d’état
programme (« Program Status Word », ou PSW), registre global servant à stocker
les bits d’état fournis par l’ALU suite à la dernière opération arithmétique ou de
comparaison exécutée, et qui servent à orienter l’exécution des branchements conditionnels. Du fait des dépendances induites par la nécessité de préserver l’état des
bits du PSW entre une opération arithmétique et les branchements conditionnels
qui lui sont associés, il est impossible d’entrelacer deux calculs indépendants menant
à deux tests différents. Les registres de prédicats des processeurs LIW, présentés en
section 3.5.3, page 37, permettent de remédier à ce problème.
3.4.4
Branchements conditionnels
Une autre source très importante de bulles est l’existence des branchements,
qui sont principalement les branchements conditionnels, dus aux boucles : dans
le cas où le branchement est pris (« branch taken »), il faut vidanger le pipe-line,
Cours d’architectures et systèmes des calculateurs parallèles
29
3.4. PIPE-LINE
qui s’était automatiquement rempli avec les instructions situées directement après le
branchement. De nombreuses techniques ont été développées afin de réduire l’impact
des branchements sur le pipe-line d’instructions.
Déroulage de boucle
Le déroulage de boucle (« loop unrolling ») consiste en la recopie en plusieurs
exemplaires du corps de la boucle, qui permet de supprimer les branchements intermédiaires et par là même d’éviter la vidange du pipe-line d’instructions à chaque
tour de boucle.
Lorsque le nombre d’itérations initial n’est pas connu ou n’est pas un multiple
de la valeur de déroulage, une copie de la boucle originale est ajoutée avant ou
après la boucle déroulée, afin d’exécuter les itérations restantes, comme représenté
en figure 3.24.
i = ideb
i = ideb
i < ifin
f(i)
i ++
i < ifin-3
f(i)
f(i+1)
f(i+2)
f(i+3)
i += 4
Fig. 3.24 – Déroulage d’ordre 4 d’un corps de boucle à nombre d’itérations inconnu.
La partie en pointillés située en bas du schéma de droite est une copie de la boucle
originale représentée à gauche.
À titre d’exemple, la figure 3.25, page 31, présente un fragment de code machine Intel IA-32 correspondant au déroulage de la boucle d’un programme C de
sommation des valeurs d’un tableau. Le nombre d’itérations étant inconnu, le compilateur prend tous les cas possibles en compte. Il déroule la boucle quatre fois, et
fait précéder la portion déroulée d’un pré-traitement destiné à traiter les itérations
résiduelles, dont le nombre est compris entre zéro et trois. Ce code fonctionne de la
façon suivante :
– si le nombre d’itérations est inférieur ou égal à zéro, on évite la boucle ;
– si le nombre d’itérations, modulo 4, est 1, on va à la portion du code permettant de réaliser une itération, puis d’effectuer un test de terminaison, avant
d’entrer dans la boucle déroulée ;
– si le nombre d’itérations, modulo 4, est 2, on va à la portion du code permettant de réaliser une itération, puis de continuer avec le code précédent,
qui réalise la deuxième itération avant d’effectuer le test de terminaison et
d’entrer dans la boucle déroulée ;
– si le nombre d’itérations, modulo 4, est 3 (cas restant), on charge directement
la somme avec la première valeur du tableau (le compilateur a donc pris en
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
30
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
compte que la valeur de la somme était nulle avant le début de la boucle), et
on met le compteur à 1, avant d’entrer dans le code précédent, qui réalise les
deux itérations suivantes.
Lorsque le nombre d’itérations est connu à la compilation, le compilateur déroule
la boucle d’un ordre qui divise exactement le nombre d’itérations, ou bien fait
précéder le corps de la boucle déroulée d’un nombre d’instances du code égal au
modulo du nombre d’itérations par l’ordre de déroulage.
Le compilateur peut tirer parti du déroulage des boucles pour factoriser le code
déroulé et calculer des optimisations inter-itérations, au prix d’une augmentation de
la taille du code généré et du nombre de registres mis en œuvre. Cependant, cette
technique peut diminuer l’efficacité d’autres techniques d’optimisation, comme le
renommage dynamique de registres ou la prédiction de branchement, ce qui peut
parfois conduire à une diminution de la concurrence des programmes [28, page 24] ;
Prédiction de branchement
La prédiction de branchement (« branch prediction ») a pour but d’augmenter la
probabilité de suivre la bonne branche, et ainsi de ne pas rompre le flot du pipe-line.
En l’absence de tout mécanisme de prédiction, les études statistiques effectuées sur
de très nombreux codes indiquent que les meilleurs résultats sont obtenus lorsqu’on
suppose que tous les branchements sont pris. Cependant, cette technique n’est pas
efficace dans tous les cas.
Les techniques statiques utilisent uniquement l’information contenue dans le
code pour effectuer la prédiction. Cette information peut être un bit de l’instruction de branchement, qui indique si le branchement est considéré comme pris ou
non, et qui est positionné par le compilateur en fonction de son analyse du code (si
la probabilité estimée que le branchement soit pris est supérieure à 12 ou non). Elle
peut aussi être constituée de l’adresse du branchement elle-même : si l’adresse de
destination est inférieure à l’adresse courante (branchement remontant), le branchement est considéré comme pris, et si elle est supérieure (branchement descendant),
non. Ces postulats correspondent à la manière dont les compilateurs codent les
boucles, qui sont les plus grosses consommatrices de branchements conditionnels :
test de do...while dans le cas remontant, et test de sortie de for ou de while
dans le cas descendant.
Sur un ensemble représentatif de programmes, il a été montré [28, page 10] que
la prédiction statique suivant le signe du déplacement (remontant ou descendant)
donne le bon résultat dans 55% des cas, alors que supposer que le branchement
est toujours pris est efficace dans 63% des cas, et qu’une pré-détermination par
inspection du code à la compilation peut amener un taux de réussite moyen de
90%.
Un des problèmes de la prédiction statique de branchement est que le biais de
beaucoup de branchements conditionnels évolue dans le temps : la probabilité qu’ils
soient pris évolue au cours du temps, même dans le cas de données quelconques (ce
qui est d’ailleurs rarement le cas). Ainsi, pour le fragment de code suivant :
L1:
L2:
L3:
L4:
max = a[0];
for (i = 1; i < N; i ++)
if (a[i] > max)
max = a[i];
et en supposant que les a[i] sont aléatoires, la probabilité que (a[i] > max) pour
un i donné est la probabilité que a[i] soit plus grand que les i valeurs précédentes
Cours d’architectures et systèmes des calculateurs parallèles
31
3.4. PIPE-LINE
int
t[1000], n, s, i;
n = f ();
for (i = 0, s = 0; i < n; i ++)
s += t[i];
// f() renvoie 1000 mais le
// compilateur ne le sait pas
...
xorl %edx,%edx
movl %eax,%esi
movl %edx,%eax
cmpl %esi,%edx
jge .L45
movl %esi,%ecx
leal -4000(%ebp),%ebx
andl $3,%ecx
je .L47
cmpl $1,%ecx
jle .L59
cmpl $2,%ecx
jle .L60
movl -4000(%ebp),%edx
movl $1,%eax
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
addl (%ebx,%eax,4),%edx
incl %eax
// Fait un tour de boucle
// Incrémente le compteur
addl (%ebx,%eax,4),%edx
incl %eax
cmpl %esi,%eax
jge .L45
.align 4
//
//
//
//
//
//
//
Met la somme à zéro
Met le nombre dans esi
Met le compteur à zéro
Valeur de fin atteinte ?
Si oui, rien à faire
Copie le compteur dans ecx
Adresse du tableau dans ebx
Si compteur multiple de 4
Va à la boucle déroulée
Si valeur modulo 4 est 1
Fait un tour et déroule
Si valeur modulo 4 est 2
Fait deux tours et déroule
Charge la première valeur
Un tour fait, reste deux
.L60:
.L59:
.L47:
.L45:
addl (%ebx,%eax,4),%edx
addl 4(%ebx,%eax,4),%edx
addl 8(%ebx,%eax,4),%edx
addl 12(%ebx,%eax,4),%edx
addl $4,%eax
cmpl %esi,%eax
jl .L47
...
Fait un tour de boucle
Incrémente le compteur
Valeur de fin atteinte ?
Termine si c’est le cas
Alignement pour cache
Corps de boucle déroulée
Déroulage d’ordre 4
// Ajoute 4 au compteur
// Valeur de fin atteinte ?
// Reboucle si non atteinte
Fig. 3.25 – Exemple de déroulage de boucle à nombre d’itérations inconnu réalisé
par gcc-2.8.1 sur une architecture Intel IA-32. La boucle est déroulée quatre fois,
et est précédée d’un code destiné à traiter les itérations résiduelles, dont le nombre
est compris entre zéro et trois.
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
32
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
déjà rencontrées (partant de 0), c’est-à-dire la plus grande de i+1 valeurs aléatoires,
1
. La probabilité que le branchement du if soit pris diminue
et est donc égale à i+1
dans le temps. Les occurrences successives du branchement sont donc corrélées dans
le temps, et leur traitement efficace nécessite donc un mécanisme de prédiction utilisant un historique.
Les techniques dynamiques utilisent l’historique des décisions prises sur un ou
plusieurs branchements précédents pour prédire la décision suivante. Pour des raisons d’efficacité, seul entre en compte l’historique récent (les deux ou trois derniers
branchements).
Afin de prendre en compte l’historique des décisions précédentes, on peut conserver dans le cache d’instructions, dans le code de chaque instruction de branchement,
un bit d’historique qui est mis à jour lorsque la condition est réévaluée (ce bit peut
avoir été pré-initialisé par le compilateur, au cours d’une phase d’analyse du code ;
notez également que le segment de code reste en lecture seule). On dispose alors
d’un prédicteur dynamique à un bit d’historique.
De même, Lee et Smith [18] ont proposé d’utiliser un tampon de destination de
branchement (« Branch Target Buffer », ou BTB) pour réaliser une prédiction
de branchement. Ce dispositif est constitué comme un cache associatif adressé
par l’adresse précédant l’adresse du branchement (ce qui permet d’anticiper la
prédiction, et d’augmenter l’effet d’historique selon la manière dont on arrive à
l’instruction de branchement), et contenant les informations d’historique ainsi que
l’adresse de destination prédite. L’information contenue sera mise à jour en fonction
de la destination effective du branchement, lorsqu’elle sera connue. Le plus souvent,
la prédiction est effectuée en fonction d’un diagramme d’état, similaire à celui de la
figure 3.26, pour conserver l’historique des deux décisions précédentes.
P
P
PP
N
P
PN
Branchement
pris
NP
N
N
NN
P
N
Branchement
non pris
Fig. 3.26 – Diagramme d’état d’un prédicteur à deux bits.
Un cache de destination de branchement (« Branch Target Cache », ou BTC)
peut être associé au BTB, et a pour but de contenir les quelques instructions situées
à l’adresse de destination du branchement, afin de les charger rapidement dans le
pipe-line d’instructions.
L’avantage de disposer d’au moins deux bits de prédiction apparaı̂t clairement
pour les boucles. Si l’on considère le fragment de code suivant :
L1:
L2:
for (i = 0; i < 100; i ++) {
for (j = 0; j < 4; j ++) {
...
exécuté en régime stationnaire, alors avec un prédicteur à un bit, la prédiction de
L2 est fausse lors de la première itération de la boucle (on y reste, alors qu’on en
était sorti la dernière fois), et de la dernière (on en sort, alors qu’on avait bouclé
Cours d’architectures et systèmes des calculateurs parallèles
3.4. PIPE-LINE
33
les fois précédentes). Avec un prédicteur à deux bits, en régime stationnaire, on
considère toujours que la boucle est prise, puisqu’au moins deux itérations « pris »
mettent le prédicteur dans l’état « PP », et que l’itération de sortie ne le met que
dans l’état « PN ». Le branchement L2 n’est donc mal prédit que pour l’itération de
sortie. Le prédicteur à deux bits est donc bien plus efficace pour traiter les boucles
que le prédicteur à un bit, car il génère deux fois moins de mauvaises prédictions
(seulement en sortie de boucle, mais pas en entrée).
Cependant, ce type d’historique, local à chaque branchement, peut ne pas être
efficace. En effet, si l’on considère le fragment de code suivant :
L1:
L2:
if (cond1) action1;
if ((cond1) || (cond2)) action2;
et si l’on suppose que les deux conditions cond1 et cond2 sont aléatoires et non
corrélées entre elles, alors la probabilité que action1 soit exécutée est de 12 et la
probabilité que action2 soit exécutée est de 1 − 21 × 12 = 43 .
Avec un prédicteur local à un bit (mais les résultats pour L1 restent valables pour
un plus grand nombre de bits), la probabilité qu’un branchement soit bien prédit est
égale à la somme, pour chaque branche du test, de la probabilité que cette branche
soit prise multipliée par la probabilité que cette branche soit bien prédite qui est,
avec un prédicteur à un bit, la probabilité qu’elle ait également été prise la fois
précédente. C’est donc la somme des carrés des probabilités que chaque branche
2
2
soit prise. La probabilité que L1 soit bien prédite est donc de 12 + 12 = 21 , et la
2
2
probabilité que L2 soit bien prédite est de 34 + 41 = 85 . Avec un prédicteur de
type « toujours pris », la probabilité que L2 soit bien prédit est égale à la probabilité
que action2 soit exécutée, c’est-à-dire de 34 = 68 . Dans cet exemple, la probabilité
de bonne prédiction avec un prédicteur dynamique est inférieure à la probabilité
de bonne prédiction avec un prédicteur statique, ce qui peut sembler étrange. Ceci
est dû au fait que, L2 étant très fortement biaisé vers « pris », considérer qu’il sera
non-pris à nouveau est très pénalisant. Il faut donc rendre le prédicteur plus stable
par rapport aux cas exceptionnels, en utilisant par exemple un prédicteur à deux
bits au lieu d’un seul, mais le problème de tels branchements corrélés demeure.
À titre d’exemple, si l’on pouvait prédire L2 en fonction du résultat de cond1,
la probabilité que L2 soit pris si L1 est pris serait de 1, et la probabilité que L2 soit
pris si L1 n’est pas pris serait de 21 . De même, la probabilité que L2 soit bien prédit
si L1 est pris serait de 1, et la probabilité que L2 soit bien prédit si L1 est non-pris
2
2
serait de 21 + 12 = 21 . La probabilité que L2 soit bien prédit connaissant L1
serait donc de 12 × 1 + 21 × 12 = 43 . L’utilisation d’un historique global, prenant en
compte les résultats des autres branchements, peut donc être plus efficace que de
ne considérer que l’historique local.
Pour cela, on peut utiliser un prédicteur à deux niveaux [19]. Le premier est
constitué d’une fonction de hachage prenant comme arguments l’adresse du branchement et l’historique global, sur b bits, des décisions de branchement les plus
récentes, codé comme un registre à décalage de b bits (« 1 » pour « pris » et « 0 »
pour « non pris »). Le deuxième est constitué d’un tableau de prédicteurs à deux
bits indexé par cette fonction de hachage. Le schéma d’un tel mécanisme est donné
en figure 3.27.
Le prédicteur à historique global permet de traiter plus efficacement les branchements corrélés. Ainsi, dans l’exemple précédent, lorsque le branchement L1 est
pris, le registre à décalage global vaut xxx1 lors de la prédiction de L2, et adresse
des prédicteurs à deux bits différents de ceux utilisés lorsque le registre à décalage
vaut yyy0. Si l’on ne considère pas les risques d’interférences dus à la fonction de
hachage, la probabilité de bonne prédiction est donc celle calculée précédemment,
c’est-à-dire 34 .
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
34
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
Adresse du branchement
Fonction de hachage
Historique global
Table de prédicteurs 2 bits
Fig. 3.27 – Schéma d’un prédicteur à historique global à deux niveaux. L’adresse
du branchement et la chaı̂ne de bits de l’historique global courant servent d’index
dans une table de prédicteurs à deux bits.
Branchement retardé
Le problème fondamental en cas de mauvaise prédiction de branchement est que
le pipe-line est vidangé, ce qui crée autant de bulles qu’il existe d’étages en amont de
l’étage d’exécution. C’est cette contrainte qui incite les constructeurs à ne pas trop
augmenter le nombre d’étages des pipe-lines d’instructions. Cependant, si toutes
les instructions déjà présentes dans le pipe-line au moment du branchement étaient
des instructions exécutables dans tous les cas (branchement pris ou non), il serait
inutile de vidanger le pipe-line, et la pénalité de mauvaise prédiction serait annulée.
La technique du branchement retardé (« delayed branch ») permet de limiter
la purge du pipe-line d’instructions lors de la prise de branchements. Un branchement retardé de d cycles permet d’exécuter d instructions utiles après l’instruction
de branchement, que celui-ci soit pris ou non. Pour que cela soit possible, il faut
que ces instructions soient indépendantes du résultat du branchement ; dans le cas
contraire, des instructions NOP doivent être insérées à la place, comme illustré en
figure 3.28
load
dec
bz
div
mul
r1,
r2
r2,
r1,
r1,
[A]
pc+2
r1, r2
r1, r3
Avec d = 0.
dec
bz
load
nop
div
mul
dec
bz
load
div
mul
r2
r2,
r1,
r1,
r1,
pc+3
[A]
r1, r2
r1, r3
Avec d = 1.
r2
r2, pc+4
r1, [A]
r1, r1, r2
r1, r1, r3
Avec d = 2.
Fig. 3.28 – Remplissage ou non des emplacements de branchement retardé en fonction de l’indépendance relative des instructions.
Des études statistiques ont montré qu’autoriser des branchements retardés de
plus de trois cycles n’était pas souhaitable, du fait de la difficulté de trouver plus
de trois instructions indépendantes à déplacer en aval du branchement, et donc du
Cours d’architectures et systèmes des calculateurs parallèles
35
3.4. PIPE-LINE
nombre de NOP qui doivent être insérés et rendent le code inefficace. Dans les architectures actuelles, le nombre de cycles de branchement retardé est compris entre
un et deux ; il est par exemple de un pour les architectures SPARC, comme illustré
en figure 3.29.
int
i, r, s, t[100];
for (i = 1, s = 0, r = 1; i < 100; i ++) {
if (t[i] != 0)
s += i;
r *= i;
}
...
mov
mov
mov
add
ld
1, %o5
0, %o2
1, %o1
%fp, -412, %o4
[%o4], %o0
//
//
//
//
//
%o5
%o2
%o1
%o4
%o0
smul
cmp
be
add
add
%o1,
%o0,
.LL6
%o4,
%o2,
//
//
//
//
//
r *= i
Compare t[i] à 0
Si égaux, saute le "s += i"
Br.Retardé : index de i ++
s += i
add
cmp
ble,a
ld
...
%o5, 1, %o5
%o5, 99
.LL10
[%o4], %o0
//
//
//
//
i ++
Compare i à 99
Si inférieur ou égal, boucle
Br.Retardé : %o0 reçoit t[i]
stocke
stocke
stocke
stocke
reçoit
la valeur de i
la valeur de s
la valeur de r
l’index de i
t[1]
.LL10:
%o5, %o1
0
4, %o4
%o5, %o2
.LL6:
Fig. 3.29 – Mise en évidence du branchement retardé de 1 cycle des architectures
SuperSPARC, qui disposent en outre d’une instruction câblée de multiplication
(utiliser l’option de compilation -mcpu=v8 de gcc).
L’inconvénient du branchement retardé est que cette technique doit être intégrée
dès la phase de conception de l’architecture, et empêche toute évolution ultérieure,
puisque le code objet doit être réordonné statiquement en fonction du nombre de
branchements retardés mis en œuvre par l’architecture. Le branchement retardé
pénalise aussi la lecture du code assembleur, car il faut connaı̂tre le nombre de
branchements retardés pour l’interpréter correctement.
On peut remarquer que les mécanismes de prédiction de branchement et de
branchement retardé sont antagonistes : une prédiction de branchement parfaitement efficace rend inutile les branchements retardés, et réciproquement. Cependant,
comme il est impossible de prédire totalement les branchements, les architectures
récentes implémentent souvent au moins un niveau de branchement retardé, pour
amortir la pénalité des branchements mal prédits.
Exécution spéculative
L’exécution spéculative (« speculative execution ») utilise les performances des
architectures superscalaires pour exécuter concurremment les deux flots d’instrucc 2000, 2007, 2010 F. Pellegrini – ENSEIRB
36
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
tions associés à chacune des branches, dont les résultats sont temporairement stockés
dans des registres fantômes (« shadow registers »). Dès que la condition de branchement est effectivement évaluée, les registres fantômes correspondant à la branche
prise sont renommés pour devenir les registres visibles par l’utilisateur ; ceux correspondant à l’autre branche sont simplement libérés, pour resservir lors d’un prochain
branchement conditionnel.
L’exécution spéculative nécessite des mécanismes très évolués pour retarder les
effets de bord tels que les écritures en mémoire ou les exceptions tant que la branche
qui les génère n’est pas encore définitivement acceptée ou rejetée. En pratique, les
processeurs actuels n’autorisent qu’au plus quatre niveaux simultanés d’exécution
spéculative. En fait, il est plus économique d’implémenter des mécanismes efficaces de prédiction de branchement que d’augmenter la profondeur d’exécution
spéculative. Les deux mécanismes sont antagonistes, et disposer d’un mécanisme
parfaitement efficace pour réaliser l’une rend inutile la présence de l’autre [28, page
27]. Une approche proposée consiste à utiliser l’exécution spéculative si elle est possible, et ensuite la prédiction de branchement lorsque la profondeur maximale a été
atteinte.
3.5
3.5.1
Parallélisme d’instructions
Superscalarité
Les processeurs classiques, dits « scalaires » (par opposition aux processeurs
vectoriels, voir section 3.7), n’exécutent au plus qu’une instruction par cycle : en
l’absence de dépendances, une seule instruction est introduite à chaque cycle dans
le pipe-line d’instructions.
Les processeurs superscalaires, eux, disposent de plusieurs pipe-lines d’instructions qui leur permettent de traiter plusieurs instructions par cycle, et ainsi d’exploiter le parallélisme existant entre instructions consécutives. Le nombre de pipe-lines
d’instructions d’un processeur superscalaire est appelé « degré » du processeur.
Typiquement, sur un code sans déroulement de boucles, le nombre d’instructions consécutives sans dépendances est proche de deux. De fait, les processeurs
superscalaires actuels ont un degré compris entre trois et cinq.
Il existe différents modèles d’exécution superscalaires, que l’on peut classer suivant la façon dont l’allocation et le réordonnancement des instructions sont effectués.
On entend par allocation la technique qui permet d’affecter une instruction à une
unité de calcul, et par réordonnancement la technique qui permet d’exécuter une
instruction avant ou après une autre.
Ces deux techniques peuvent être traitées soit de façon statique au niveau du
compilateur (celui-ci devra alors, au moyen des techniques de déroulage de boucle,
d’entrelacement de code, et de prédiction de branchement, fournir un code dont
les instructions consécutives soient les plus indépendantes possible), soit de façon
dynamique au niveau du processeur.
Parmi les processeurs superscalaires à allocation dynamique, on trouve tous les
processeurs récents, tels le Pentium IV, l’Alpha 21164, le Power, etc. L’avantage
de l’allocation dynamique est que le code exécutable d’un tel processeur ne dépend
pas du nombre d’unités fonctionnelles qu’il possède, et est donc identique à celui
des architectures traditionnelles, garantissant la compatibilité au sein d’une même
famille, comme par exemple la famille Power d’IBM.
Cours d’architectures et systèmes des calculateurs parallèles
3.5. PARALLÉLISME D’INSTRUCTIONS
3.5.2
37
VLIW
Les processeurs VLIW (« Very Long Instruction Word ») sont les représentants
des processeurs superscalaires à allocation statique. Les instructions des processeurs
VLIW, de grande taille (jusqu’à 1024 bits), permettent de coder dans leurs différents
champs les opérandes de toutes les unités fonctionnelles du processeur, qui peuvent
toutes travailler en parallèle. Les programmes écrits au moyen d’instructions courtes
doivent être réarrangés pour former des instructions VLIW. Ceci doit être fait par
le compilateur, qui doit mettre en œuvre des stratégies très élaborées.
Les différences entre les processeurs VLIW et les processeurs à allocation dynamique sont que :
– la densité du code est presque toujours meilleure pour les architectures à allocation dynamique, de nombreuses unités fonctionnelles du processeur VLIW
étant inhibées en cas de dépendances2 ;
– le décodage des instructions VLIW est très simple, puisqu’il ne concerne que
les opérandes ;
– les processeurs VLIW n’ont pas besoin de circuiterie de gestion des dépendances et de reséquencement des instructions, puisque cette tâche est dévolue
au compilateur.
3.5.3
LIW
Actuellement, on s’oriente vers une voie hybride, avec des processeurs de type
LIW (« Long Instruction Word »). Ainsi, l’architecture IA-64 développée conjointement par Intel et HP, et mise en œuvre dans les processeurs Itanium d’Intel, est
basée sur une architecture LIW où une instruction longue (appelée « bundle », ou
« paquet ») de 64 bits code trois instructions courtes qui peuvent être exécutées
concurremment. Le Crusoe de Transmeta est un autre exemple de ce modèle. Ici encore, on supprime la circuiterie de réordonnancement et de gestion des dépendances,
à charge pour le compilateur de réarranger le code en paquets de trois instructions
courtes.
L’avantage des architectures LIW par rapport aux VLIW est qu’elles garantissent une compatibilité ascendante lorsque le nombre d’unités fonctionnelles augmente : si le nouveau processeur de la famille possède deux unités fonctionnelles
d’addition au lieu d’une seule, les instructions LIW du plus vieux processeur seront
cependant toujours légales sur le nouveau. En revanche, les instructions LIW du
nouveau processeur pourront contenir deux instructions d’addition par paquet, ce
qui n’est pas légal pour le vieux processeur.
L’ensemble des techniques permettant de mettre en œuvre le parallélisme explicite au niveau des instructions (VLIW et LIW), est désignée en anglais par l’acronyme EPIC, pour « Explicit Parallel Instruction Computing ».
L’architecture IA-64 met aussi en œuvre des mécanismes destinés à accélérer le
traitement des instructions en réduisant les ruptures de pipe-lines. En plus de ses 128
registres généraux de 64 bits et de ses 128 registres flottants de 82 bits, cette architecture dispose de 64 registres de prédicat à 1 bit, agencés en paires de telle sorte que
les registres P2i et P2i+1 contiennent toujours des valeurs opposées. Les valeurs de
ces registres peuvent être utilisées pour conditionner l’exécution des instructions localisées au sein de branches conditionnelles, ce qui permet de ne pas avoir à vidanger
l’ensemble du pipe-line en cas de mauvaise prédiction, comme illustré en figure 3.30.
Dans le cas d’instructions uniques, des formes d’instructions conditionnelles, comme
par exemple CMOVZ, permettent de n’effectuer des copies entre registres que quand la
2 En revanche, si l’on arrive à remplir suffisamment chaque instruction VLIW, l’absence des
opcodes associés à chaque instruction peut rendre le code plus compact qu’un code scalaire.
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
38
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
valeur d’un troisième registre est nulle ou non, ce qui évite d’avoir à positionner les
registres de prédicats et réduit encore plus le nombre d’instructions à exécuter. Cependant, pour traiter au plus vite ces instructions sans attendre le résultat du calcul
de la condition, il faut disposer de capacités importantes d’exécution spéculative,
dont la complexité limite les gains en performance [3].
Un avantage majeur des registres de prédicat est qu’ils suppriment les dépendances globales au registre PSW, puisque les résultats de tests de condition
indépendants peuvent être stockés dans des registres de prédicat différents. On
peut ainsi entrelacer non seulement les instructions conduisant au positionnement
des différents registres de prédicat, mais aussi celles des blocs conditionnels qui en
dépendent. On retrouve ainsi, à une échelle scalaire, les mécanismes de masquage
d’instructions des machines SIMD pour l’exécution des instructions conditionnelles
(voir page 12).
if (R1 == R2)
R3 = R4 + R5;
else
R6 = R4 - R5;
ET1:
ET2:
CMP
JNE
MOV
ADD
JMP
MOV
SUB
...
R1,R2
ET1
R3,R4
R3,R5
ET2
R6,R4
R6,R5
Sans instructions à prédicats.
Code IA-32.
<P4>
<P5>
CMPEQ R1,R2,P4
ADD
R3,R4,R5
SUB
R6,R4,R5
...
Avec instructions à prédicats.
Code IA-64.
Fig. 3.30 – Réduction de la taille et augmentation de l’efficacité du code machine
correspondant à des instructions conditionnelles, grâce aux instructions à prédicats.
3.6
Application à la programmation
Les techniques matérielles et logicielles d’optimisation décrites ci-dessus ont un
impact majeur sur la performance des programmes, selon que ceux-ci en tirent parti
ou non.
À titre d’exemple, considérons le problème suivant : on dispose d’un tableau
d’entiers, de très grande taille, ne contenant que des 0, des 1, et des 2, et l’on
souhaite connaı̂tre le nombre de 0, de 1, et de 2 contenus dans le tableau. Pour ce
faire, on peut écrire au moins trois programmes différents :
– le premier programme (P1), présenté en figure 3.31, est conçu pour tirer parti
de l’exécution spéculative : à chaque tour de boucle, un ou deux tests sont
effectués, qui conditionnent l’incrémentation de deux compteurs, la valeur du
troisième étant déduite à la fin par soustraction ;
– le deuxième programme (P2), présenté en figure 3.32, est basé sur l’indexation d’un tableau de compteurs, dont le contenu est incrémenté en fonction
des valeurs du tableau initial. Ce programme ne requiert aucun branchement
conditionnel au sein du corps de boucle, mais nécessite obligatoirement des
Cours d’architectures et systèmes des calculateurs parallèles
3.6. APPLICATION À LA PROGRAMMATION
39
c0 = c1 = 0;
for (i = 0; i < n; i ++) {
if (t[i] == 0)
c0 ++
else if (t[i] == 1)
c1 ++;
}
c2 = n - c0 - c1;
Fig. 3.31 – Programme de comptage des 0, 1, et 2 d’un tableau, basé sur l’exécution
spéculative.
accès mémoire (même si ils ne se feront à priori que dans le cache de premier niveau), puisque les cases à incrémenter ne peuvent pas être connues à
la compilation, et donc ne peuvent être affectées à des registres.
c[0] = c[1] = c[2] = 0;
for (i = 0; i < n; i ++)
c[t[i]] ++;
Fig. 3.32 – Programme de comptage des 0, 1, et 2 d’un tableau, basé sur l’indexation
d’un tableau de compteurs.
– le troisième programme (P3), présenté en figure 3.33, diffère du précédent en ce
que l’on remplace l’accès au tableau indexé de compteurs par une mise à jour
conjointe de deux compteurs par des valeurs obtenues par masquage binaire
des valeurs du tableau. Cette version ne contient aucun test interne, et peut
être compilée uniquement avec des registres, mais nécessite plus d’opérations
arithmétiques par tour de boucle.
c1 = c2 = 0;
for (i = 0; i
c1 += (t[i]
c2 += (t[i]
}
c2 >>= 1;
c0
= n - c1
< n; i ++) {
& 1);
& 2);
- c2;
Fig. 3.33 – Programme de comptage des 0, 1, et 2 d’un tableau, basé sur la mise à
jour conjointe de deux compteurs par des valeurs masquées.
Le comportement des deuxième et troisième programme ne dépend pas de la distribution des données contenues dans le tableau. En revanche, pour le premier,
l’historique de prédiction de branchement au sein du corps de boucle dépendra fortement des proportions relatives de 0, 1, et 2, ainsi que de leur placement dans le
tableau (de grandes plages de valeurs identiques seront préférables à une répartition
aléatoire des valeurs).
Le tableau 3.1 donne les temps d’exécution des trois versions sur de nombreuses
architectures3 , pour un tableau de données rempli soit de façon aléatoire, soit uniquement avec des zéros. Il n’est pas très pertinent de comparer quantitativement les
temps obtenus pour deux architectures différentes ; seule l’étude qualitative, ligne
par ligne, nous intéresse ici. On peut en dégager les renseignements suivants :
3 Ces résultats ont été fournis par Baptiste Malguy, ENSEIRB Info PRCD, promotion 2001, et
par Christophe Giaume, promotion 2003.
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
40
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
Processeur
Celeron 300A 450Mhz
Pentium III 700Mhz
Duron 700Mhz
Athlon XP 1.4Ghz
PowerPC 60Mhz
Power Whwk2+ 375Mhz 32b
Power Whwk2+ 375Mhz 64b
PowerPC G4 1.1 1.07Ghz 32b
U.SPARC IIe 400Mhz 4ML2 32b
U.SPARC IIe 400Mhz 4ML2 64b
U.SPARC II 450Mhz 4ML2 32b
U.SPARC II 450Mhz 4ML2 64b
U.SPARC IIe 500Mhz 256KL2 32b
U.SPARC IIe 500Mhz 256KL2 64b
Alpha EV67 667Mhz
HP PA-8700 750MHz
t[i] = { 0, 1, 2 }
P1
P2
P3
9860
6650
7160
9070
6660
7290
7860
5510
5830
2850
2030
2110
13550 12220 11810
5640
3960
2160
6840
3970
4150
4810
4130
3490
11650 9960
8710
13230 11710 11160
10330 8880
8050
11830 10540 9860
8030
7290
6370
9500
8660
7890
7678
6002
4555
4480
3540
3310
P1
5730
6090
5340
2000
12280
3480
4820
3650
9670
10450
8800
9340
6610
7300
4728
4370
t[i] = 0
P2
P3
6650
7170
6670
7280
5490
5830
1990
2110
12270 11810
6190
2190
6180
4150
4200
3360
10770 9170
11740 11140
9670
7940
10540 9920
8080
6360
8680
7870
6003
4553
4870
3320
Tab. 3.1 – Temps d’exécution des programmes présentés en figures 3.31, 3.32, et
3.33, mesurés sur différentes architectures, en mode 32 bits, pour un tableau rempli
de façon aléatoire ou constitué uniquement de zéros.
– quelle que soit la distribution des valeurs du tableau, la version P3 donne des
temps identiques sur toutes les architectures, et est la plus efficace sur les
architectures Power, Alpha, et HP PA-RISC, fortement superscalaires (pour
plus d’informations sur la superscalarité, voir la section 3.5.1) ;
– quelle que soit la distribution des valeurs du tableau, la version P2 donne elle
aussi des temps équivalents pour toutes les architectures, sauf sur le Power
WinterHawk2+, où les temps explosent lorsque le tableau n’est rempli qu’avec
des zéros. Ceci est extrêmement surprenant, vu le niveau « haut de gamme »
du processeur, et doit provenir de conflits d’accès au cache lors de demandes
de lectures-écritures multiples à la même adresse mémoire. Le HP PA semble
également sensible, bien que dans une moindre mesure, au même phénomène ;
– la version P2 est la plus performante sur les architectures Intel lorsque la
prédiction de branchement est inopérante, car celles-ci possèdent un cache
de premier niveau peu coûteux et ne sont pas très superscalaires, rendant la
version P3 moins efficace ;
– la version P1 est en revanche la plus efficace, sur les architectures Intel uniquement, lorsque la prédiction de branchement donne des résultats optimaux. Intel, limité en superscalarité par son jeu d’instructions CISC, a en revanche fait
des efforts importants pour disposer d’une unité d’exécution spéculative performante, qui réduit d’un tiers le temps d’exécution. Celle du HP PA semble
aussi très performante, puisque le temps de la version P1 dépend relativement
peu de la distribution des données.
Ces résultats montrent bien qu’il est important d’écrire des tests biaisés le plus possible, et que, sur les architectures fortement superscalaires, il est préférable de remplacer des tests potentiellement générateurs de ruptures de pipe-line par quelques
opérations supplémentaires. Un compendium de « hacks » relatifs au codage, sans
tests ou avec le moins d’instructions possibles, de la plupart des opérations classiques d’arithmétique entière, est disponible ici [2].
Cours d’architectures et systèmes des calculateurs parallèles
3.7. PROCESSEURS VECTORIELS
3.7
41
Processeurs vectoriels
De nombreux problèmes scientifiques sont intrinsèquement vectoriels, c’est-àdire qu’ils opèrent sur des vecteurs unidimensionnels de données. Pour les traiter efficacement, certaines architectures (dont les CRAY ont été les plus célèbres
représentants) disposent d’instructions vectorielles, qui s’appliquent à des tableaux
unidimensionnels de données de même nature (le plus souvent des nombres en virgule flottante), élément après élément.
Quand l’unité de contrôle décode et exécute une instruction vectorielle, le premier élément du (ou des) vecteur(s) impliqué(s) est soumis à l’unité de traitement
considérée. Après un certain nombre de cycles, le second élément est soumis, et ainsi
de suite, jusqu’à ce que toutes les opérandes du (ou des) vecteur(s) aient été traités.
Cette technique permet de remplacer une séquence d’instructions scalaires par
une instruction vectorielle qui ne sera décodée qu’une seule fois, mais surtout permet
d’utiliser à plein les pipe-lines des unités de traitement en virgule flottante (addition,
multiplication, inverse) auxquelles les instructions vectorielles sont le plus souvent
associées.
Afin d’accélérer encore plus les calculs, il est possible de chaı̂ner plusieurs opérations vectorielles entre elles (« pipeline chaining »). Le résultat d’une unité de
traitement pipe-linée est alors soumis en entrée d’une autre, sans attendre que la
première opération vectorielle ait terminé. Ainsi, le chaı̂nage de deux opérations
vectorielles ne coûte que le temps d’initialisation du pipe-line de la deuxième unité
de traitement, en plus du coût d’exécution de la première instruction vectorielle.
Ceci revient, du point de vue de l’efficacité, à augmenter la profondeur du pipe-line
de traitement.
À titre d’exemple, on peut étudier la manière dont on calcule l’inverse de nombres
flottants sur le CRAY 1. Cette machine ancienne (1976), dont une version simplifiée
de l’architecture fonctionnelle est présentée en figure 3.34, est basée sur une architecture vectorielle pipe-linée, avec un temps de cycle τ = 12, 5 ns, une profondeur
de pipe-line de 6 cycles pour l’addition, 7 cycles pour la multiplication, et 14 cycles
pour l’approximation réciproque (« reciprocal approximation », ou RA), qui permet
d’obtenir une approximation à deux bits près de l’inverse d’un nombre flottant. Le
surcoût d’utilisation des registres vectoriels est d’un cycle pour la lecture et d’un
cycle pour l’écriture, et le surcoût de chaı̂nage entre deux unités est de deux cycles.
Sur le CRAY 1, on calcule l’inverse x = a1 d’un nombre a par la méthode de Newton,
c’est-à-dire la recherche du zéro de la fonction f (x) = a − x−1 , avec f ′ (x) = x−2 ,
en itérant :
f (xn )
= (2 − a · xn )xn .
xn+1 = xn − ′
f (xn )
Comme la fonction RA donne une bonne approximation de a1 , une seule itération
de la méthode de Newton, appelée raffinement de Newton-Raphson, est nécessaire
pour déterminer les deux derniers bits de la mantisse de a1 , et donc
x = (2 − a · RA(a))RA(a) .
Si l’on effectue le calcul S6 = S1 / S2 au moyen des registres scalaires, selon la
1
séquence de la figure 3.35, on obtient une puissance de 29τ
≈ 2, 76 Mflop/s. Si
l’on réalise maintenant la division de façon vectorielle, avec des vecteurs de taille
l ≥ 9, selon la séquence de la figure 3.36, illustrée par la figure 3.37, on obtient une
l
puissance de (24+3
l)τ ≈ 23, 7 Mflop/s pour des vecteurs de taille l = 64. Remarquons
que, dans l’algorithme vectoriel, on a inversé l’ordre des deux instructions du milieu,
car sinon les deux instructions chaı̂nées accéderaient en même temps en lecture au
vecteur V2, ce qui n’est pas possible sur le CRAY 1.
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
42
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
P
Instruction pointer
(24 bits)
Branch
0 (16 bits)
1
Control
63
Instruction buffers
(4 * 64 * 16 bits = 256 instructions stack)
320 Mwords/s
4 words / clock period
1 Mword
(64 bits)
VL
Address registers
(24 bits)
Integer addition
Integer multiplication
Scalar registers
(64 bits)
Integer addition
Shift
Logical
Population count
Floating−point addition
Floating−point multiplication
Reciprocal approximation
A0
A1
A2
A3
A4
A5
A6
A7
S0
S1
S2
S3
S4
S5
S6
S7
VM
Vector length
(7 bits)
B0
B1
B2
B3
B4
B62
B63
Address buffer registers
Memory
cycle
50 ns
1 word / 2 clock periods
T0
T1
T2
T3
T4
80 Mwords/s
1 word /
clock period
16 banks
T62
T63
Scalar buffer registers
1 word / 2 clock periods
Vector mask
(64 bits)
Vector registers 0 V0
12
(64 bit elements)
1
3 4
2
5 6
7
3
4
Integer addition
Shift
Logical
62
63
Fig. 3.34 – Schéma simplifié de l’architecture fonctionnelle du CRAY 1. Extrait
de [11, page 73].
Dans le cas de la multiplication vectorielle V3 = V1 * V2, le temps de calcul
pour un vecteur de taille l est 1 + 7 + 1 + (l − 1) = 8 + l, ce qui donne une puissance
de 70 Mflop/s pour des vecteurs de taille l = 64.
Il est à noter que la limitation d’accès à la mémoire a fait l’objet d’améliorations
très précoces. Dès la génération X-MP, les CRAY se sont vus dotés de trois pipe-line
d’accès à la mémoire : deux pour la lecture, et un pour l’écriture. Ainsi, l’exécution
de la fonction BLAS-1 SAXPY, qui réalise l’opération Y = aX + Y sur deux vecteurs X et Y , avec a scalaire [17], s’effectue-t-elle au moyen de trois chaı̂nes sur le
CRAY 1, mais seulement avec une sur le CRAY X-MP, comme illustré en figure 3.38.
La vectorisation automatique a été un sujet de recherche très actif au cours de la
dernière décennie, qui a permis d’offrir aux utilisateurs des compilateurs vectoriseurs
efficaces. Ceux-ci utilisent plusieurs techniques :
– le déroulage de boucles (« loop unrolling »), pour transformer les opérations
scalaires de plusieurs itérations en opérations vectorielles plus efficaces ;
– la segmentation de tableaux (« strip-mining »), pour convertir des opérations
logiques sur des vecteurs de grandes tailles en instructions opérant sur les
Cours d’architectures et systèmes des calculateurs parallèles
43
3.8. CO-PROCESSEURS FAIBLEMENT VECTORIELS
Instruction
= RA(S2)
= (2 - S3 * S2)
= S1 * S3
= S4 * S5
S3
S4
S5
S6
Unité
RA
mul ( !)
mul
mul
Début
0
14
15
22
Fin
14
21
22
29
Pipe-line
Fig. 3.35 – Séquencement des instructions scalaires nécessaires au calcul de S6 =
S1 /S2.
V3
V5
V4
V6
Instruction
= RA(V2)
= V1 * V3
= (2 - V3 * V2)
= V4 * V5
Unité
RA
mul
mul
mul
Début
0
1 + 14 + 1
17 + (l - 1)
17 + 1 + 2 (l - 1)
Fin
16 + (l
25 + (l
26 + 2 (l
27 + 3 (l
-
1)
1)
1)
1)
Chaı̂nage
Pipe-line
Pipe-line
Fig. 3.36 – Séquencement des instructions vectorielles nécessaires au calcul de V6
= V1 /V2.
Chaînage
0
10
Pipe−line
20
Pipe−line
30
40
50
Temps
57
RA
MUL
MUL
MUL
Éléments
Fig. 3.37 – Séquencement des calculs vectoriels de la figure 3.36.
registres vectoriels de la machine, qui sont de taille fixe ;
– la transformation des boucles (« loop transformation »), qui permet de modifier l’ordre dans lequel l’espace des itérations d’un nid de boucle est parcouru
afin de maximiser la localité des données dans les boucles les plus internes,
comme illustré en figure 3.39.
3.8
Co-processeurs faiblement vectoriels
Alors que la fréquence CPU plafonne et que le nombre d’instructions indépendantes pouvant être extraites du flot d’instructions limite fortement les performances induites par la superscalarité, la recherche de performances croissantes sur
les processeurs scalaires impose de solliciter l’utilisateur pour identifier des portions
de code contenant le plus grand nombre d’instructions indépendantes, et de disposer
d’instructions matérielles spécifiques pour réaliser ces opérations en parallèle.
Les traitements actuellement les plus coûteux sur les processeurs généralistes
étant les algorithmes de traitement d’images 2D et 3D, qui sont de plus très réguliers,
des co-processeurs spécifiques ont été développés pour traiter par blocs et en parallèle plusieurs éléments. On se retrouve alors avec des sous-systèmes SIMD de petite taille, c’est-à-dire opérant sur de petits vecteurs de données (« small vectors »),
car devant être interfacés avec la hiérarchie mémoire des processeurs scalaires, qui
est orientée « lignes » et non pas « vecteurs ».
Ainsi, l’unité Altivec des processeurs PowerPC (technologie appelée « Velocity
Engine » ou « VEX » chez d’autres vendeurs) dispose de 32 registres spécialisés
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
44
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
Mémoire
R/W
V2
Mémoire
Mémoire
R/W
R1
R2
V1
S1
V1
*S
V2
V2
S1
V3
*S
+V
V3
V4
+V
V4
V4
W
R/W
Mémoire
Mémoire
Fig. 3.38 – Chaı̂nages des instructions vectorielles nécessaires à la réalisation de
l’opération BLAS-1 SAXPY, sur le CRAY 1 et sur le CRAY X-MP.
de 128 bits, qui peuvent être considérés chacun comme un vecteur de 128 bits, de
16 octets, de 8 entiers courts sur 16 bits, de 8 pixels sur 16 bits, de 4 entiers sur
32 bits, de 4 pixels sur 32 bits, ou encore de 4 nombres à virgule flottante de type
float conformes à la norme IEEE-754 [7, section 4.2]. Sur ces registres peuvent
être appliquées en parallèle des opérations entières :
– d’addition/soustraction entre éléments, avec saturation ou non en cas de
débordement ;
– de multiplication avec stockage dans des registres de destination différents des
parties hautes et basses, et éventuellement avec accumulation avec les valeurs
précédentes contenues dans ces registres ;
– de décalage et de rotation bit à bit d’éléments ;
– de comparaison entre éléments de vecteurs, avec résultats partiels et résultat
global, et stockage éventuel des minimums et maximums,
des opérations flottantes :
– d’addition/soustraction et de multiplication entre éléments ;
– de conversion de types et d’arrondi ;
– d’approximations réciproques pour la division et
√l’extraction de racines carrées
(pour cette dernière, on calcule le raffinement a = y + 21 y(1 − ay 2 ), où y est
Cours d’architectures et systèmes des calculateurs parallèles
45
3.9. PARALLÉLISME DE TÂCHES
10
20
DO 20 I = 2, N
DO 10 J = 2, I
A(I, J) = A(I, J - 1)
*
+ A(I - 1, J)
CONTINUE
CONTINUE
10
20
DO 20 J = 2, N
DO 10 I = 2, J
A(I, J) = A(I, J - 1)
*
+ A(I - 1, J)
CONTINUE
CONTINUE
Fig. 3.39 – Exemple de transformation de boucles. L’échange des boucles en I et J
permet d’accéder au tableau A par colonnes, qui est l’ordre naturel en FORTRAN,
et donc de le charger en mémoire par des instructions vectorielles.
√
l’approximation réciproque de a),
et des opérations de chargement, de sauvegarde, de compactage, et de décalage des
éléments des vecteurs à partir d’autres registres ou de la mémoire.
3.9
Parallélisme de tâches
Comme on l’a vu précédemment, le gain en performance apporté par le parallélisme au niveau des instructions ne peut dépasser un facteur 3 en moyenne, du
fait de la séquentialité intrinsèque des flots d’instructions. Les seuls gains possibles
de performance en terme de parallélisme ne peuvent donc plus concerner un flot
unique d’instructions, générant du parallélisme à grain fin, mais provenir du traitement concurrent de flots d’instructions distincts, correspondant à un parallélisme à
grain moyen de type multi-threads.
Dans ce cadre, c’est au programmeur d’identifier dans son application des flots
d’exécution distincts et concurrents, qu’il modélisera sous forme de threads, à charge
pour le processeur d’exécuter de façon concurrente ces flots de la façon globalement
la plus efficace possible, même si l’exécution individuelle de chaque flot n’est pas
améliorée.
3.9.1
Hyperthreading
L’hyperthreading est la technique la moins coûteuse en terme de transistors
pour gérer des threads multiples au niveau du matériel. Elle consiste à entrelacer
plusieurs flots d’exécution, en pratique typiquement deux, en alimentant les unités
d’exécution du processeur avec des instructions provenant à tour de rôle de chacun
des flots.
L’intérêt de cette technique est d’amortir les problèmes de dépendances entre instructions, qui génèrent des bulles dans le pipe-line d’exécution, en laissant à chaque
instruction d’un flot plus de temps pour s’exécuter puisque l’instruction suivante
d’un flot donné ne sera considérée qu’après avoir traité dans l’intervalle les instructions provenant des autres flots. Le seul surcoût massif en terme de transistors
concerne la duplication des registres, pour que chaque flot d’exécution puisse disposer de ses propres registres. Les circuits de gestion des interruptions sont également
dupliqués, afin que les erreurs d’exécution survenant au niveau d’un flot n’impactent
pas l’exécution des autres flots.
3.9.2
Processeurs multi-cœurs
Dans le cas des processeurs multi-cœurs, on dispose d’unités de traitement
complètement séparées pour exécuter les différents flots d’instructions. En revanche,
les différents cœurs partagent leurs caches de deuxième niveau.
On trouve actuellement des processeurs quadri-cœurs, mais certains prototypes
disposent déjà de 80 cœurs [14], avec une structure hiérarchique d’accès à la mémoire,
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
46
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
de petits groupes de cœurs partageant leurs caches de deuxième niveau, ces groupes
accédant à un cache de troisième niveau commun.
De fait, tant dans le cas de l’hyperthreading que des processeurs multi-cœurs, le
goulot d’étranglement, amplifié par le nombre de flots d’instructions exécutables en
parallèle, reste le bus mémoire commun reliant le processeur à la mémoire, et des
performances élevées ne pourront être obtenues qu’avec des applications partageant
une fraction significative de leur code et de leurs données. Nul ne sait vraiment, à
l’heure actuelle, quelles applications cela pourrait concerner.
3.9.3
Accélérateurs
L’augmentation continue de la densité d’intégration rend de plus en plus critique le problème de la dissipation thermique. Pour le résoudre, les constructeurs
mettent actuellement en place des mécanismes permettant de couper l’alimentation
des unités fonctionnelles non utilisées, mais cette technique, si elle peut être utile
sur une machine de bureau ou un ordinateur portable, pour augmenter l’autonomie,
ne concerne pas les machines hautes performances, dont on attend un rendement
maximal, et donc l’utilisation simultanée du plus d’unités fonctionnelles possible.
Afin de pouvoir traiter des problèmes de très grande taille, la plupart des simulations scientifiques sont basées sur des structures de données irrégulières et creuses,
et les programmes qui les manipulent ne peuvent s’exécuter de façon efficace sur des
architectures purement vectorielles. Cependant, il est généralement possible d’exhiber au sein de leurs algorithmes, éventuellement au prix d’un remplissage partiel des
structures de données, des noyaux de calcul denses (multiplications matrice-vecteur
ou matrice-matrice) pouvant être traités efficacement par un sous-ensemble de routines optimisées, telles que les BLAS (« Basic Linear Algebra Subprograms ») [5, 17].
Sur les processeurs superscalaires, le codage de ces routines est basé sur les
effets pipe-line et le pré-chargement des lignes de cache. Sur les architectures hyperthreadées ou multi-cœurs, il est possible de déléguer à plusieurs threads des calculs
portant sur des sous-ensembles disjoints de données, mais se pose alors le problème
de l’efficacité énergétique : pourquoi devoir alimenter en énergie chacune des logiques
de contrôle dédiées à l’exécution des différents flots d’instructions, comprenant les
coûteux mécanismes de prédiction de branchement et d’exécution spéculative, alors
que les instructions sont simples et régulières et portent sur des flots de données
consécutives. On retrouve ainsi, de façon enfouie, les besoins ayant conduit par le
passé à la construction des machines SIMD et vectorielles.
Face à ce constat, les constructeurs peuvent donc légitimement hésiter entre la
multiplication du nombre de cœurs complexes, ou bien l’adjonction à un cœur complexe d’unités d’exécution plus simples, dotées de capacités vectorielles. C’est ainsi
qu’IBM, qui domine le marché des processeurs multi-cœurs hautes performances
avec sa gamme Power [13, 26], expérimente avec son architecture Cell [12] une architecture hybride comprenant un cœur complexe pilotant huit unités fonctionnelles
vectorielles.
C’est également pour cela que les principaux fournisseurs de pipe-lines vectoriels
de calcul, à savoir les constructeurs de cartes graphiques, cherchent actuellement,
à travers le « GPU computing », à élargir leur marché en proposant des serveurs
de calcul basés sur leurs moteurs de rendu graphique [1, 20]. Cependant, cette
approche n’est pas exempte de problèmes. Le premier est que les bus graphiques,
comme par exemple l’AGP, sont très fortement asymétriques : si l’envoi de données
depuis la carte mère vers la carte graphique s’effectue avec un débit important,
indispensable pour charger les masses de données nécessaires au rendu graphique
Cours d’architectures et systèmes des calculateurs parallèles
3.10. ÉVALUATION DES PERFORMANCES DES PROCESSEURS
47
(polygones, textures), la redescente de données en sens inverse n’a pas été conçue
pour supporter les mêmes débits, pénalisant fortement les applications nécessitant
des débits symétriques. De plus, la consommation de ces dispositifs est loin d’être
négligeable, parfois supérieure à celle d’un processeur classique.
Un autre problème inhérent à ces accélérateurs est la non-portabilité des programmes, qui doivent être écrits pour faire appel à des bibliothèques spécifiques à
chaque vendeur. On retrouve ainsi, à une plus petite échelle, les problèmes induits
par les extensions vectorielles différentes qu’Intel et AMD avaient ajouté au jeu
d’instruction IA-32. Afin que cette solution soit commercialement viable, des efforts
de standardisation sont donc à réaliser.
3.10
Évaluation des performances des processeurs
La fréquence d’horloge est un paramètre déterminant de la puissance des processeurs. Cependant, de nombreux autres critères architecturaux doivent être pris
en compte, sans parler de l’environnement du processeur (la hiérarchie mémoire, en
particulier). De fait, l’augmentation de la fréquence d’horloge n’est significative par
elle même qu’au sein d’une famille de processeurs donnée.
Pour comparer les performances de deux machines différentes (architecture, type
de jeu d’instructions, etc.), il faut décomposer le temps total d’exécution des programmes en leurs constituants. Le temps mis pour exécuter un programme donné
est le produit du nombre de cycles nécessaires par le temps de cycle :
T = cτ .
Le nombre de cycles peut être quant à lui réécrit comme le nombre d’instructions
exécutées multiplié par le nombre moyen de cycles par instructions :
c
τ .
T =i
i
Le nombre d’instructions dépend de facteurs logiciels (algorithme choisi, compilateur), mais aussi du type de jeu d’instructions utilisé (CISC ou RISC). Le fait
d’avoir un jeu d’instructions plus complexe n’accélère pas forcément l’exécution,
car ces instructions nécessitent plus de cycles pour s’exécuter.
Le nombre moyen de cycles par instruction ne mesure pas seulement la complexité du jeu d’instructions, et dépend également de critères architecturaux : superscalarité, pipe-lines, etc.
Le temps de cycle du processeur dépend de la technologie et des matériaux
utilisés, mais aussi de l’architecture du processeur. Un jeu d’instructions petit et
une circuiterie simple nécessitent une surface de silicium moins importante, d’où un
temps de parcours de l’information plus petit.
La mesure effective des performances des machines s’effectue en mesurant le
temps d’exécution de programmes de complexité connue : calcul matriciel (LinPACK 100×100 ou 1000×1000), etc. Cette méthode seule permet de prendre en
compte l’intégralité des phénomènes mis en jeu, tant du point de vue matériel que
logiciel. Elle n’est cependant valide que pour une application donnée (si celle-ci est
plutôt vectorielle ou superscalaire, on pourra avoir des performances très variables
sur des architectures différentes).
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
48
CHAPITRE 3. ARCHITECTURE DES PROCESSEURS
Cours d’architectures et systèmes des calculateurs parallèles
Chapitre 4
Architecture des mémoires
4.1
Hiérarchie mémoire
L’efficacité des processeurs dépend très fortement du temps d’accès aux informations stockées en mémoire. Cependant, pour des raisons techniques (vitesse de
la lumière) autant que financières, il n’est pas possible de réaliser une mémoire de
grande capacité ayant un temps d’accès compatible avec les fréquences de cadencement des processeurs actuels (4 GHz, soit 250 ps de latence).
Dans tout programme, il est possible de mettre en évidence un phénomène de
localité des accès mémoire, exprimé en termes de :
– localité temporelle : plus une zone mémoire a été accédée récemment, et plus
sa probabilité de ré-accès est élevée ;
– localité spatiale : plus une zone mémoire est proche de la dernière zone
mémoire accédée, et plus la probabilité qu’elle soit à son tour accédée est
importante.
Ceci est vrai :
– pour les instructions. C’est le cas du déroulement normal d’un programme
séquentiel sans branchements, dont on tire également parti dans les pipe-lines
d’instructions ;
– pour les données. C’est le cas lors des mises à jour de variables, de l’accès à
des données structurées, du parcours séquentiel de tableaux, etc.
On s’appuie sur ce principe pour mettre en place une hiérarchie de la mémoire,
entre mémoires rapides de faible capacité et mémoires de grande capacité aux temps
d’accès plus longs, afin que les informations les plus fréquemment utilisées soient
disponibles le plus rapidement possible ; cette structure pyramidale est illustrée en
figure 4.1.
Le transfert des informations entre zones lentes et zones rapides s’effectue soit de
façon logicielle (registres, zones cache utilisateur, va-et-vient (« swap ») disque),
soit de façon matérielle (cache).
4.2
Registres
Les registres sont des mémoires très rapides (temps d’accès de l’ordre des cent
pico-secondes), situés le plus souvent sur le processeur lui-même.
Afin de mettre en œuvre efficacement les techniques pipe-line et superscalaires,
et de réduire le nombre d’accès à la mémoire, les processeurs actuels possèdent de
plus en plus de registres. Les architectures les plus courantes en ont de 32 à 192,
mais on trouve des processeurs en ayant jusqu’à 2048. Cependant, le plus souvent,
49
50
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
Logiciel
Registres
Matériel
Vitesse
Cache(s)
Capacité
Coût/octet
Mémoire centrale
Mémoire de masse
Fig. 4.1 – Hiérarchie mémoire. Les mécanismes de remontée de l’information sont
soit logiciels, soit matériels.
tous ne sont pas simultanément accessibles à l’utilisateur.
Pour éviter que les changements de contexte liés aux appels de fonctions ne
génèrent des accès mémoire coûteux, certains processeurs disposent d’un mécanisme
de « fenêtres de registres », introduit dans le processeur RISC I de l’Université de
Berkeley en 1972, et repris par les processeurs SPARC. Ceux-ci disposent de 32
registres visibles pour exécuter les programmes. Huit sont des registres globaux,
communs à tous les contextes, et les 24 autres sont des registres fenêtrés associés à
chaque procédure, comme illustré en figure 4.2.
Fenêtre
précédente
r[31]
r[24]
r[23]
In
r[16]
r[15]
Local
Out
r[8]
r[31]
Fenêtre
courante
r[24]
r[23]
In
r[16]
r[15]
Local
r[8]
Out
r[31]
Fenêtre
suivante
r[24]
In
r[7]
r[0]
Global
Fig. 4.2 – Fenêtres de registres du processeur SPARC.
Chaque fenêtre est divisée en trois sections :
– les Ins : paramètres passés par la procédure appelante ;
– les Locals : accessibles seulement à la procédure courante ;
– les Outs : paramètres passés aux procédures appelées, pouvant également servir de variables locales.
Les 136 registres du processeur SPARC sont donc organisés en huit fenêtres
glissantes de 24 registres chacune, auxquelles il faut ajouter les huit registres globaux
(comprenant les pointeurs d’instructions et de pile), comme illustré en figure 4.3.
Un indicateur de fenêtre courante et des bits d’invalidité permettent de déterminer
quelles fenêtres sont utilisables ou nécessitent une sauvegarde de contexte.
En théorie, on peut parcourir sept niveaux de récursion sans effectuer d’accès
mémoire, ce qui permet un gain considérable en efficacité pour les programmes
fortement récursifs. En pratique, l’intégralité de la fenêtre de registres doit être
sauvegardée entre chaque changement de contexte de processus, ce qui est très
pénalisant dans un environnement multi-processus en temps partagé.
Cette idée a néanmoins été reprise et étendue dans l’architecture Itanium, qui
possède 128 registres visibles, organisés en 32 registres globaux, numérotés de 0 à
31, visibles de tous les contextes, et 96 registres empilés, numérotés de 32 à 127.
Chaque fonction déclare lors de la création de son contexte le nombre de registres In
qu’elle reconnaı̂t, ainsi que le nombre total de registres disponibles dont elle a besoin
Cours d’architectures et systèmes des calculateurs parallèles
51
4.3. MÉMOIRE CACHE
L7
I7
O7
I0
L0
O6
I1
L6
O0
O5 I6
L1
I2 O1
L5
O4
I5
L2
L4
O2
I4
O3
I3
L3
Global
Fig. 4.3 – Recouvrement des fenêtres de registres glissantes du processeur SPARC.
(In, Local et Out), nécessairement inférieur ou égal à 96. Un mécanisme matériel
spécifique, appelé Register Stack Engine, est chargé de sauvegarder (« spill ») dans
la pile mémoire, appelée zone de backing store, le contenu des registres physiques
des contextes appelants les plus anciens et devant servir comme registres locaux du
contexte courant, et à les restaurer (« fill ») lorsqu’on retournera à ces contextes,
donnant ainsi l’illusion d’une pile de registres de taille infinie. Une étude menée
par des ingénieurs d’Intel a montré que le mécanisme de pile de registres pouvait
conduire à un gain de performance de l’ordre de 10% par rapport à un processeur
n’en disposant pas [25].
4.3
Mémoire cache
La mémoire cache est une mémoire rapide faisant tampon entre le processeur et
la mémoire centrale. Selon leur localisation, on distingue :
– les caches internes, situés sur le processeur, d’une vitesse presque équivalente
à celle des registres, et de taille comprise entre 1 et 64 ko ;
– les caches externes, extérieurs au processeur, plus lents mais de capacité plus
importante, pouvant aller jusqu’à 1 Mo.
Ces deux types de cache peuvent coexister au sein de la même architecture ; le cache
interne est alors appelé cache de premier niveau, et le cache externe, cache de second
niveau. Dans certaines architectures, on trouve même un troisième niveau de cache.
L’existence de plusieurs niveaux de cache découle de la nécessité d’équilibrer la
charge des accès mémoire au sein de la hiérarchie, en fonction des différents niveaux
de localité présents au sein des algorithmes : localité forte pour les éléments de
structure et les nids de boucles, localité moyenne pour les segments de tableaux et
les fonctions de bibliothèques, localité faible pour les tableaux entiers et les modules.
4.3.1
Mécanismes d’accès
Quand le processeur souhaite lire une donnée à partir de la mémoire, il génère
l’adresse correspondante, et émet une requête sur le bus, qui est interceptée par le
cache. Si la donnée est présente dans le cache (on parle alors de « cache hit »), elle
est directement envoyée au processeur. Sinon, en cas de défaut de cache (« cache
miss »), la requête est transmise à la mémoire centrale. Lorsque la donnée est fournie
par la mémoire, une copie est conservée dans le cache (en cas d’accès futur), qui
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
52
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
doit libérer la place nécessaire à son stockage.
Afin de tirer parti du phénomène de localité, les transferts entre le cache et la
mémoire s’effectuent par blocs (ou « lignes », « lines »). Pour des raisons d’efficacité
liées au temps de transfert ainsi qu’à la limitation de la place dans le cache, la taille
de ces blocs est cependant limitée à quelques octets, entre 16 et 64 en pratique. Une
taille plus importante augmenterait en effet la probabilité de charger des données
non utilisées par la suite, et donc de consommer inutilement la bande passante
mémoire, pénalisant les accès ultérieurs.
Les lignes de cache sont chargées sur demande. Lorsqu’une donnée à lire n’est
pas présente dans le cache, celui-ci demande à la mémoire de lui transmettre la
ligne à laquelle appartient la donnée demandée, et libère l’espace nécessaire à son
stockage.
Plusieurs optimisations permettent d’accélérer les lectures à partir de la mémoire.
Habituellement, en l’absence de cache, les mots sont lus individuellement à partir
de la mémoire : l’adresse du mot à lire est placée sur le bus, et la mémoire renvoie
alors son contenu, au bout d’un certain temps. Pour optimiser le chargement des
lignes de cache à partir de la mémoire (opération dite de « cache line fill »), on
peut effectuer une lecture en mode « rafale » (« burst »). Dans ce cas, on place sur
le bus l’adresse du premier mot de la ligne à charger, les mots de la ligne étant alors
envoyés par la mémoire les uns après les autres. Par ce moyen, on diminue grandement le temps de chargement d’une ligne de cache, puisqu’on ne spécifie qu’une
seule fois l’adresse de lecture, au lieu de le faire une fois par mot de la ligne. De
plus, lorsque le cache charge une ligne, il retourne le mot demandé au processeur
dès qu’il le reçoit, sans attendre la fin du chargement complet de la ligne.
Quand le processeur souhaite écrire une donnée en mémoire, trois techniques
peuvent être utilisées par le cache pour réaliser cette opération :
– « write through » : toute opération d’écriture demandée par le processeur
provoque l’écriture effective de la donnée en mémoire, même en cas de « cache
hit ». Si la donnée était déjà présente dans le cache, celui-ci est également mis
à jour.
Faute d’optimisations, cette technique reviendrait à supprimer le cache lors
des opérations d’écriture, et ralentirait celles-ci de façon catastrophique. Pour
éviter cela, les caches de ce type disposent de tampons d’écriture (« fast write
buffers »), qui permettent le traitement asynchrone des opérations d’écriture,
en évitant au processeur d’attendre leur réalisation effective. Le problème ne
resurgit que lorsque les tampons sont pleins.
Le grand avantage de la technique « write through » est qu’elle rend les lignes
de cache immédiatement disponibles pour leur réallocation ;
– « write back » : toute opération d’écriture demandée par le processeur provoque la mise à jour du cache, mais la ligne modifiée n’est recopiée en mémoire
que lorsqu’elle doit faire place à de nouvelles données dont le processeur a
besoin.
La technique « write back » ralentit le remplacement de lignes de cache, du fait
des écritures à réaliser avant le chargement des nouvelles lignes, mais ceci est
en général compensé par la suppression des (multiples) opérations d’écriture
en mémoire qui ont ainsi été évitées ;
– « write allocate » : dans le cas où la donnée à écrire n’est pas déjà présente
dans le cache, cette technique consiste à allouer la ligne de cache correspondante, en la chargeant à partir de la mémoire une fois que l’écriture a été
prise en compte par celle-ci. Comme la donnée a effectivement été écrite, le
processeur peut immédiatement poursuivre son traitement, pendant que le
chargement de la ligne s’effectue de façon asynchrone.
La technique « write allocate » n’est vraiment utile que pour les caches de
Cours d’architectures et systèmes des calculateurs parallèles
53
4.3. MÉMOIRE CACHE
type « write back ». De fait, lors du chargement de la nouvelle ligne de cache,
on aura souvent d’abord à écrire une ligne de cache modifiée avant de charger
la nouvelle ligne à sa place.
À cause de cette complexité, la plupart des caches n’implémentent pas de
stratégie « write allocate » ; les opérations d’écriture provoquant des défauts
de cache sont simplement répercutées vers la mémoire centrale, et ignorées
par le cache.
Aucune des deux stratégies, « write back » ou « write through », ne l’emporte
clairement sur l’autre : leurs performances relatives dépendent de la structure des
accès mémoire réalisés par les programmes.
4.3.2
Structure
Tout cache nécessite, en plus de la zone mémoire réservée aux données (servant
au stockage des lignes), des informations de contrôle servant d’index de recherche
(ou de répertoire, « directory ») dans le cache. On distingue quatre types principaux
d’organisation des données dans les caches, présentés ci-dessous.
La correspondance directe (« direct mapping »)
Chaque donnée de la mémoire a une place précalculée unique dans le cache.
Ainsi, avec un cache de capacité 2s+l , les données situées aux adresses a, a + 2s+l ,
a + 2 · 2s+l , a + 3 · 2s+l seront-elles stockées à la même place de la même ligne du
cache, comme illustré en figure 4.4.
Étiquette
Ensemble Déplacement
Comparateur
hit/miss
donnée
Fig. 4.4 – Structure d’un cache à correspondance directe.
Cette organisation est extrêmement simple et rapide, car elle ne requiert qu’un
unique comparateur pour tester si l’étiquette de la ligne de cache correspond bien
à la partie haute de l’adresse fournie ; si c’est le cas, on a un « cache hit », sinon
un « cache miss ». L’inconvénient majeur de ces caches est que leurs performances
dépendent fortement de l’alignement des structures de données qu’ils cachent. Dans
le cas d’une copie élément par élément entre deux tableaux dont les adresses de
début diffèrent d’un multiple de 2s+l , par exemple, on aura deux défauts de cache
par élément.
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
54
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
La k-associativité par ensemble (« k-way set associativity »)
Le cache est subdivisé en ensembles (« sets ») contenant plusieurs lignes (de
deux à seize), et disposant d’informations de contrôle propres servant à la gestion
individuelle de ces lignes. Chaque donnée de la mémoire a un unique ensemble
destination, mais dans cet ensemble sa position est totalement libre. Un cache kassociatif nécessite donc k comparateurs. Cette structure est illustrée en figure 4.5,
pour un cache 2-associatif. Le choix de la ligne à remplacer lorsqu’une nouvelle
ligne doit être chargée dans un ensemble s’effectue au moyen d’une politique LRU
(« Least Recently Used »).
Étiquette
Ensemble Déplacement
Comparateurs
hit/miss
donnée
Fig. 4.5 – Structure d’un cache 2-associatif.
L’associativité totale (« full associativity »)
Le cache est constitué d’un unique ensemble, et donc une donnée peut être
placée dans n’importe quelle ligne. Ce type de cache offre les meilleurs taux de
réussite (« hit ratio »), mais est le plus coûteux à implémenter, puisqu’il nécessite
un comparateur par ligne.
La correspondance par secteurs (« sector mapping »)
Le cache est subdivisé en secteurs (« sectors ») contenant plusieurs lignes (de
2 à 32) qui disposent chacune d’un bit de validité. La recherche d’une donnée dans
le cache s’effectue par comparaison associative totale entre son étiquette de secteur
(« sector frame ») et toutes les étiquettes de secteur du cache, puis par indexation
directe à l’intérieur du secteur choisi. Cette structure est illustrée en figure 4.6.
Cette architecture revient à augmenter la granularité du cache, tout en conservant une taille de données en lecture/écriture égale à celle d’une ligne. Elle est moins
coûteuse que l’associativité totale, en ce que les comparaisons s’effectuent sur un
nombre plus restreint d’étiquettes. Elle convient bien aux machines dédiées au calcul numérique, car les boucles réalisées dans les algorithmes de calcul scientifique
Cours d’architectures et systèmes des calculateurs parallèles
55
4.3. MÉMOIRE CACHE
Étiquette
Ligne
Déplacement
Comparateurs
hit/miss
donnée
Fig. 4.6 – Structure d’un cache sectoriel.
opérant sur des structures de données denses n’accèdent en général qu’à quelques
zones de grande taille à la fois.
4.3.3
Adressage
Les adresses mémoire soumises aux caches peuvent être soit les adresses logiques
fournies par le processeur, soit les adresses physiques résultant de la traduction des
adresses logiques par la MMU (« Memory Management Unit »).
Lorsque le cache utilise l’adressage physique, l’accès au cache s’effectue après
que l’adresse logique émise par le processeur a été traduite en adresse physique par
la MMU, ce qui ralentit les accès mémoire.
Lorsque le cache utilise l’adressage logique, les accès au cache s’effectuent parallèlement à la traduction de l’adresse logique en adresse physique, ce qui permet
d’accélérer le traitement des accès mémoire. En revanche, cela génère un problème de
cohérence pour les systèmes multi-processus, puisqu’alors la même adresse logique,
utilisée par des processus différents, doit correspondre à des données différentes.
Pour remédier à cela, une solution simple consiste à invalider l’ensemble des lignes
du cache lors des changements de contextes entre processus. Cette solution est cependant extrêmement coûteuse, surtout dans le cas des caches « write back », qui
nécessitent l’écriture en mémoire de toutes leurs lignes modifiées. Une autre solution
consiste à associer aux lignes du cache, en plus de leur étiquette, un identificateur
de processus. Cette solution est viable, mais induit un surcoût mémoire qui peut
être important.
4.3.4
Cohérence
L’existence de caches internes aux processeurs rend la réalisation de machines
multi-processeurs à mémoire partagée plus délicate, du fait des incohérences pouvant
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
56
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
exister entre les valeurs d’une même référence mémoire contenues dans les caches
de processeurs différents, comme illustré en figure 4.7.
Processeur
x
Cache
x
x
x
x
x
y
x
y
x
Bus
Mémoire
x/y
Fig. 4.7 – Mise en évidence d’incohérences potentielles entre caches locaux et
mémoire commune sur une machine bi-processeur. Lorsque le premier processeur
demande la valeur d’une case mémoire, cette valeur est conservée dans son cache
local. Il en est de même lorsque le deuxième processeur effectue la même requête. Si
le premier processeur modifie la valeur, la modification peut être ou non répercutée
en mémoire selon le type de cache (« write through » ou « write back »), mais lors
d’accès ultérieurs, le deuxième processeur verra toujours l’ancienne valeur contenue
dans son cache local.
L’incohérence entre caches est uniquement causée par les écritures. Elle se produit lorsqu’une opération d’écriture, qui modifie la valeur de la référence mémoire
contenue dans le cache du processeur effectuant l’écriture (et également la mémoire
elle-même, dans le cas d’un cache « write-through »), n’est pas répercutée sur les
autres caches possédant l’ancienne valeur.
Deux politiques sont envisageables pour maintenir la cohérence entre caches :
– l’invalidation sur écriture (« write-invalidate ») : la mise à jour effectuée sur
un cache provoque l’invalidation (réinitialisation du bit de validité) de toutes
les copies de la ligne possédées par les autres caches ; les lignes invalidées sont
dites « périmées » (« dirty »).
Ainsi, lorsqu’un autre processeur demandera à lire cette référence mémoire,
la valeur transmise sera lue à partir de la mémoire centrale, et non à partir
d’une ligne de cache périmée.
De fait, cette politique n’est envisageable que pour les caches de type « writethrough », qui assurent en permanence la cohérence entre l’état de la mémoire
et des caches.
– la mise à jour sur écriture (« write-update ») : la mise à jour effectuée sur un
cache est également effectuée sur tous les autres caches possédant la référence
mémoire.
La prise en compte par tous les caches des opérations d’écriture effectuées par
l’un d’entre eux nécessite dans tous les cas une circuiterie supplémentaire.
Lorsque les caches sont de type « write through », un protocole d’espionnage
du bus (« snooping protocol ») permet de tracer toutes les opérations d’écriture
réalisées par les autres caches, et éventuellement de lire à la volée la nouvelle valeur
des références mémoires afin de répercuter localement la mise à jour. Cependant,
dans un environnement multi-processeurs, l’utilisation systématique du bus par les
caches « write-through » fait de celui-ci un goulet d’étranglement.
Pour remédier à cela, tout en assurant la cohérence des caches dans un environnement multi-processeurs, a été développé un protocole de gestion de caches dit
« à écriture unique » (« write-once ») [8], dont le représentant le plus connu est
Cours d’architectures et systèmes des calculateurs parallèles
4.3. MÉMOIRE CACHE
57
le protocole MESI (« Modified, Exclusive, Shared, Invalid »). Selon ce protocole,
chaque ligne de cache peut prendre quatre états distincts :
– « invalid » : la ligne de cache ne contient pas de données valides ;
– « shared » : la ligne de cache contient des données à jour, qui n’ont pas été
modifiées depuis leur chargement dans le cache. D’autres processeurs peuvent
également posséder des copies de ces données dans leurs propres caches ;
– « exclusive » : les données de la ligne de cache n’ont été modifiées localement
qu’une seule fois depuis leur chargement dans le cache (elles étaient alors en
mode « shared »), et la modification a été répercutée en mémoire centrale,
selon le principe « write-through ». Aucun autre cache ne possède de copie
valide de la ligne (d’où le nom) ;
– « modified » : les données de la ligne de cache ont été modifiées localement
plusieurs fois depuis leur chargement dans le cache, mais les modifications
successives n’ont pas été répercutées en mémoire centrale, selon le principe
« write-back ». On ne peut arriver à cet état qu’à partir de l’état « exclusive ».
Ici encore, aucun autre cache ne possède de copie valide.
Lorsqu’une nouvelle ligne est chargée dans le cache à partir de la mémoire, son état
est positionné à « shared ». D’autres caches peuvent également charger les mêmes
données, qui seront également étiquetées localement « shared ».
Lorsqu’une ligne « shared » est modifiée localement pour la première fois, son
état passe à « exclusive », et la modification est répercutée à travers le bus vers
la mémoire centrale, selon le principe « write-through ». Ainsi, par l’espionnage du
bus, tous les caches possédant une copie « shared » de la ligne l’invalideront.
Lorsqu’une ligne « exclusive » est modifiée localement pour la première fois, son
état passe à « modified », et la modification n’est pas répercutée vers la mémoire
centrale, de même que les suivantes. D’après ce qui précède, aucun autre cache ne
possède de copie valide de la ligne, puisque le passage précédent de la ligne en mode
« exclusive » les a toutes invalidées. Cependant, la mémoire centrale n’est plus à
jour, et tout processeur redemandant cette ligne chargera des données périmées.
Pour éviter cela, le cache possédant une copie en mode « modified » d’une ligne
demandée par un autre cache doit intercepter la requête, et placer lui-même la
nouvelle valeur de la ligne sur le bus, en prenant le pas sur la mémoire centrale ; cette
notion de préemptivité du bus est implémentée dans tous les bus récents (Multibus
et Futurebus). Dans le même temps, le cache propriétaire écrira la ligne en question
en mémoire centrale, et remettra l’état de sa ligne à « shared », puisqu’une autre
copie existe sur un autre cache.
L’idée des caches « write-once » est donc de remplacer la mise à jour systématique de la mémoire, génératrice d’engorgements, par une mise à jour à la
demande, les caches propriétaires des lignes modifiées se chargeant alors de les
fournir aux caches demandeurs. Un grand avantage de ce protocole est qu’il permet
d’associer librement sur le même bus mémoire des unités de traitement disposant de
caches (comme les processeurs) et d’autres n’en ayant pas (comme les périphériques
d’entrées/sorties) ; c’est en fait la préemptivité du bus qui permet de les interfacer
sans circuiterie supplémentaire.
4.3.5
Hiérarchies de caches
Le besoin de performance accrue en terme de débit mémoire des processeurs a
conduit les concepteurs de caches à réaliser des caches à triple niveau. Ainsi, sur
l’architecture Itanium2, on trouve :
– un cache de premier niveau constitué d’un cache d’instructions de 16 Ko et
d’un cache de données write-through de 16 Ko également, capable de servir
quatre requêtes en lecture par cycle, ou deux requêtes en lecture et deux en
écriture, structuré en lignes de 64 octets ;
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
58
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
– un cache de deuxième niveau unifié de 256 Ko, 8-way associative write-back,
structuré en 16 bancs de lignes de 128 octets, et capable de servir quatre
requêtes par cycle, avec une latence d’au moins 6 cycles. Le cache est nonbloquant, capable de gérer simultanément, grâce à une file spécifique appelée L2OzQ, jusqu’à 32 requêtes provenant du cache de premier niveau,
de réordonner les requêtes en fonction des conflits de bancs et des identités
d’adresses sur les Load et les Store, et de gérer jusqu’à 16 requêtes simultanées
vers le cache de troisième niveau ;
– un cache de troisième niveau unifié de 1.5 Mo, 12-way associative, avec une
latence d’au moins 12 cycles.
La complexité des mécanismes mis en œuvre dans ces hiérarchies de cache, qui
peuvent interférer entre eux et induire des pertes de performance considérables (de
plus de la moitié) selon les positions relatives en mémoire des flots de données manipulés, nécessitent une analyse approfondie afin de réaliser les noyaux de calcul les
plus efficaces possible [15].
Comme en moyenne, sur les processeurs Itanium de première génération, il a
été mesuré que le traitement des cache miss de données représentait plus de la
moitié du temps d’exécution des programmes, l’architecture Itanium2 permet de
spécifier, au niveau des instructions Load , la localisation probable de la donnée
dans la hiérarchie de cache (pour adapter la latence de chargement) ainsi que le
niveau de cache dans lequel il sera préférable de conserver la donnée une fois qu’elle
aura été accédée (qui permet aux caches de niveaux inférieurs de marquer la donnée
comme pouvant être remplacée de préférence à des données plus utiles). Ces indices
de gestion des caches (« cache hints ») sont positionnés par le compilateur après
analyse statique du code à la compilation, ou bien par le programmeur en langage
machine souhaitant réaliser des noyaux de calcul efficaces.
4.4
4.4.1
Mémoire centrale
Structure
Sur la plupart des machines, la vitesse de la mémoire centrale n’est pas suffisante
à elle seule pour alimenter le processeur selon ses besoins ; plusieurs cycles doivent
s’écouler entre le moment où une donnée est demandée et celui où elle est disponible
sur le bus.
Afin d’améliorer le débit de la mémoire, on organise celle-ci en bancs entrelacés (« interleaved banks ») indépendants, dont chacun gère une partie de l’espace
d’adressage. Typiquement, si l’on dispose de N bancs mémoire numérotés de 0 à
N − 1, le banc i est affecté aux adresses de la forme bN + i, ce qui permet un accès
concurrent à des adresses consécutives de la mémoire.
Pour que la mémoire puisse fournir le débit demandé par le processeur, il faut
que le nombre de bancs de mémoire soit au moins égal au nombre de cycles de
latence de celle-ci. Sur le CRAY 1 (1976), qui avait un temps de cycle τ de 12, 5
ns et une latence mémoire de 50 ns, soit 4 cycles, la mémoire était divisée en 16
bancs indépendants, permettant ainsi un débit entre le processeur et la mémoire de
4 mots par cycle, comme illustré en figure 4.8.
Dans le cas d’architectures disposant d’instructions mémoire/mémoire, et comme
certaines instructions agissent sur trois opérandes, il faut disposer d’un débit mémoire
effectif entre processeur et mémoire de trois mots par cycle, et garantir ce débit pour
chaque processeur dans le cas d’architectures multi-processeurs. Pour cela, certaines
architectures disposent de chemins d’accès multiples. Ainsi, sur le CRAY Y-MP, qui
Cours d’architectures et systèmes des calculateurs parallèles
59
4.4. MÉMOIRE CENTRALE
0
4
8
12
1
5
9
13
2
6
10
14
A 4 -A19
3
7
11
15
320 Mm/s instructions
A 3 -A 2
A 1 -A 0
80 Mm/s données
Fig. 4.8 – Schéma de câblage des bancs mémoire du CRAY 1. Ce schéma est à
rapprocher de celui de l’architecture fonctionnelle générale du CRAY 1, présentée
en figure 3.34, page 42.
avait 8 processeurs de latence τ égale à 6 ns, et nécessitait donc un débit de 4 Gm/s,
la mémoire était divisée en 128 bancs, accessibles par trois chemins séparés (deux
en lecture et un en écriture) pour chaque processeur, ce qui permettait d’atteindre
les 4 Gm/s si aucun conflit n’intervenait.
4.4.2
Optimisation des accès mémoire
Il y a conflit d’accès si deux opérations sont demandées au même banc dans un
intervalle inférieur au temps de latence de la mémoire. Par exemple, si une mémoire
de 16 bancs a une latence de 4 cycles, un conflit se produira si au moins deux accès
sur quatre consécutifs diffèrent d’un multiple de 16, comme c’est le cas pour un
incrément multiple de 8 (un conflit tous les deux accès), voire de 16 (un conflit par
accès). Le tableau 4.1 donne la performance en Mflop/s du calcul terme à terme
d’un produit scalaire entre deux vecteurs dont les termes utiles sont espacés d’un
incrément donné, en fonction de la valeur de cet incrément.
La performance de la mémoire dépend donc assez fortement de l’alignement
des données, qu’il est souhaitable de modifier en conséquence. Ainsi, dans le cas
d’un programme de diffusion sur une grille périodique de taille 128 × 128, tel que
celui présenté en figure 4.9, on allouera les tableaux TAB1 et TAB2 comme des grilles
(130, 128) plutôt que (128, 128), pour optimiser le schéma d’accès à la mémoire,
chaque cellule et ses quatre voisines étant alors situées sur des bancs mémoire tous
différents, comme illustré en figure 4.10.
4.4.3
Optimisation des accès TLB
Sur les architectures modernes, un mécanisme de pagination permet de disposer d’une mémoire virtuelle de taille supérieure à celle de la mémoire physique.
Il nécessite, lors de chaque accès mémoire, une traduction entre numéro de page
virtuelle et numéro de page physique, qui est effectuée à la volée par un disposic 2000, 2007, 2010 F. Pellegrini – ENSEIRB
60
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
Incrément
Bancs
1
2
3
4
5
6
7
8
9
16
Cray X-MP/48
64
86
67
72
52
75
71
76
63
68
73
NEC SX-2
512
255
244
244
211
244
244
244
160
244
103
Fujitsu VP-200
128
365
127
228
67
227
127
228
67
225
67
Tab. 4.1 – Performance, en Mflop/s, du calcul terme à terme d’un produit scalaire
entre deux vecteurs dont les termes utiles sont espacés d’un incrément donné. Le
Cray X-MP/48 [4] avait un temps de cycle de 8.5 ns (117 MHz), le NEX SX-2 de
6.25 ns (160 MHz), et le Fujitsu VP200 de 7.5 ns (133 MHz).
10
20
DO 20 I = 1, 128
DO 10 J = 1, 128
IA = (I + 127) MOD 128
IP = (I + 1)
MOD 128
JA = (J + 127) MOD 128
JP = (J + 1)
MOD 128
TAB2 (I, J) = (TAB1(IA, J) + TAB1(I, JA) +
*
TAB1(I, JP) + TAB1(IP,J)) / 4
CONTINUE
CONTINUE
Fig. 4.9 – Boucle principale d’un programme résolvant l’équation de la chaleur sur
un réseau torique carré.
tif annexe au processeur et appelé MMU, pour « Memory Management Unit ».
La correspondance entre numéros de pages virtuelles et physiques s’effectue au
moyen d’une table (« mapping table ») qui, pour des raisons d’encombrement
mémoire, est structurée de façon hiérarchique, afin que seules les portions utilisées soient effectivement allouées et stockées en mémoire physique. Du fait de
cette hiérarchisation, et en l’absence d’optimisations, chaque accès à une adresse
mémoire virtuelle nécessiterait, pour la conversion de celle-ci en adresse physique,
plusieurs accès mémoire supplémentaires, ce qui conduirait à un écroulement des
performances du système. Afin d’accélérer le processus de traduction, les MMU
disposent d’un mécanisme appelé TLB (pour « Translation Lookaside Buffer »),
qui est en fait un cache totalement associatif, de petite taille (entre 128 et 1024
entrées au plus), indexé par les numéros de pages virtuelles et mémorisant les correspondances les plus récemment effectuées. Ce n’est que lorsque le numéro de page
virtuelle demandé n’est pas trouvé dans le TLB que les tables de pages doivent être
consultées.
Les pénalités induites par les défauts de TLB peuvent en fait être bien plus
coûteuses que les défauts de cache, de par le nombre d’accès mémoire à effectuer et
du fait que les données en question, étant peu accédées, sont souvent absentes des
caches L2 et à plus forte raison L1. C’est pour cela que certaines implémentations de
routines de calcul intensif, comme les BLAS, ont été optimisées non pour minimiser
les défauts de cache mais les défauts de TLB [9].
Cours d’architectures et systèmes des calculateurs parallèles
61
4.5. DISQUES
128
128
i−1
i−1
i
128
i
i−2 i i+2
i
i+1
130
i+1
1111111111111111111111
0000000000000000000000
0000000000000000000000
1111111111111111111111
Fig. 4.10 – Motifs des accès aux données du tableau TAB1 effectués par la boucle du
fragment de code présenté en figure 4.9, pour une machine disposant d’un nombre
de bancs inférieur ou égal à 128, et selon que le tableau est déclaré avec 128 ou 130
lignes. En Fortran, le stockage des données se fait par colonnes.
4.5
4.5.1
Disques
Gestion des accès
Lorsqu’on manipule de très gros volumes de données, celles-ci ne peuvent tenir entièrement en mémoire centrale. Lorsque cela est techniquement réalisable
(quand chaque processeur possède son propre disque local, ou que l’on ne risque
pas d’écrouler le réseau d’interconnexion), il est alors possible d’utiliser des disques
comme espace de stockage temporaire. Cette fonctionnalité peut être gérée à deux
niveaux :
– au niveau du système (matériel et noyau) : ce sont les mécanismes classiques
de mémoire virtuelle et de « va-et-vient » (« swapping »). Ces mécanismes
automatiques évitent de modifier l’algorithme, mais il est souvent possible
d’exhiber des cas pathologiques d’écroulement résultant d’interférences entre
l’algorithme de calcul et l’algorithme de gestion du va-et-vient ;
– au niveau de l’application elle-même (logiciel) : le chargement et la sauvegarde explicites des ensembles de données sont spécifiés par le programmeur, en
fonction de l’algorithme, qui est alors dit « out-of-core ». Cette approche est
la plus efficace, mais elle est coûteuse en temps de développement et demande
une connaissance approfondie des paramètres du système (temps moyen des
accès disques, taille des tampons système, etc.).
4.5.2
Organisation des données
L’organisation des données et leurs schémas d’accès peuvent avoir des conséquences extrêmement importantes sur les temps d’exécution des programmes. À titre
d’exemple, considérons un algorithme calculant le produit matriciel C = AB + C
sur des matrices carrées de taille 1024×1024 rangées par colonne (style FORTRAN),
sur une architecture disposant d’une mémoire centrale de 16 pages de 65536 valeurs
chacune (chaque page pouvant ainsi stocker 64 colonnes de matrice).
En écrivant l’algorithme de produit matriciel de façon classique, comme décrit
en figure 4.11, chaque lecture d’une ligne de A provoque 16 défauts de page, d’où
plus de 16 millions de défauts de page au total.
Avec une approche par blocs colonne, où l’on partitionne la matrice A en blocs de
64 colonnes et B en blocs de 64×64 valeurs, comme présenté en figure 4.12, chaque
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
62
10
20
30
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
DO 30 I = 1, 1024
DO 20 J = 1, 1024
DO 10 K = 1, 1024
C(I, J) = C(I, J) + A(I, K) * B(K, J)
CONTINUE
CONTINUE
CONTINUE
Fig. 4.11 – Écriture classique de l’algorithme de calcul d’un produit de matrices.
parcours de J génère 16 défauts de page, et la boucle KK en génère elle-même 16,
d’où 16 × (16 + 1) = 272 défauts de page au total.
10
20
30
40
DO 40 KK = 1, 1024, 64
DO 30 J = 1, 1024
DO 20 I = 1, 1024
DO 10 K = KK, KK + 63
C(I, J) = C(I, J) + A(I, K) * B(K, J)
CONTINUE
CONTINUE
CONTINUE
CONTINUE
Fig. 4.12 – Écriture classique de l’algorithme de calcul d’un produit de matrices.
Avec une approche purement par blocs, où l’on partitionne les matrices en blocs
de 256 × 256 valeurs, on descend jusqu’à 96 défauts de page au total !
Le principal problème des disques provient de la nature mécanique des accès :
les vitesses de rotation et les contraintes thermiques sont en effet telles qu’un
réalignement de la tête de lecture est nécessaire entre chaque accès, même si les
blocs à lire sont situés sur la même piste, ce qui limite actuellement les débits aux
environs de 20 Mo/s. Le « disk striping », qui répartit les fichiers sur plusieurs
disques, permet le transfert des données en parallèle sur plusieurs unités, au moyen
de protocoles évolués tels que IPI3 (« Intelligent Parallel Interface, version 3 ») ou
HiPPI (« High Performance Parallel Interface »), qui permettent d’atteindre des
débits de 100 Mo/s.
Dans le futur, les techniques holographiques, encore confidentielles, permettront
des temps d’accès de l’ordre de 1 à 10 µs et des taux de transfert de 100 Mo à
1 Go/s. En revanche, le stockage ne peut être que de courte durée, ce qui limitera l’utilisation de ces techniques aux caches des unités centrales et aux unités de
stockage temporaires.
4.5.3
Baies de disques (RAID)
Les baies de disques (« disk arrays ») sont une solution efficace. Elles sont
constituées d’un grand nombre de disques peu chers, accédés en parallèle, et disposant de protocoles évolués dits RAID (« Redundant Arrays of Inexpensive Disks ») [22]
permettant la correction d’erreurs et la reprise à chaud.
L’émergence de la technologie RAID tient au fait que, si la capacité unitaire des
disques hautes performances (« Single Large Expensive Disk », ou SLED) a crû en
rapport avec l’augmentation des puissances de calcul et des tailles des mémoires
centrales, le temps de positionnement des bras n’a diminué que d’un facteur deux
de 1971 à 1981.
Cours d’architectures et systèmes des calculateurs parallèles
63
4.5. DISQUES
Caractéristique
Capacité (Mo)
Prix par Mo
MTTF annoncé (h)
MTTF effectif
Nombre de têtes
Débit (Mo/s)
Puissance (W)
IBM 3380
7500
$18-$10
30000
100000
4
3
6600
Conners CP3100
100
$10-$7
30000
?
1
1
10
Ratio
75,0
1,00-2,50
1,00
?
4,00
3,00
660
Tab. 4.2 – Caractéristiques des disques IBM 3380 modèle AK4 et Conners CP3100.
La table 4.2 compare quelques paramètres significatifs d’un disque SLED IBM 3380
modèle AK4 et d’un disque de PC Conners CP3100, disponibles en 1987 lors de la
publication de l’article définissant le RAID.
Ces caractéristiques ont permis d’imaginer la définition de systèmes de stockage
constitués d’un grand nombre de disques peu chers et de petite capacité, gérés soit
de manière entrelacée pour absorber les gros volumes produits par les supercalculateurs, soit de manière indépendante pour traiter les nombreux petits transferts
générés par les applications transactionnelles.
Le problème majeur des systèmes RAID est la tolérance aux pannes. En effet, le
MTTF (« Mean Time To Failure ») d’un système composé de plusieurs disques est
inversement proportionnel au nombre de ces disques. Ainsi, un système RAID de
100 disques Conners CP3100 disposerait d’un MTTF annoncé de 300 heures, soit
moins de deux semaines !
Pour remédier à cela, il faut mettre en place des mécanismes autorisant le
système à fonctionner malgré la panne d’au moins un disque, et permettant la
réparation « à chaud ». Du point de vue du stockage, ceci nécessite l’utilisation de
disques supplémentaires pour dupliquer l’information, afin de palier la panne d’un
disque et de reconstruire l’information manquante lors de son remplacement par un
disque neuf vierge. On organise donc les D disques de données en groupes de G
disques, à chacun desquels sont adjoints C disques de contrôle. Si on définit MTTR
(« Mean Time to Repair ») comme le temps moyen de maintenance, on obtient la
formule :
2
MTTFDisk
.
MTTFRAID =
D 1+ C
G (G + C − 1)MTTR
Plusieurs niveaux d’organisation RAID ont été définis, qui offrent chacun des
niveaux de sécurité et de performance différents. Pour évaluer cette dernière, on
distinguera les « grosses » entrées/sorties générées par les supercalculateurs, qui
mobilisent au moins un secteur de chaque disque d’un groupe, des « petites »
entrées/sorties générées par les systèmes transactionnels, qui sont basées sur des
cycles indépendants de lecture-modification-écriture.
Dans tous les cas, on supposera que la taille des blocs de données manipulées
par l’utilisateur est au moins égale à la taille d’un secteur disque.
Notons que l’utilisation de systèmes RAID pour des entrées/sorties mobilisant
plusieurs disques génère un surcoût S par rapport au temps d’un accès unique sur
un disque unique, car il faut attendre la terminaison d’un ensemble de disques non
synchronisés. Dans tous les cas énumérés ci-dessous, on prendra MTTR = 1 heure,
et D = 100 disques, afin de rendre la capacité du système RAID équivalente à celle
du SLED de même génération décrit plus haut.
– RAID 1 : disques miroirs (« mirroring »). Chaque disque est pourvu d’une
copie conforme, donc G = 1 et C = 1. C’est l’option la plus coûteuse, puisque
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
64
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
Caractéristique
MTTF annoncé (années)
Nombre total de disques
Surcoût (%)
Capacité utile (%)
Grosses lectures (1/s)
Grosses écritures (1/s)
Grosses L-M-É (1/s)
Petites lectures (1/s)
Petites écritures (1/s)
Petites L-M-É (1/s)
RAID 1
> 500
2D
100
50
2 D/S
D/S
2 D/3 S
2D
D
2 D/3
Tab. 4.3 – Caractéristiques d’un système RAID 1. Pour les petites écritures, le
débit est supposé égal à D et non D/S car le premier disque ayant terminé sera
prêt à servir la requête suivante.
chaque donnée est dupliquée. Elle est d’ailleurs économiquement bien trop
coûteuse, puisque le MTTF annoncé est très largement supérieur à la durée
de vie du produit, comme indiqué dans le tableau 4.3.
– RAID 2 : codage d’erreur par code de Hamming. Ce codage ne nécessite
que O(log2 (G)) disques de contrôle pour déterminer le disque fautif et regénérer l’information manquante. Ainsi, avec G = 25, on a C = 5. Afin de
paralléliser les accès, chaque bloc de données est réparti sur tous les disques
de données du groupe chargé de son stockage, comme illustré en figure 4.13.
Les caractéristiques du RAID 2 sont données dans le tableau 4.4.
000
0000
0000
1111
000
000
0000
000
000
0000
1111
000
000
0000
000
000111
111
000111
111
0001111
0000
1111
0000
1111
0000
1111
0000 111
1111
000111
111
000111
000
111
00001111
1111
0000111
1111
000111
000111
111
000 111
000
111
0000
1111
0000
1111
0000 111
1111
000111
111
000111
000
111
00001111
1111
0000
1111
0000111
1111
000111
111
000
000
111
0000
1111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
000
111
000
111
0000
1111
0000
1111
0000
1111
0000
1111
000
111
000
111
000
111
0000
1111
0000
1111
000
111
000
111
000
111
000
111
0000
1111
0000
1111
0000
1111
000
111
000
111
000
111
0000
1111
0000
1111
0000
1111
000
111
000
111
000
0000
0000
1111
000
000
0000
000
000
0000
1111
000
000
0000
000
000111
111
000111
111
0001111
0000
1111
0000
1111
0000
1111
0000 111
1111
000111
111
000111
000
111
00001111
1111
0000111
1111
000111
000111
111
000 111
000
111
0000
1111
0000
1111
0000 111
1111
000111
111
000111
000
111
00001111
1111
0000
1111
0000111
1111
000111
111
000
a0 b0 c0 d0 a1 b1 c1 d1 a2 b2 c2 d2 a3 b3 c3 d3 a’x b’x c’x d’x ay b’y c’y d’y a’z b’z c’z d’z
1111
0000
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
Fig. 4.13 – Organisation des données dans un système RAID 2.
Caractéristique
MTTF annoncé (années)
Nombre total de disques
Surcoût (%)
Capacité utile (%)
Grosses lectures (1/s)
Grosses écritures (1/s)
Grosses L-M-É (1/s)
Petites lectures (1/s)
Petites écritures (1/s)
Petites L-M-É (1/s)
RAID 2
12
1, 2 D
20
83
D/S
D/S
D/2 S
D/S G
D/2 S G
D/2 S G
Tab. 4.4 – Organisation des données et caractéristiques d’un système RAID 2.
– RAID 3 : codage d’erreur par parité. Le codage de Hamming utilisé dans le
RAID 2 est lui-même trop coûteux, puisqu’il permet de déterminer le disque
fautif, alors que dans la presque totalité des cas ceci pourra être déterminé
simplement au niveau du contrôleur. On n’a donc besoin que d’un codage
par parité pour regénérer l’information manquante, qui ne coûte qu’un disque
supplémentaire par groupe, comme illustré en figure 4.14. Avec C = 1 et G =
25, la réduction du nombre de disques de contrôle permet même d’augmenter
le MTTF par rapport au RAID 2, comme indiqué dans le tableau 4.5.
Cours d’architectures et systèmes des calculateurs parallèles
65
4.5. DISQUES
0000
1111
000
111
000
111
0000
1111
000
111
000 1111
111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
0000
000
111
000
111
0000
1111
0000
1111
000
111
0000
1111
0000
1111
000
111
0000
1111
0000
1111
000
111
0000
1111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
0000
1111
000
000
0000
000
000
0000
1111
000
000
0000
1111
0000 111
1111
000111
111
00001111
1111
0000111
1111
000 111
111
0000
1111
0000 111
1111
000111
111
0000
1111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
0000
1111
000
111
000
111
0000
1111
000
000
0000
000
000
0000
1111
000
000
0000
1111
0000 111
1111
000111
111
00001111
1111
0000111
1111
000 111
111
0000
1111
0000 111
1111
000111
111
0000
1111
a0 b0 c0 d0 a1 b1 c1 d1 a2 b2 c2 d2 a3 b3 c3 d3 a’ b’ c’ d’
000
111
111
000
000
111
000
111
000
111
000
111
Fig. 4.14 – Organisation des données dans un système RAID 3.
Caractéristique
MTTF annoncé (années)
Nombre total de disques
Surcoût (%)
Capacité utile (%)
Grosses lectures (1/s)
Grosses écritures (1/s)
Grosses L-M-É (1/s)
Petites lectures (1/s)
Petites écritures (1/s)
Petites L-M-É (1/s)
RAID 3
40
1, 04 D
4
96
D/S
D/S
D/2 S
D/S G
D/2 S G
D/2 S G
Tab. 4.5 – Caractéristiques d’un système RAID 3.
– RAID 4 : lectures et écritures indépendantes. Répartir un transfert sur plusieurs disques a comme avantage de réduire le temps de transfert de grosses
entrées/sorties car l’ensemble de la bande passante peut être exploitée. Cependant, les petites entrées/sorties nécessitent d’utiliser tous les disques du groupe
concerné, et donc les RAID 2 et 3 ne peuvent effectuer qu’une entrée/sortie
par groupe à la fois. De plus, si les disques ne sont pas synchronisés, le temps
de réalisation de l’entrée/sortie est celui du disque ayant terminé le dernier,
d’où l’existence du facteur S pour les petites entrées/sorties. Le RAID 4, en
conservant chaque bloc de données sur un unique disque, permet d’effectuer
plusieurs entrées/sorties simultanées dans chaque groupe. Avec cette nouvelle
organisation, illustrée en figure 4.15, le calcul de parité s’effectue sur les mêmes
portions de blocs différents.
On pourrait penser qu’une petite écriture implique tous les disques d’un
groupe, du fait de la nécessité de recalculer le contrôle d’erreur. Cependant,
comme celui-ci se fait par parité, on peut calculer localement sa variation par
ou exclusif entre les anciennes données et les nouvelles. Les caractéristiques
du RAID 4 sont données dans le tableau 4.6.
111
000
0000
1111
0000
1111
0000
1111
000
111
0000
1111
0000
1111
0000
1111
000
111
0000
1111
0000
1111
0000
1111
000
111
0000
1111
0000
1111
0000
1111
000
111
0000
1111
0000
1111
0000
1111
000
111
0000
1111
0000
1111
0000
1111
a0 a1 a2 a3
0000
1111
0000
1111
000
111
000
111
0000
1111
0000
1111
000
111
000
111
111
000
000
111
000
111
0000
1111
0000
1111
0000
1111
000
111
000
111
0000
1111
0000
1111
000
111
000
111
000
111
000
000
0000
1111
0000
0000
1111
000
111
000
111
0000
1111
0000
1111
000
111
000
111
000111
111
000111
111
000
111
00001111
1111
b0 b1 b2 b3 c0 c1 c2 c3
000 1111
111
0000
000
111
0000
1111
111
000
0000
1111
0000
1111
0000
1111
000
111
000
111
0000
1111
00
11
000
111
0000
1111
000
111
0000
1111
0000
1111
0000
1111
000
111
000
111
0000
1111
000
111
0000
1111
00
000
111
0000
1111
0000
1111
0000 11
1111
000
111
d0 d1 d2 d3 0’ 1’ 2’ 3’
Fig. 4.15 – Organisation des données dans un système RAID 4.
– RAID 5 : équivalent au RAID 4, mais avec entrelacement des disques de
contrôle. La faiblesse du RAID 4 réside dans les disques de contrôle de parité,
qui sont des goulots d’étranglement puisqu’ils sont sollicités à chaque écriture
dans un groupe. Pour remédier à cela, le RAID 5 répartit les secteurs de
contrôle sur tous les disques, de façon cyclique. Les caractéristiques du RAID 5
sont données dans le tableau 4.7.
Parce que les lectures sont maintenant réparties sur l’ensemble des disques,
y compris ceux qui étaient uniquement des disques de contrôle dans le mode
RAID 4, toutes les petites E/S sont améliorées d’un facteur (1 + C/G), et
les petites écritures ne bloquent pas l’ensemble des disques du groupe, mais
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
66
CHAPITRE 4. ARCHITECTURE DES MÉMOIRES
Caractéristique
MTTF annoncé (années)
Nombre total de disques
Surcoût (%)
Capacité utile (%)
Grosses lectures (1/s)
Grosses écritures (1/s)
Grosses L-M-É (1/s)
Petites lectures (1/s)
Petites écritures (1/s)
Petites L-M-É (1/s)
RAID 4
40
1, 04 D
4
96
D/S
D/S
D/2 S
D
D/2 G
D/2 G
Tab. 4.6 – Caractéristiques d’un système RAID 4.
Caractéristique
MTTF annoncé (années)
Nombre total de disques
Surcoût (%)
Capacité utile (%)
Grosses lectures (1/s)
Grosses écritures (1/s)
Grosses L-M-É (1/s)
Petites lectures (1/s)
Petites écritures (1/s)
Petites L-M-É (1/s)
RAID 5
40
1, 04 D
4
96
D/S
D/S
D/2
S
C
D
1 + G
1+ C
D/4
G
1+ C
G D/4
Tab. 4.7 – Caractéristiques d’un système RAID 5.
seulement le disque portant le secteur de données et celui portant son secteur
de contrôle, ce qui transforme la pénalité de 1/G en 1/2.
4.6
Systèmes de fichiers parallèles
Une autre approche consiste à distribuer les disques sur les nœuds de la machine
parallèle, afin que chacun d’entre eux dispose d’une zone de va-et-vient (« swap »)
et d’un espace temporaire de stockage (pour l’exécution de programmes « out-ofcore ») propres. Ces disques peuvent être fédérés au moyen de protocoles parallèles
tels que GPFS [10] pour constituer un système de fichiers distribué.
Cours d’architectures et systèmes des calculateurs parallèles
Chapitre 5
Systèmes d’exploitation
5.1
Généralités
Les systèmes d’exploitation supportant le parallélisme appartiennent à deux
familles distinctes :
– celle des systèmes d’exploitation distribués (ou répartis), qui permettent d’utiliser et de partager des ressources et services répartis sur le réseau, en assurant
à l’utilisateur la transparence de celui-ci, ainsi qu’une fiabilité maximale ;
– celle des systèmes d’exploitation des machines multiprocesseurs et parallèles
dédiées au calcul intensif, pour lesquels l’obtention de performances élevées
est primordiale.
À mesure que les représentants de ces deux familles gagnent en maturité, on assiste
à un rapprochement entre ces deux tendances, par influence mutuelle, mais le rapprochement est loin d’être achevé, si tant est qu’il puisse l’être.
Les architectures supportant ces systèmes d’exploitation peuvent être regroupées
en trois classes :
– les machines multiprocesseurs à mémoire partagée, de type UMA ou NUMA,
voire COMA ;
– les machines parallèles à mémoire distribuée, de type NORMA, dont les éléments
sont liés par un réseau d’interconnexion rapide (de 1 à 10 Gbit/s) disposant
éventuellement de fonctionnalités spécifiques : diffusion, synchronisation ;
– les systèmes distribués, constitués d’un ensemble de machines autonomes liées
par un réseau local (avec un débit de 10 Mbit/s à 1 Gbit/s).
5.2
Structure
Un nombre très important de (prototypes de) systèmes d’exploitation a été proposé durant les dix dernières années, qui diffèrent par leurs buts, leur structure, et
leurs fonctionnalités. Du point de vue de leur structure, on peut les regrouper en
plusieurs catégories :
– les systèmes monolithiques. Ils sont constitués d’un noyau complexe et de
grande taille, isolé de l’espace utilisateur par des moyens matériels simples
(segmentation et mode privilégié), mais ne possédant pas de barrières entre
ses différents modules. Toute communication entre processus implémentant
des fonctionnalités système de haut niveau (démons) s’effectue au moyen de
zones de mémoire partagée gérée par le système, ou par envoi explicite de
requêtes (« sockets »). C’est le cas d’UNIX et de VMS, par exemple.
67
68
CHAPITRE 5. SYSTÈMES D’EXPLOITATION
Le manque de barrières solides au sein du noyau, ainsi que la grande taille
et la complexité de celui-ci, rendent de tels systèmes difficiles à maintenir et
à valider, et à fortiori à adapter à un environnement distribué. De fait, les
implémentations parallèles de tels systèmes se sont limitées aux architectures
de type UMA : Solaris pour Sun, IRIX pour SGI, AIX pour IBM, par exemple ;
– les systèmes micro-noyaux. Ils sont constitués d’un micro-noyau minimal s’exécutant sur chaque processeur et ne supportant qu’un nombre restreint de services (gestion de processus, de la mémoire, des communications inter-processus,
et support des gestionnaires de périphériques), le reste des fonctionnalités du
système étant assuré par des serveurs éventuellement situés sur des nœuds
spécialisés (entrée/sorties). L’exécution de la plupart des primitives système
s’effectue donc par l’intermédiaire d’appels de procédures distantes (« Remote
Procedure Call », ou RPC), ce qui fait du réseau d’interconnexion un facteur
critique de performance. Parmi les systèmes micro-noyaux, on trouve MACH
(et OSF/1), Amoeba, Chorus, Choices, Clouds, etc.
La modularité de l’architecture micro-noyau facilite l’adaptation, l’extension,
et la maintenance du système, de même que son implémentation en environnement distribué. Cependant, les chercheurs du projet de micro-noyau
PEACE sont arrivés à la conclusion qu’un micro-noyau supportant le multiprocessus, même reconfigurable, pénalisait le fonctionnement d’une application mono-processus, et donc qu’il était préférable de disposer d’une famille de
micro-noyaux distincts plutôt que d’un micro-noyau évolutif. Des méthodes
de conception orientées objet permettent alors, par la définition de classes
interchangeables, la définition aisée d’une famille de systèmes d’exploitation ;
c’est le cas de Choices, Apertos, Clouds, etc.
– les systèmes orientés objet. À la différence des systèmes uniquement basés
objet, les systèmes orientés objet permettent à l’utilisateur d’utiliser les mécanismes objet de définition par héritage, de renommage dynamique lors de
l’appel des méthodes de l’objet, et de polymorphisme.
Le support du modèle objet repose sur quatre concepts clés : le nommage,
la protection, la synchronisation, et la reprise sur erreur. Chorus et Mach
ne sont pas intrinsèquement des systèmes orientés objet, mais permettent
d’implémenter des environnements de programmation orientés objet, tels que
COOL pour Chorus et Avalon/C++ pour Mach.
5.3
Fonctionnalités
Un système d’exploitation de machine parallèle doit disposer des mêmes fonctionnalités que celles présentes dans un système monoprocesseur. Cependant, leur
complexité est largement supérieure, du fait des contraintes fortes de performance à
respecter, et des nouvelles fonctionnalités à prendre en compte. Parmi les problèmes
spécifiques aux machines parallèles, on peut citer la gestion et la protection de
grands espaces d’adressage, la prévention des interblocages, la gestion efficace d’entités asynchrones telles que les processus et les tâches légères, leur synchronisation,
l’équilibrage de charge et la distribution des données, etc.
5.3.1
Gestion et ordonnancement de processus
Dans les systèmes d’exploitation traditionnels, à un processus correspond un
domaine de protection et un espace d’adressage virtuel servant à l’exécution d’un
unique flot d’instructions. De tels processus sont appelés « lourds », car la création et
la destruction de tels processus sont coûteuses. Le parallélisme exprimé par ce moyen
est à gros grain, et correspond rarement au niveau de granularité des problèmes
Cours d’architectures et systèmes des calculateurs parallèles
5.3. FONCTIONNALITÉS
69
irréguliers.
La plupart des systèmes d’exploitation actuels découplent l’espace d’adressage
et les flots d’instructions, permettant à plusieurs d’entre eux de partager le même
espace. Ces tâches, dites tâches moyennement lourdes (« middleweight threads »),
sont directement gérées par le noyau (on les appelle aussi « kernel threads »), et
disposent de toutes les fonctionnalités système offertes aux processus lourds. De fait,
la gestion de ces tâches se fait au moyen d’appels système lourds (POSIX Pthreads,
par exemple), qui ne permettent pas de mettre en œuvre un parallélisme à grain
fin.
Pour exprimer le parallélisme à grain fin sont apparues les tâches légères (« lightweight threads »), qui s’appuient sur des systèmes de poids lourd ou moyen, et
laissent à la charge de l’utilisateur les fonctions d’ordonnancement. L’utilisateur
peut ainsi définir la politique de gestion des tâches convenant le mieux à son application. C’est le cas des LWP et threads de SunOS et Solaris, des Cthreads de MACH,
etc. Cette architecture à deux niveaux ne permet cependant pas aux tâches légères
de réagir aux événements liés au noyau (préemption, interruptions I/O, ordonnancement des processus moyens, . . . ), ce qui empêche d’adapter le séquencement des
tâches légères au fonctionnement du système. Plusieurs solutions ont été proposées,
comme le report des événements système à l’ordonnanceur des tâches légères, ou
la possibilité pour les tâches utilisateur d’influencer l’ordonnancement des tâches
moyennes sur les processeurs (tel que le « concurrency level » des threads Solaris).
L’ordonnancement (« scheduling ») des processus influe grandement sur les performances des machines parallèles. Il s’agit de minimiser le temps de réponse moyen
du système, en répartissant la charge (« load balancing ») de façon efficace (mais
ce problème est NP-dur)
– l’ordonnancement statique est calculé lors du lancement du programme parallèle, et n’est jamais remis en cause. Il génère un faible surcoût, mais suppose
que le comportement de l’application est stable dans le temps ;
– l’ordonnancement dynamique permet une évolutivité dans le temps convenant
aux applications très irrégulières, mais alourdit beaucoup le code, du fait des
mécanismes d’évaluation de la charge et de migration des données devant être
implémentés, qui doivent parfois opérer de façon asynchrone, par threads ;
– le co-ordonnancement (« coscheduling », ou « gang scheduling ») a pour but
de favoriser l’exécution simultanée de processus appartenant au même programme parallèle, ce qui est très utile dans le cas de processus coopératifs
à grain fin et communiquant fréquemment. Cette technique est complexe, et
soulève de nombreux problèmes, tels la préemption simultanée, l’attente des
processus retardataires, etc.
5.3.2
Gestion de la mémoire
La gestion de la mémoire par les machines de type UMA est semblable à celle des
machines uniprocesseur multiprogrammées. Un gestionnaire de mémoire convertit
les adresses de l’espace virtuel des processus en adresses physiques, gère les défauts
de page, et assure éventuellement les opérations de synchronisation si une page est
accédée simultanément par plusieurs tâches.
Les machines de type NUMA et NORMA nécessitent des mécanismes plus
évolués, qui s’appuient cependant souvent sur les gestionnaires de mémoire locaux,
afin d’offrir un service de mémoire virtuellement partagée (« Distributed Shared Memory ») ; c’est le cas de machines comme les anciens CRAY T3D et SGI Origin, et
de leurs successeurs, par exemple.
Les premières machines NUMA ne géraient que des caches locaux, et s’appuyaient sur une couche logicielle pour gérer la cohérence de la mémoire entre
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
70
CHAPITRE 5. SYSTÈMES D’EXPLOITATION
processeurs, en permettant toutefois à l’utilisateur d’influer sur la répartition des
données en mémoire pour augmenter la localité des accès ; c’était le cas de l’Uniform
System de la BBN Butterfly.
Les architectures NUMA récentes offrent maintenant une mémoire virtuellement partagée globalement cohérente, mais proposent toujours des instructions
matérielles de pré-chargement et de post-écriture (c’est-à-dire permettant la conservation dans un tampon local des écritures distantes non urgentes pour factoriser les
écritures multiples dans le temps et dans l’espace) afin d’accroı̂tre sa performance.
Les recherches actuelles sur la gestion de la mémoire dans les architectures distribuées portent sur la possibilité de décharger le programmeur du placement explicite du code et des données, en s’appuyant sur les similitudes existant avec la
gestion des caches sur les machines UMA. Plusieurs politiques de gestion ont été
étudiées et implantées dans des systèmes tels que Mach OSF/1, Psyche, Platinum,
etc :
– migration : les données sont migrées vers la mémoire locale du processeur qui
les référence, afin de tirer parti le plus possible de la localité des références pour
amortir le coût de la migration. Le mécanisme mis en œuvre pour migrer les
pages d’un nœud à un autre est similaire par son principe à celui qu’emploient
les machines COMA pour migrer les lignes d’un cache à un autre ;
– duplication en lecture : afin de permettre à plusieurs processus de lire localement les mêmes données, on duplique celles-ci sur tous les lecteurs qui en font
la demande. Cependant, les opérations d’écriture deviennent plus coûteuses,
car il faut alors invalider ou mettre à jour les copies des données sur tous les
processeurs qui en possèdent. Des mécanismes matériels peuvent être utilisés
pour optimiser les écritures, comme c’était le cas avec les mécanismes de diffusion et d’invalidation des machines KSR. La relaxation des contraintes de
cohérence forte permet également de gagner en vitesse, si les caractéristiques
de l’application le permettent (accès à des données périmées).
5.3.3
Synchronisation
Lorsque des processus coopérants s’exécutent simultanément, des primitives de
synchronisation sont nécessaires pour contrôler leur concurrence, en particulier afin
d’assurer l’exclusion mutuelle et l’ordonnancement global d’événements. Ceci se fait
principalement au moyen de verrous (« locks »).
Un verrou est un objet qui n’appartient qu’à un seul processus à la fois. Pour
entrer en section critique, un processus doit d’abord acquérir le verrou qui lui est
associée. Dans le cas contraire, il doit s’endormir (« blocking lock »), ou boucler en
attente active (« spin lock »). Cette dernière solution, qui peut sembler onéreuse,
se révèle plus efficace lorsque la section critique est petite ou que la machine est
sous-utilisée, car on évite ainsi de coûteux changements de contexte, et on réduit
la latence entre le moment où le verrou est libéré et celui où on en acquerra la
propriété.
5.3.4
Systèmes de fichiers parallèles et distribués
Le parallélisme, en permettant de traiter des problèmes de grande taille, pose
le problème du stockage et de l’accès aux données manipulées. Pour le résoudre,
plusieurs solutions ont été proposées :
– la délégation des fonctions d’entrées/sorties à des processeurs spécialisés (ou
des nœuds de la machine, pour les architectures NORMA). Dans ce cas, le
système de fichiers est physiquement centralisé, et les requêtes issues des
nœuds de calcul sont traitées par appel distant de procédure (RPC). Cette
Cours d’architectures et systèmes des calculateurs parallèles
5.3. FONCTIONNALITÉS
71
approche est simple à mettre en œuvre, mais tant le réseau que les nœuds
disques peuvent constituer des goulots d’étranglement du système ;
– la distribution du stockage sur les nœuds de la machine, au moyen de disques
locaux. Ceci suppose de pouvoir maintenir une vision cohérente des systèmes
de fichiers, et de savoir sur quels disques se trouvent les différentes portions
des fichiers. En effet, afin de répartir la charge d’accès sur tous les processeurs,
les fichiers sont découpés en blocs (« disk stripping ») qui sont distribués sur
l’ensemble des disques des processeurs.
Des implémentations de ces mécanismes sont maintenant proposés de façon
standard par les constructeurs (comme GPFS [10], par exemple) ou en tant
que projets libres (comme PVFS [23]), et une interface de programmation
pour l’accès parallèle aux fichiers a même été normalisée dans le cadre de la
norme MPI-2, même si l’on s’éloigne quelque peu de la communication par
échange de messages.
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
72
CHAPITRE 5. SYSTÈMES D’EXPLOITATION
Cours d’architectures et systèmes des calculateurs parallèles
Annexe A
Représentation des nombres
à virgule flottante
A.1
Domaine de représentation
Avec n bits, il est possible de coder 2n combinaisons, qui permettent de représenter les nombres entiers non signés compris entre 0 et 2n − 1 ou, en notation dite
« complément à deux », les nombres entiers signés compris entre −2n−1 et 2n−1 − 1,
la valeur 0 étant contenue dans le domaine des nombres positifs.
Cependant, dans de nombreux cas, il n’est pas possible d’utiliser des nombres
entiers, du fait de l’étendue du domaine des nombres manipulés. Par exemple, en
ce qui concerne les masses, celle de l’électron est de 9 × 10−28 grammes, alors que
celle du soleil est de 2 × 1033 grammes. Le domaine dépasse les 1060 , et n’est donc
pas représentable à base d’entiers sur 64 bits. Il est donc nécessaire de disposer d’un
format adapté pour représenter de tels nombres avec un petit nombre de bits (32
ou 64 en pratique).
Comme le domaine à représenter doit être le moins limité possible, il faut
l’échantillonner de façon représentative. On représentera donc un nombre à virgule
sous la forme scientifique du type n = f × be , où f est la « fraction », appelée aussi
« mantisse », b la base et e l’exposant, sous la forme d’un entier signé. Le domaine
dépend de la taille maximale de l’exposant, et la précision du nombre maximal de
chiffres significatifs de la mantisse. On a donc une représentation de la forme :
eE−1
n = m0 , m 1
m2 . . . mM −1 × b
...
e1
e0
,
avec 0 ≤ mi < b et 0 ≤ ei < b. Avec un tel codage, il peut exister plusieurs
représentations possibles du même nombre, qui diffèrent par le nombre de chiffres
initiaux à 0. Afin de maximiser la précision, on on privilégiera toujours le stockage
d’un nombre sous forme normalisée, telle que le premier chiffre de la mantisse soit
significatif, c’est-à-dire différent de zéro.
A.2
La norme IEEE 754
Ce standard définit trois formats de nombres à virgule flottante : les nombres en
« simple précision » sur 32 bits (équivalents au type « float » du langage C), les
nombres en « double précision » sur 64 bits (type « double » du langage C), et les
nombres en « précision étendue » sur 80 bits, ce dernier format étant utilisé pour
73
74ANNEXE A. REPRÉSENTATION DES NOMBRES À VIRGULE FLOTTANTE
1
8
1
11
↑
↑
Signe Exposant
23
52
↑
Mantisse
Fig. A.1 – Structure des nombres en simple et double précision selon la norme
IEEE 754.
Bit de signe
Bits d’exposant
Bits de mantisse
Taille totale
Codage de l’exposant
Valeur de l’exposant
Plus petit nombre normalisé
Plus grand nombre normalisé
Domaine décimal
Simple précision
1
8
23
32
Excédent 127
−126 à +127
2−126
< 2+128
≈ 10−38 à 10+38
Double précision
1
11
52
64
Excédent 1023
−1022 à +1023
2−1022
< 2+1024
−308
≈ 10
à 10+308
Tab. A.1 – Tableau récapitulatif des principales caractéristiques des nombres codés
en simple et double précision selon la norme IEEE 754.
stocker les résultats intermédiaires des calculs effectués au sein des coprocesseurs
arithmétiques tel que celui de l’architecture IA-32. Des extensions plus récentes permettent de gérer des nombres sur 128 bits (type « long double » du langage C99,
supporté nativement par certaines architectures).
Le format IEEE 754 s’appuyant sur une représentation binaire de l’information,
la mantisse est codée en base 2. Les bits de celle-ci représentent donc les puissances
négatives de 2, et comme un nombre normalisé commence nécessairement par un
chiffre différent de 0, qui ne peut être dans ce cas que 1, la valeur de la mantisse est toujours dans l’intervalle [1; 2[. Il n’est donc pas nécessaire de stocker le
chiffre 1 initial, présent de façon implicite, et le bit ainsi gagné sert à augmenter la
précision de la mantisse qui, codée sous cette forme, est appelée « pseudo-mantisse »
ou « significande ». Le nombre de bits dévolus à la mantisse et à l’exposant des
nombres en simple et double précision est représenté en figure A.1, et les domaines
de représentation associés en table A.1.
L’exposant est pour sa part codé sous forme entière, par excédent. Ce dernier
est de 127 pour la simple précision et de 1023 pour la double précision. Les valeurs
binaires d’exposant minimum (0) et maximum (255 ou 2047 selon la précision) sont
réservées pour des codages spéciaux, comme indiqué en table A.2.
Par exemple, le nombre 0.75(10) se code en simple précision de la façon suivante :
0.75(10) = 1.1(2) × 2−1 . Le significande vaut donc .1000 . . . 0(2) , et l’exposant se code
par excès de 127 sous la forme −1 + 127 = 126 = 01111110(2) . Le codage du nombre
sur 32 bits est est donc égal, en hexadécimal, à 3F400000(16) .
Un des problèmes principaux avec les nombres à virgule flottante est la gestion
des erreurs numériques telles que :
– débordements (« overflow ») : le nombre est trop grand pour être représenté ;
Cours d’architectures et systèmes des calculateurs parallèles
75
A.3. USAGE DES NOMBRES FLOTTANTS
Normalisé
Dénormalisé
Infini
Nan
Exposant
000 . . . 00 < < 111 . . . 11
000 . . . 00
111 . . . 11
111 . . . 11
Mantisse
Toute configuration
Tout sauf tous les bits à 0
000 . . . 00
Tout sauf tous les bits à 0
Tab. A.2 – Codage des nombres et valeurs spéciales selon la norme IEEE 754.
– débordements inférieurs (« underflow ») : le nombre est trop petit pour être
représenté ;
– résultat qui n’est pas un nombre (« not-a-number », ou « NaN »), comme par
exemple le résultat d’une division par 0.
En plus des nombres normalisés classiques, la norme IEEE 754 définit donc quatre
autres types numériques :
– not-a-number : résultat impossible ;
– infini : infinis positif et négatif, pour le débordement ;
– zéro : zéros positif et négatif, pour le débordement inférieur ;
– nombres dénormalisés, pour les valeurs trop petites pour être représentables
de façon normalisée.
Les nombres dénormalisés codent des nombres inférieurs au plus petit nombre normalisé représentable. Leur exposant est égal à 0, et leur mantisse est non nulle (sinon
on retomberait sur le codage du zéro). En simple précision, le plus petit nombre
normalisé est 1.0 × 2−126 . Le plus grand nombre dénormalisé est 0.111 . . . 1 × 2−127 ,
qui est équivalent au précédent, et le plus petit est 0.00 . . . 01 × 2−127 , c’est-à-dire
2−23 × 2−127 = 2−150 .
A.3
Usage des nombres flottants
Du fait de leur précision limitée, les nombres flottants peuvent être délicats à
utiliser. La multiplication et la division ne posent pas de problème particulier, car
ils consistent en une multiplication (resp. une division) entière, en virgule fixe, de la
mantisse, et à une addition (resp. soustraction) entière des exposants. En revanche,
l’addition et la soustraction doivent être maniées avec précaution.
A.3.1
Addition
L’addition de deux nombres en virgule flottante s’effectue de la façon suivante :
– comparaison des exposants et calcul de leur différence (soustraction entière) ;
– alignement par dénormalisation (décalage) de la mantisse du plus petit des
deux sur celle du plus grand ;
– addition des mantisses (addition entière) ;
– calcul du facteur de renormalisation de la mantisse résultante (comptage des
bits de poids fort à zéro) ;
– normalisation du résultat (décalage) si nécessaire et si possible.
Lorsque la différence entre les ordres de grandeur des deux nombres à additionner
est plus grande que le nombre de bits de la mantisse, la dénormalisation du plus
petit des deux donne une valeur nulle, et le résultat de l’addition est alors identique
au plus grand des deux nombres. Quand le calcul ne porte que sur deux valeurs,
l’ordre de grandeur du résultat reste correct, mais ce phénomène peut conduire
c 2000, 2007, 2010 F. Pellegrini – ENSEIRB
76ANNEXE A. REPRÉSENTATION DES NOMBRES À VIRGULE FLOTTANTE
à une perte importante de signification des calculs si ceux-ci mettent en jeu de
nombreuses valeurs.
Par exemple, pour calculer la somme d’un tableau de valeurs numériques, il
est dangereux de faire une simple boucle sur les indices, car le résultat obtenu
pourra varier selon la distribution des valeurs dans le tableau : plus les grandes
valeurs auront été rencontrées précocément, et plus elles empêcheront la prise en
compte de valeurs plus petites. Pour conserver au résultat sa signification, il faut
donc idéalement effectuer les additions des plus petites valeurs avant celles des plus
grandes, mais le tri de grands tableaux peut être coûteux, et difficile à mener en
parallèle. On peut alternativement utiliser des tableaux d’accumulation, indicés par
ordre de grandeur : une valeur est ajoutée à la case du tableau dont l’indice est celui
de son ordre de grandeur, et si le résultat est d’un ordre de grandeur supérieur il est
propagé à la case d’indice supérieur, la case utilisée étant alors remise à zéro. À la fin
du calcul, le contenu du tableau d’accumulation est sommé par indices croissants. En
terme de complexité, on remplace un tri en n log(n) par un traitement en n log(2b ),
où b est le nombre de bits de l’exposant.
A.3.2
Soustraction
Les problèmes posés par la soustraction sont duaux de ceux posés par l’addition.
Ils surviennent lorsqu’on soustrait deux nombres de même ordre de grandeur, dont
les bits de poids fort de la mantisse sont identiques. Dans ce cas, la renormalisation
du résultat conduit à un décalage à gauche de la mantisse, et à l’apparition dans
celle-ci de bits dont la valeur ne peut être connue. La perte de précision induite
par ce phénomène peut être partielle si seulement quelques bits sont concernés, ou
totale si presque tous les bits étaient identiques.
Au delà du cas particulier de la soustraction de deux nombres dont le codage
est identique, et pour lesquels on supposera que les nombres initiaux l’étaient aussi,
conduisant à un résultat effectivement nul, la question est de savoir quelles valeurs
donner à ces bits indéterminés devant être introduits dans la mantisse. Si on les
positionne tous à zéro, on conduit à un biais qui peut faire diverger les calculs par
défaut. Si on les positionne tous à un, on conduit également à un biais, mais cette
fois par excès. Certains coprocesseurs positionnent donc ces bits de façon aléatoire,
afin de ne générer aucun biais en moyenne. Cette méthode offre sur la durée une
bonne stabilité numérique aux calculs.
Pour éviter les situations à risque et ne pas multiplier les erreurs commises,
il est préférable de multiplier les nombres avant de les soustraire (distribution de
la multiplication par rapport à la soustraction) lorsque le facteur multiplicatif est
plus grand que un, et inversement d’effectuer la multiplication après la soustraction
lorsque le facteur est plus petit que un.
Cours d’architectures et systèmes des calculateurs parallèles
Bibliographie
[1] ATI Stream Computing.
ting/.
http ://ati.amd.com/technology/streamcompu
[2] S. E. Anderson. Bit twiddling hacks.
~seander/bithacks.html.
http://graphics.stanford.edu/
[3] Y. Choi, A. Knies, L. Gerke, and T. Ngai. The impact of If-conversion and
branch prediction on program execution on the Intel Itaniumtm processor. In
Proceedings of the 34th Annual IEEE/ACM International Symposium on Microarchitecture, pages 30–40, December 2001. http ://www.capsl.udel.edu/
COMPILER/MICRO34/pdf/choi y.pdf.
[4] Cray X-MP/48. http ://en.wikipedia.org/wiki/Cray X-MP.
[5] J. J. Dongarra, J. Du Croz, S. Hammarling, and R. J. Hanson. An extended set of Fortran Basic Linear Algebra Subprograms. ACM Transactions on
Mathematical Software, 14(1) :1–17, March 1988.
[6] M. J. Flynn. Some computer organizations and their effectiveness. IEEE Trans.
Computers, 21(9) :948–960, 1972.
[7] Altivectm technology programming environments manual, April 2006. http ://
www.freescale.com/files/32bit/doc/ref manual/ALTIVECPEM.pdf.
[8] J. R. Goodman. Cache memory optimization to reduce processor/memory
traffic. Technical Report 580, University of Wisconsin–Madison, 1985.
[9] K. Goto and R. van de Geijn. On reducing TLB misses in matrix multiplication.
Technical Report TR-2002-55, University of Texas–Austin, 2002. http ://www.
cs.utexas.edu/users/flame/pubs/FLAWN9.ps.gz.
[10] General parallel file system.
software/gpfs.html.
http ://www.ibm.com/systems/clusters/
[11] R. W. Hockney and C. R. Jesshope. Parallel Computers – Architecture, programming and algorithms. Adam Hilger, Bristol, 1983.
[12] The Cell project at IBM Research. http ://www.research.ibm.com/cell/.
[13] Power architecture. http ://www.ibm.com/power/.
[14] Intel Tera-scale Computing. http ://www.intel.com/research/platform/
terascale/.
[15] W. Jalby and C. Lemuet. Exploring and optimizing Itanium2tm cache(s) performance for scientific computing. In Proceedings of EPIC2, pages 4–19, November
2002. http ://systems.cs.colorado.edu/EPIC2/.
[16] C. Koelbel, D. Loveman, R. Schreiber, G. Steele, and M. Zosel. The High
Performance Fortran Handbook. MIT Press, Cambridge, MA, 1994.
[17] C. L. Lawson, R. J. Hanson, D. R. Kincaid, and F. T. Krogh. Basic Linear
Algebra Subprograms for Fortran usage. ACM Transactions on Mathematical
Software, 5(3) :308–323, September 1979.
77
78
BIBLIOGRAPHIE
[18] J. Lee and A. Smith. Branch prediction strategies and branch target buffer
design. IEEE Trans. Computers, 21(7) :6–22, 1984.
[19] P. Michaud. Chargement des instructions sur les processeurs superscalaires.
Thèse de Doctorat, IRISA, Université Rennes I, November 1998.
[20] NVIDIA Tesla GPU computing solutions for HPC. http ://www.nvidia.com/
page/hpc.html.
[21] OpenMP Fortran Application Program Interface. http://www.openmp.org/.
[22] D. A. Patterson, G. Gibson, and R. H. Katz. A case for redundant arrays of
inexpensive disks (raid). Research Report 87/391, CS Division, University of
California at Berkeley, 1987. ftp ://sunsite.berkeley.edu/pub/techreps/
CSD-87-391.html.
[23] Parallel virtual file system. http ://www.pvfs.org/.
[24] S. Raina. Virtual shared memory : A survey of techniques and systems.
Research Report CSTR-92-36, Department of Computer Science, University
of Bristol, December 1992. http ://www.cs.bris.ac.uk/Tools/Reports/
Abstracts/1992-raina.html.
[25] R. Rakvic, E. Grochowski, B. Black, M. Annavaram, T. Diep, and P. Shen.
Performance advantage of the register stack in Intel Itaniumtm processors. In
Proceedings of EPIC2, pages 30–40, November 2002. http ://systems.cs.
colorado.edu/EPIC2/.
[26] B. Sinharoy, R. N. Kalla, J. M. Tendler, R. J. Eickemeyer, and J. B. Joyner.
POWER5 system microarchitecture. IBM Journal of Research and Development, 49(4/5), July 2005. http ://www.research.ibm.com/journal/rd/494/
sinharoy.html.
[27] http ://www.top500.org/. Site recensant les systèmes installés les plus puissants au monde.
[28] D. W. Wall. Limits of instruction-level parallelism. Research Report 93/6,
DEC Western Research Laboratory, November 1993. ftp ://gatekeeper.
research.compaq.com/pub/DEC/WRL/research-reports/WRL-TR-93.6.pdf.
Cours d’architectures et systèmes des calculateurs parallèles
Téléchargement