Fonctionnement d'un ordinateur/La hiérarchie mémoire

Un livre de Wikilivres.

Sur la plupart des systèmes embarqués ou des tous premiers ordinateurs, on n'a que deux mémoires : une mémoire RAM et une mémoire ROM, comme indiqué dans le chapitre précédent. Mais ces systèmes sont très simples et peuvent se permettre d'implémenter l'architecture de base sans devoir y ajouter quoi que ce soit. Ce n'est pas le cas sur les ordinateurs plus puissants.

Un ordinateur moderne ne contient pas qu'une seule mémoire, mais plusieurs. Entre le disque dur, la mémoire RAM, les différentes mémoires cache, et autres, il y a de quoi se perdre. Et de plus, toutes ces mémoires ont des caractéristiques, voire des fonctionnements totalement différents. Certaines mémoires seront très rapides, d'autres auront une grande capacité mémoire (elles pourront conserver beaucoup de données), certaines s'effacent quand on coupe le courant et d'autres non.

Finalement, l'architecture d'un ordinateur moderne diffère de l'architecture de base par la présence d'une grande quantité de mémoires, organisées sous la forme d'une hiérarchie qui va des mémoires très rapides mais très petites à des mémoires de forte capacité très lentes. Le reste de l’architecture ne change pas trop par rapport à l'architecture de base : on a toujours un processeur, des entrées-sorties, un bus de communication, et tout ce qui s'en suit.

La distinction entre mémoire primaire et secondaire[modifier | modifier le wikicode]

La première amélioration de l'architecture de base consiste à rajouter un niveau de mémoire. Il n'y a alors que deux niveaux de mémoire : la mémoires primaires directement accessible par le processeur, et la mémoire secondaire accessible comme les autres périphériques. La mémoire primaire, correspond actuellement à la mémoire RAM de l'ordinateur, dans laquelle sont chargés les programmes en cours d’exécution et les données qu'ils manipulent. Les mémoires secondaires correspondent aux disques durs, disques SSD, clés USB et autres. Ce sont des périphériques connectés sur la carte mère ou via un connecteur externe.

Distinction entre mémoire primaire et mémoire secondaire.

Le démarrage de l'ordinateur à partir d'une mémoire secondaire[modifier | modifier le wikicode]

L'ajout de deux niveaux de mémoire pose quelques problèmes pour le démarrage de l'ordinateur : comment charger les programmes depuis un périphérique ?

Les tout premiers ordinateurs pouvaient démarrer directement depuis un périphérique. Ils étaient conçus pour cela, directement au niveau de leurs circuits. Ils pouvaient automatiquement lire un programme depuis une carte perforée ou une mémoire magnétique, et le copier en mémoire RAM. Par exemple, l'IBM 1401 lisait les 80 premiers caractères d'une carte perforée et les copiait en mémoire, avant de démarrer le programme copié. Si un programme faisait plus de 80 caractères, les 80 premiers caractères contenaient un programme spécialisé, appelé le chargeur d’amorçage, qui s'occupait de charger le reste. Sur l'ordinateur Burroughs B1700, le démarrage exécutait automatiquement le programme stocké sur une cassette audio, instruction par instruction.

Les processeurs "récents" ne savent pas démarrer directement depuis un périphérique. À la place, ils contiennent une mémoire ROM utilisée pour le démarrage, qui contient un programme qui charge les programmes depuis le disque dur. Rappelons que la mémoire ROM est accessible directement par le processeur.

Sur les premiers ordinateurs avec une mémoire secondaire, le programme à exécuter était en mémoire ROM et la mémoire secondaire ne servait que de stockage pour les données. Le système d'exploitation était dans la mémoire ROM, ce qui fait que l'ordinateur pouvait démarrer même sans mémoire secondaire. La mémoire secondaire était utilisée pour stocker données comme programmes à exécuter. Les programmes à utiliser étaient placés sur des disquettes, des cassettes audio, ou tout autre support de stockage. Les premiers ordinateurs personnels, comme les Amiga, Atari et Commodore, étaient de ce type.

