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