Par la suite, le système d'exploitation aussi a été déporté sur la mémoire secondaire, à savoir qu'il est installé sur le disque dur, voire un SSD. Un cas d'utilisation familier est celui de votre ordinateur personnel. Le système d'exploitation et les logiciels que vous utilisez au quotidien sont mémorisés sur le disque dur. Mais vu qu'aucun ordinateur ne démarre directement depuis le disque dur ou une clé USB, il y a forcément une mémoire ROM dans un ordinateur moderne, qui n'est autre que le BIOS sur les ordinateurs anciens, l'UEFI sur les ordinateurs récents. Elle est utilisée lors du démarrage de l'ordinateur pour le configurer à l'allumage et démarrer son système d'exploitation. La ROM en question ne sert donc qu'au démarrage de l'ordinateur, avant que le système d'exploitation prenne la relève. L'avantage, c'est qu'on peut modifier le contenu du disque dû assez facilement, tandis que ce n'est pas vraiment facile de modifier le contenu d'une ROM (et encore, quand c'est possible). On peut ainsi facilement installer ou supprimer des programmes, en rajouter, en modifier, les mettre à jour sans que cela ne pose problème.

Le fait de mettre les programmes et le système d'exploitation sur des mémoires secondaire a quelques conséquences. La principale est que le système d'exploitation et les autres logiciels sont copiés en mémoire RAM à chaque fois que vous les lancez. Impossible de faire autrement pour les exécuter. Les systèmes de ce genre sont donc des architectures de type Von Neumann ou de type Harvard modifiée, qui permettent au processeur d’exécuter du code depuis la RAM. Vu que le programme s’exécute en mémoire RAM, l'ordinateur n'a aucun moyen de séparer données et instructions, ce qui amène son lot de problèmes, comme nous l'avons dit au chapitre précédent.

La hiérarchie mémoire d'un ordinateur moderne[modifier | modifier le wikicode]

De nos jours, un ordinateur contient plusieurs mémoires, organisées en hiérarchie mémoire. Pour simplifier, il existe quatre grands niveaux de hiérarchie mémoire, indiqués dans le tableau ci-dessous : les registres du processeur, la ou les mémoires cache, la mémoire principale (la RAM) et les mémoires de masse. Notons que même si les ordinateurs actuels ont plus de deux niveaux de mémoire, la distinction entre mémoire primaire et secondaire est quand même présente. La mémoire primaire n'est autre que la mémoire RAM, alors que la ou les mémoires secondaires correspondent approximativement aux mémoires de masse. Si je dis approximativement, c'est que le terme mémoire de masse regroupe aussi bien les mémoires secondaires que les mémoires tertiaires et quaternaires dont nous reparlerons plus bas.

Les registres et caches sont des mémoires incorporées au processeur, alors que mémoire primaire et secondaire restent séparées du processeur. La hiérarchie mémoire d'un ordinateur moderne est donc une variante de la hiérarchie à deux niveaux de la section précédente (primaire et secondaire) à laquelle on a rajouté des mémoires internes au processeurs. Le rajout de ces niveaux supplémentaires est une question de performance.

Les processeurs anciens pouvaient se passer de mémoires caches et même de registres, ce qui fait que les ordinateurs avaient alors une hiérarchie mémoire à deux niveaux (primaire et secondaire). Mais au fil du temps, les processeurs ont gagné en performances plus rapidement que la mémoire primaire. Il a donc fallu rajouter des niveaux de hiérarchie mémoire au-dessus de la mémoire primaire, ce qui a complexifié la hiérarchie mémoire des ordinateurs.

Les nouveaux niveaux devant être très rapides, de l'ordre de la nanoseconde, il fallait réduire drastiquement la distance entre le processeur et ces mémoires. Cela n'a l'air de rien, mais l'électricité met quelques dizaines ou centaines de nanosecondes pour parcourir les connexions entre le processeur et la mémoire, temps qui fait partie du temps d'accès à la mémoire. En intégrant registres et caches dans le processeur, on s'assure que le temps d'accès est minimal, la mémoire étant la plus proche possible des circuits de calcul.

Type de mémoire Temps d'accès Capacité Relation avec la mémoire primaire/secondaire
Registres 1 nanosecondes Entre 1 et 512 bits Mémoire incorporée dans le processeur
Caches 10 - 100 nanosecondes Milliers ou millions de bits Mémoire incorporée dans le processeur, sauf pour quelques exceptions anciennes
Mémoire RAM 1 microsecondes Milliards de bits Mémoire primaire
Mémoires de masse (Disque dur, disque SSD, autres) 1 millisecondes Centaines de milliards de bits Mémoire secondaire
Hiérarchie mémoire

Les mémoires de masse[modifier | modifier le wikicode]

Les mémoires de masse sont des mémoires de grande capacité, qui servent à stocker de grosses quantités de données. De plus, elles conservent des données qui ne doivent pas être effacés et sont donc des mémoire de stockage permanent (on dit qu'il s'agit de mémoires non-volatiles). Concrètement, elles conservent leurs données mêmes quand l'ordinateur est éteint et ce pendant plusieurs années, voir décennies. Les disques durs, mais aussi les CD/DVD et autres clés USB sont des mémoires de masse. Du fait de leur grande capacité, elles sont très lentes. Leur lenteur pachydermique fait qu'elles n'ont pas besoin de communiquer directement avec le processeur, ce qui fait qu'il est plus pratique d'en faire de véritables périphériques, plutôt que de les souder/connecter sur la carte mère.

Parmi les mémoires de masse, on trouve notamment :

  • les mémoires magnétiques, comme disques durs ou les fameuses disquettes (totalement obsolètes de nos jours) ;
  • les mémoires électroniques, comme les mémoires Flash utilisées dans les clés USB et disques durs SSD ;
  • les disques optiques, comme les CD-ROM, DVD-ROM, et autres CD du genre ;
  • mais aussi quelques mémoires très anciennes et rarement utilisées de nos jours, comme les rubans perforés et quelques autres.

Les mémoires de masse se classent en plusieurs types : les mémoires secondaires proprement dit, les mémoires tertiaires et les mémoires quaternaires. Toutes sont traitées comme des périphériques par le processeur, la différence étant dans l’accessibilité.

  • Une mémoire secondaire a beau être un périphérique, elle est située dans l'ordinateur, connectée à la carte mère. Elle s'allume et s'éteint en même temps que l'ordinateur et est accessible tant que l'ordinateur est allumé. Les disques durs et disques SSD sont dans ce cas.
  • Une mémoire tertiaire est un véritable périphérique, dans le sens où on peut l'enlever ou l'insérer dans un connecteur externe à loisir. Par exemple, les clés USB, les CD/DVD ou les disquettes sont dans ce cas. Une mémoire tertiaire est donc rendue accessible par une manipulation humaine, qui connecte la mémoire à l'ordinateur. Le système d'exploitation doit alors effectuer une opération de montage (connexion du périphérique à l’ordinateur) ou de démontage (retrait du périphérique).
  • Quant aux mémoires quaternaires, elles sont accessibles via le réseau, comme les disques durs montés en cloud.

La mémoire principale[modifier | modifier le wikicode]

La mémoire principale est appelée, par abus de langage, la mémoire RAM. Il s'agit d'une mémoire qui stocke temporairement des données que le processeur doit manipuler (on dit qu'elle est volatile). Elle sert donc essentiellement pour maintenir des résultats de calculs, contenir temporairement des programmes à exécuter, etc. Il s'agit d'une mémoire adressable.

La mémoire principale peut être vue comme un rassemblement de "registres" de même taille (qui ont tous une même quantité de bits). Cette description simpliste décrit parfaitement certaines mémoires RWM électroniques, dans le sens où chaque byte correspond bien à un registre dans la mémoire. Cette description est à nuancer quelque peu pour d'autres mémoires RWM et pour les mémoires ROM, qui implémentent leur contenu avec d'autres composants que des bascules.

Les caches et local stores[modifier | modifier le wikicode]

Illustration des mémoires caches et des local stores. Le cache est une mémoire spécialisée, de type SRAM, intercalée entre la RAM et le processeur. Les local stores sont dans le même cas, mais ils sont composées du même type de mémoire que la mémoire principale (ce qui fait qu'ils sont abusivement mis au même niveau sur ce schéma).

Le troisième niveau est intermédiaire entre les registres et la mémoire principale. Il regroupe deux types distincts de mémoires : les mémoires caches (du moins, certains caches) et les local stores.

Dans la majorité des cas, la mémoire intercalée entre les registres et la mémoire RAM/ROM est ce qu'on appelle une mémoire cache. De nos jours, ce cache est intégré dans le processeur, mais il a existé des caches qui s'installaient sur un port dédié de la carte mère, du temps du Pentium 1 et 2. Aussi bizarre que cela puisse paraître, elle n'est jamais adressable ! Le contenu du cache est géré par un circuit spécialisé et le programmeur ne peut pas gérer directement ce cache.

Le cache contient une copie de certaines données présentes en RAM et cette copie est accessible bien plus rapidement, le cache étant beaucoup plus rapide que la RAM. Tout accès mémoire provenant du processeur est intercepté par le cache, qui vérifie si une copie de la donnée demandée est présente ou non dans le cache. Si c'est le cas, on accède à la copie le cache : on a un succès de cache (cache hit). Sinon, c'est un défaut de cache (cache miss) : on est obligé d’accéder à la RAM et/ou de charger la donnée de la RAM dans le cache. Tout s'éclairera dans le chapitre dédié aux mémoires caches.

Sur certains processeurs, les mémoires caches sont remplacées par des mémoires RAM appelées des local stores. Ce sont des mémoires RAM, identiques à la mémoire RAM principale, mais qui sont plus petites et plus rapides. Contrairement aux mémoires caches, il s'agit de mémoires adressables, ce qui fait qu'elles ne sont plus gérées automatiquement par le processeur : c'est le programme en cours d'exécution qui prend en charge les transferts de données entre local store et mémoire RAM. Ces local stores consomment moins d'énergie que les caches à taille équivalente : en effet, ceux-ci n'ont pas besoin de circuits compliqués pour les gérer automatiquement, contrairement aux caches. Côté inconvénients, ces local stores peuvent entraîner des problèmes de compatibilité : un programme conçu pour fonctionner avec des local stores ne fonctionnera pas sur un ordinateur qui en est dépourvu.

Les registres du processeur[modifier | modifier le wikicode]

Enfin, le dernier niveau de hiérarchie mémoire est celui des registres, de petites mémoires très rapides et de faible capacité. Celles-ci sont intégrées à l'intérieur du processeur. La capacité des registres dépend fortement du processeur. Au tout début de l'informatique, il n'était pas rare de voir des registres de 3, 4, voire 8 bits. Par la suite, la taille de ces registres a augmenté, passant rapidement de 16 à 32 bits, voire 48 bits sur certaines processeurs spécialisés. De nos jours, les processeurs de nos PC utilisent des registres de 64 bits. Il existe toujours des processeurs de faible performance qui utilisent des registres relativement petits, de 8 à 16 bits.

Certains processeurs disposent de registres spécialisés, dont la fonction est prédéterminée une bonne fois pour toutes : un registre est conçu pour stocker, uniquement des nombres entiers, ou seulement des flottants, quand d'autres sont spécialement dédiés aux adresses mémoires. Par exemple, les processeurs présents dans nos PC séparent les registres entiers des registres flottants. Pour plus de flexibilité, certains processeurs remplacent les registres spécialisés par des registres généraux, utilisables pour tout et n'importe quoi. Pour reprendre notre exemple du dessus, un processeur peut fournir 12 registres généraux, qui peuvent stocker 12 entiers, ou 10 entiers et 2 flottants, ou 7 adresses et 5 entiers, etc. Dans la réalité, les processeurs utilisent à la fois des registres généraux et quelques registres spécialisés.

L'origine de la hiérarchie mémoire et son fonctionnement[modifier | modifier le wikicode]

La raison à la présence de toutes ces mémoires est une question de performance. Toutes les mémoires ne sont pas aussi rapides. La rapidité d'une mémoire se mesure grâce à deux paramètres : le temps de latence et son débit binaire.

  • Le temps de latence correspond au temps qu'il faut pour effectuer une lecture ou une écriture : plus il est bas, plus la mémoire est rapide.
  • Le débit mémoire correspond à la quantité d'informations qui peut être récupéré ou enregistré en une seconde dans la mémoire : plus il est élevé, plus la mémoire est rapide

Temps d'accès et débit dépendent de la capacité mémoire mémoire : plus une mémoire peut contenir de données, plus elle est lente. Le fait est que si l'on souhaitait utiliser une seule grosse mémoire dans notre ordinateur, celle-ci serait trop lente et l'ordinateur serait inutilisable. Pour résoudre ce problème, il suffit d'utiliser plusieurs mémoires de taille et de vitesse différentes, qu'on utilise suivant les besoins. Des mémoires très rapides de faible capacité seconderont des mémoires lentes de capacité importante. On peut regrouper ces mémoires en niveaux : toutes les mémoires appartenant à un même niveau ont grosso modo la même vitesse.

Les principes de localité spatiale et temporelle[modifier | modifier le wikicode]

Le but de cette organisation est de placer les données accédées souvent, ou qui ont de bonnes chances d'être accédées dans le futur, dans une mémoire qui soit la plus rapide possible. Le tout est de faire en sorte de placer les données intelligemment, et les répartir correctement dans cette hiérarchie des mémoires. Ce placement se base sur deux principes qu'on appelle les principes de localité spatiale et temporelle :

  • un programme a tendance à réutiliser les instructions et données accédées dans le passé : c'est la localité temporelle ;
  • et un programme qui s'exécute sur un processeur a tendance à utiliser des instructions et des données consécutives, qui sont proches, c'est la localité spatiale.

Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécutés : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). La localité spatiale est donc respectée tant qu'on a pas de branchements qui renvoient assez loin dans la mémoire (appels de sous-programmes). De même, les boucles (des fonctionnalité des langages de programmation qui permettent d’exécuter en boucle un morceau de code tant qu'une condition est remplie) sont un bon exemple de localité temporelle. Les instructions de la boucle sont exécutées plusieurs fois de suite et doivent être lues depuis la mémoire à chaque fois.

On peut exploiter ces deux principes pour placer les données dans la bonne mémoire. Par exemple, si on a accédé à une donnée récemment, il vaut mieux la copier dans une mémoire plus rapide, histoire d'y accéder rapidement les prochaines fois : on profite de la localité temporelle. On peut aussi profiter de la localité spatiale : si on accède à une donnée, autant précharger aussi les données juste à côté, au cas où elles seraient accédées. Ce placement des données dans la bonne mémoire peut être géré par le matériel de notre ordinateur, mais aussi par le programmeur.

Une bonne utilisation des principes de localité par les programmeurs[modifier | modifier le wikicode]

De nos jours, le temps que passe le processeur à attendre la mémoire principale devient de plus en plus un problème au fil du temps, et gérer correctement la hiérarchie mémoire est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est très importante : alors qu'une simple addition ou multiplication va prendre entre 1 et 5 cycles d'horloge, une lecture en mémoire RAM fera plus dans les 400-1000 cycles d'horloge. les processeurs modernes utilisent des techniques avancées pour masquer ce temps de latence, qui reviennent à exécuter des instructions pendant ce temps d'attente, mais elles ont leurs limites.

Bien évidement, optimiser au maximum la conception de la mémoire et de ses circuits dédiés améliorera légèrement la situation, mais n'en attendez pas des miracles. Il faut dire qu'il n'y a pas vraiment de solutions facile à implémenter. Par exemple, changer la taille d'une mémoire pour contenir plus de données aura un effet désastreux sur son temps d'accès qui peut se traduire par une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans les jeux vidéos baisser de 2 à 3 % malgré de nombreuses améliorations architecturales très évoluées : la latence du cache L1 avait augmentée de 2 cycles d'horloge, réduisant à néant de nombreux efforts d'optimisations architecturales.

Une bonne utilisation de la hiérarchie mémoire repose en réalité sur le programmeur qui doit prendre en compte les principes de localités vus plus haut dès la conception de ses programmes. La façon dont est conçue un programme joue énormément sur sa localité spatiale et temporelle. Un programmeur peut parfaitement tenir compte du cache lorsqu'il programme, et ce aussi bien au niveau :

  • de son algorithme : on peut citer l'existence des algorithmes cache oblivious ;
  • du choix de ses structures de données : un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse);
  • ou de son code source : par exemple, le sens de parcourt d'un tableau multidimensionnel peut faire une grosse différence.

Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Quoiqu'il en soit, il est quasiment impossible de prétendre concevoir des programmes optimisés sans tenir compte de la hiérarchie mémoire. Et cette contrainte va se faire de plus en plus forte quand on devra passer aux architectures multicœurs.