# Fonctionnement d'un ordinateur/Mémoires évoluées

Les mémoires vues au chapitre précédent sont les mémoires les plus simples qui soient. Mais ces mémoires peuvent se voir ajouter quelques améliorations pas franchement négligeables, afin d'augmenter leur rapidité, ou de diminuer leur consommation énergétique. Dans ce chapitre, nous allons voir quelles sont ces améliorations les plus courantes.

## L'accès en rafale

L'accès en rafale est un accès mémoire qui permet de lire ou écrire plusieurs adresses consécutives en envoyant une seule adresse, en un seul accès mémoire. On envoie la première adresse et la mémoire s'occupe de lire/écrire les adresses suivantes les unes après les autres, automatiquement. L'accès en rafale fait que l'on n'a pas à envoyer plusieurs adresses, mais une seule, ce qui libère le processeur durant quelques cycles et lui économise du travail. Un accès de ce type est appelé un accès en rafale, ou encore une rafale.
Le nombre d'adresses consécutives lues lors d'une rafale est généralement fixé une fois pour toutes et toutes les rafales ont la même taille. Par exemple, sur les mémoires asynchrones EDO-RAM, les rafales lisent/écrivent 4 octets consécutifs automatiquement, au rythme d'un par cycle d’horloge. D'autres mémoires gèrent plusieurs tailles pré-fixées, que l'on peut choisir au besoin. Par exemple, on peut choisir entre une rafale de 4 octets consécutifs, 8 octets consécutifs, ou 16 octets consécutifs. C'est le cas sur les mémoires SDRAM, où on peut choisir s'il faut lire 1, 2, 4, ou 8 octets en rafale.

### L'accès en rafale séquentiel, linéaire et entrelacé

Il existe plusieurs types d'accès en rafale : l'accès entrelacé, l'accès linéaire et l'accès séquentiel.
Le mode séquentiel est le mode rafale normal : on accède à des octets consécutifs les uns après les autres. Peu importe l'adresse à laquelle on commence, on lit les N adresses suivantes lors de l'accès en rafale. Sur certaines mémoires, la rafale peut commencer n'importe où. Mais sur d'autres, le mode séquentiel est parfois restreint et ne peut démarrer qu'à certaines adresses bien précises. Par exemple, pour une mémoire dont le mot mémoire fait 4 octets bits, avec une rafale de 8 mots, on ne peut démarrer les rafales qu'à des adresses multiples de 8 * 4 = 64 octets. Il s'agit d'une contrainte dite d'alignement de rafale. Pour le dire autrement, la mémoire est découpées en blocs qui font la même taille qu'une rafale, et une rafale ne peut transmettre qu'on bloc complet en partant du début.
Le mode linéaire est un petit peu plus compliqué. Il lit un bloc de taille fixe, qui est aligné en mémoire, comme expliqué dans le paragraphe précédent. Mais il peut commencer l'accès en rafale n'importe où dans le bloc, tout en lisant/écrivant la totalité du bloc. Par exemple, prenons une rafale de 8 octets, dont les bytes ont les adresses 0, 1, 2, 3, 4, 5, 6, et 7. Un accès séquentiel aligné doit commencer à l'adresse 0. Mais une rafale en mode linéaire peut très bien commencer par lire ou écrire au byte 3, par exemple. Dans ce cas, on commence par lire le byte numéroté 3, puis le 4, le 5, le 6 et le 7. Puis, l'accès reprend au bloc 0, avant d'accèder aux blocs 1, 2 et 3. En clair, la mémoire est découpée en bloc de 8 bytes consécutifs et l'accès lit un bloc complet. Si la première adresse lue commence à la première adresse du bloc, l'accès est identique à l'accès séquentiel. Mais si l'adresse de départ de la rafale est dans le bloc, la lecture commence à cette adresse, puis reprend au début du bloc une fois arrivé au bout.
Le mode entrelacé utilise un ordre différent. Avec ce mode de rafale, le contrôleur mémoire effectue un XOR bit à bit entre un compteur (incrémenté à chaque accès) et l'adresse de départ pour calculer la prochaine adresse de la rafale.
Pour comprendre un petit peu mieux ces notions, nous allons prendre l'exemple du mode rafale sur les processeurs x86 présents dans nos ordinateurs actuels. Sur ces processeurs, le mode rafale permet des rafales de 4 octets, alignés sur en mémoire. Les rafales peuvent se faire en mode linéaire ou entrelacé, mais il n'y a pas de mode séquentiel. Vu que les rafales se font en 4 octets dans ces deux modes, la rafale gère les deux derniers bits de l'adresse, qui sont modifiés automatiquement par la rafale. Dans ce qui suit, nous allons indiquer les deux bits de poids faible et montrer comment ils évoluent lors d'une rafale. Le reste de l'adresse ne sera pas montré, car il pourrait être n'importe quoi.
Voici ce que cela donne en mode linéaire :
Accès en mode rafale de type linéaire sur les processeurs x86. :
| | 1er accès | 2nd accès | 3ème accès | 4ème accès |
| Exemple 1 | 00 | 01 | 10 | 11 |
| Exemple 2 | 01 | 10 | 11 | 00 |
| Exemple 3 | 10 | 11 | 00 | 01 |
| Exemple 4 | 11 | 00 | 01 | 10 |
Voici ce que cela donne en mode entrelacé :
Accès en mode rafale de type entrelacé sur les processeurs x86. :
| | 1er accès | 2nd accès | 3ème accès | 4ème accès |
| Exemple 1 | 00 | 01 | 10 | 11 |
| Exemple 2 | 01 | 00 | 11 | 10 |
| Exemple 3 | 10 | 11 | 00 | 01 |
| Exemple 4 | 11 | 10 | 01 | 00 |

### L'implémentation des accès en rafale

Au niveau de la microarchitecture, l'accès en rafale s'implémente en ajoutant un compteur dans la mémoire. L'adresse de départ est mémorisée dans un registre en aval de la mémoire. Pour gérer les accès en rafale séquentiels, il suffit que le registre qui stocke l'adresse mémoire à lire/écrire soit transformé en compteur.
Pour les accès en rafale linéaire, le compteur est séparé de ce registre. Ce compteur est initialisé à 0 lors de la transmission d'une adresse, mais est incrémenté à chaque cycle sinon. L'adresse à lire/écrire à chaque cycle se calcule en additionnant l'adresse de départ, mémorisée dans le registre, au contenu du compteur. Pour les accès en rafale entrelacés, c'est la même chose, sauf que l'opération effectuée entre l'adresse de départ et le compteur n'est pas une addition, mais une opération XOR bit à bit.

## Les banques et rangées

Sur certaines puces mémoires, un seul boitier peut contenir plusieurs mémoires indépendantes regroupées pour former une mémoire unique plus grosse. Chaque sous-mémoire indépendante est appelée une banque, ou encore un banc mémoire. La mémoire obtenue par combinaison de plusieurs banques est appelée une mémoire multi-banques. Cette technique peut servir à améliorer les performances, la consommation d'énergie, et j'en passe. Par exemple, cela permet de faciliter le rafraichissement d'une mémoire DRAM : on peut rafraichir chaque sous-mémoire en parallèle, indépendamment des autres. Mais cette technique est principalement utilisée pour doubler le nombre d'adresses, doubler la taille d'un mot mémoire, ou faire les deux.

### L'arrangement horizontal

L'arrangement horizontal utilise plusieurs banques pour augmenter la taille d'un mot mémoire sans changer le nombre d'adresses. Chaque banc mémoire contient une partie du mot mémoire final. Avec cette organisation, on accède à tous les bancs en parallèle à chaque accès, avec la même adresse.
Pour l'exemple, les barrettes de mémoires SDRAM ou DDR-RAM des PC actuels possèdent un mot mémoire de 64 bits, mais sont en réalité composées de 8 sous-mémoires ayant un mot mémoire de 8 bits. Cela permet de répartir la production de chaleur sur la barrette : la production de chaleur est répartie entre plusieurs puces, au lieu d'être concentrée dans la puce en cours d'accès.

### L'arrangement vertical

L'arrangement vertical rassemble plusieurs boitiers de mémoires pour augmenter la capacité sans changer la taille d'un mot mémoire. On utilisera un boitier pour une partie de la mémoire, un autre boitier pour une autre, et ainsi de suite. Toutes les banques sont reliées au bus de données, qui a la même largeur que les sorties des banques. Une partie de l'adresse est utilisée pour choisir à quelle banque envoyer les bits restants de l'adresse. Les autres banques sont désactivées. Mais un arrangement vertical peut se mettre en œuvre de plusieurs manières différentes.
La première méthode consiste à connecter la bonne banque et déconnecter toutes les autres. Pour cela, on utilise la broche CS, qui connecte ou déconnecte la mémoire du bus. Cette broche est commandée par un décodeur, qui prend les bits de poids forts de l'adresse en entrée.
Une autre solution est d'ajouter un multiplexeur/démultiplexeur en sortie des banques et de commander celui-ci convenablement avec les bits de poids forts. Le multiplexeur sert pur les lectures, le démultiplexeur pour les écritures.
Sans la technique dite de l'entrelacement, qu'on verra dans la section suivante, on utilise les bits de poids forts pour sélectionner la banque, ce qui fait que les adresses sont réparties comme illustré dans le schéma ci-dessous. Un défaut de cette organisation est que, si on souhaite lire/écrire deux mots mémoires consécutifs, on devra attendre que l'accès au premier mot soit fini avant de pouvoir accéder au suivant (vu que ceux-ci sont dans la même banque).
Si on mélange l'arrangement vertical et l'arrangement horizontal, on obtient ce que l'on appelle une rangée. Sur ces mémoires, les adresses sont découpées en trois morceaux, un pour sélectionner la rangée, un autre la banque, puis la ligne et la colonne.

## L'entrelacement (interleaving)

La technique de l'entrelacement utilise un arrangement vertical assez spécifique, afin de gagner en performance. Avec une mémoire sans entrelacement, on doit attendre qu'un accès mémoire soit fini avant d'en démarrer un autre. Avec l'entrelacement, on peut réaliser un accès mémoire sans attendre que les précédents soient finis. L'idée est d’accéder à plusieurs banques en parallèles. Pendant qu'une banque est occupée par un accès mémoire, on en démarre un nouveau dans une autre banque, et ainsi de suite jusqu’à avoir épuisé toutes les banques libres. L'organisation en question se marie bien avec l'accès en rafale, si des bytes consécutifs sont placés dans des banques séparées.
Précisons que le temps d'accès mémoire ne change pas beaucoup avec l'entrelacement. Par contre, on peut faire plus d'accès mémoire simultanés. Cela implique que la fréquence de la mémoire augmente avec l'entrelacement. Au lieu d'avoir un cycle d'horloge assez long, capable de couvrir un accès mémoire entier, le cycle d'horloge est plus court. On peut démarrer un accès mémoire par cycle d'horloge, mais l'accès en lui-même prend plusieurs cycles. Le nombre de cycles d'un accès mémoire augmente, non pas car l'accès mémoire est plus lent, mais car la fréquence est plus élevée. D'un seul cycle par accès mémoire, on passe à autant de cycles qu'il y a de banques.
Les mémoires à entrelacement ont un débit supérieur aux mémoires qui ne l'utilisent pas, essentiellement car la fréquence a augmentée. Rappelons que le débit binaire d'une mémoire est le produit de sa fréquence par la largeur du bus. L'entrelacement est une technique qui augmente le débit en augmentant la fréquence du bus mémoire, sans pour autant changer les temps d'accès de chaque banque. Tout se passe comment si la fréquence de chaque banque restait la même, mais que l'entrelacement trichait en augmentant la fréquence du bus mémoire et en compensant la différence par des accès parallèles à des banques distinctes.

### L'entrelacement basique

Sans entrelacement, les accès séquentiels se font dans la même banque, ce qui les rend assez lents. Mais il est possible d'accélérer les accès à des bytes consécutifs en rusant quelque peu. L'idée est que des accès consécutifs se fassent dans des banques différentes, et donc que des bytes consécutifs soient localisés dans des banques différentes. Les mémoires qui fonctionnent sur ce principe sont appelées des mémoires à entrelacement simple.
Pour cela, il suffit de prendre une mémoire à arrangement vertical, avec un petit changement : il faut utiliser les bits de poids faible pour sélectionner la banque, et les bits de poids fort pour le Byte.
En faisant cela, on peut accéder à un plusieurs bytes consécutifs assez rapidement. Cela rend les accès en rafale plus rapide. Pour cela, deux méthodes sont possibles.
* La première méthode utilise un accès en parallèle aux banques, d'où son nom d'accès entrelacé parallèle. Sans entrelacement, on doit accéder à chaque banque l'une après l'autre, en lisant chaque byte l'un après l'autre. Avec l’entrelacement parallèle, on lit plusieurs bytes consécutifs en même temps, en accédant à toutes les banques en même temps, avant d'envoyer chaque byte l'un après l'autre sur le bus (ce qui demande juste de configurer le multiplexeur). Un tel accès est dit en rafale : on envoie une adresse, puis on récupère plusieurs bytes consécutifs à partir de cette adresse initiale.
* Une autre méthode démarre un nouvel accès mémoire à chaque cycle d'horloge, pour lire des bytes consécutifs un par un, mais chaque accès se fera dans une banque différente. En faisant cela, on n’a pas à attendre que la première banque ait fini sa lecture/écriture avant de démarrer la lecture/écriture suivante. Il s'agit d'une forme de pipelining, qui fait que l'accès à des bytes consécutifs est rendu plus rapide.

### Les mémoires à entrelacement par décalage

Les mémoires à entrelacement simple ont un petit problème : sur une mémoire à N banques, des accès dont les adresses sont séparées par N mots mémoires vont tous tomber dans la même banque et seront donc impossibles à pipeliner. Pour résoudre ce problème, il faut répartir les mots mémoires dans la mémoire autrement. Dans les explications qui vont suivre, la variable N représente le nombre de banques, qui sont numérotées de 0 à N-1.
Pour obtenir cette organisation, on va découper notre mémoire en blocs de N adresses. On commence par organiser les N premières adresses comme une mémoire entrelacée simple : l'adresse 0 correspond à la banque 0, l'adresse 1 à la banque 1, etc. Pour le bloc suivant, nous allons décaler d'une adresse, et continuer à remplir le bloc comme avant. Une fois la fin du bloc atteinte, on finit de remplir le bloc en repartant du début du bloc. Et on poursuit l’assignation des adresses en décalant d'un cran en plus à chaque bloc. Ainsi, chaque bloc verra ses adresses décalées d'un cran en plus comparé au bloc précédent. Si jamais le décalage dépasse la fin d'un bloc, alors on reprend au début.
En faisant cela, on remarque que les banques situées à N adresses d'intervalle sont différentes. Dans l'exemple du dessus, nous avons ajouté un décalage de 1 à chaque nouveau bloc à remplir. Mais on aurait tout aussi bien pu prendre un décalage de 2, 3, etc. Dans tous les cas, on obtient un entrelacement par décalage. Ce décalage est appelé le pas d'entrelacement, noté P. Le calcul de l'adresse à envoyer à la banque, ainsi que la banque à sélectionner se fait en utilisant les formules suivantes :
* adresse à envoyer à la banque = adresse totale / N ;
* numéro de la banque = (adresse + décalage) modulo N, avec décalage = (adresse totale * P) mod N.
Avec cet entrelacement par décalage, on peut prouver que la bande passante maximale est atteinte si le nombre de banques est un nombre premier. Seulement, utiliser un nombre de banques premier peut créer des trous dans la mémoire, des mots mémoires inadressables. Pour éviter cela, il faut faire en sorte que N et la taille d'une banque soient premiers entre eux : ils ne doivent pas avoir de diviseur commun. Dans ce cas, les formules se simplifient :
* adresse à envoyer à la banque = adresse totale / taille de la banque ;
* numéro de la banque = adresse modulo N.

### L'entrelacement pseudo-aléatoire

Une dernière méthode de répartition consiste à répartir les adresses dans les banques de manière pseudo-aléatoire. La première solution consiste à permuter des bits entre ces champs : des bits qui étaient dans le champ de sélection de ligne vont être placés dans le champ pour la colonne, et vice-versa. Pour ce faire, on peut utiliser des permutations : il suffit d'échanger des bits de place avant de couper l'adresse en deux morceaux : un pour la sélection de la banque, et un autre pour la sélection de l'adresse dans la banque. Cette permutation est fixe, et ne change pas suivant l'adresse. D'autres inversent les bits dans les champs : le dernier bit devient le premier, l'avant-dernier devient le second, etc. Autre solution : couper l'adresse en morceaux, faire un XOR bit à bit entre certains morceaux, et les remplacer par le résultat du XOR bit à bit. Il existe aussi d'autres techniques qui donnent le numéro de banque à partir d'un polynôme modulo N, appliqué sur l'adresse.

## Les mémoires multiports

Les mémoires multiports sont reliées non pas à un, mais à plusieurs bus. Chaque bus est connecté sur la mémoire sur ce qu'on appelle un port. Ces mémoires permettent de transférer plusieurs données à la fois, une par port. Le débit est sont donc supérieur à celui des mémoires mono-port. De plus, chaque port peut être relié à des composants différents, ce qui permet de partager une mémoire entre plusieurs composants. Comme autre exemple, certaines mémoires multiports ont un bus sur lequel on ne peut que lire une donnée, et un autre sur lequel on ne peut qu'écrire.

### Le multiports idéal

Une première solution consiste à créer une mémoire qui soit vraiment multiports. Avec une mémoire multiports, tout est dupliqué sauf les cellules mémoire. La méthode utilisée dépend de si la cellule mémoire est fabriquée avec une bascule, ou avec une cellule SRAM. Elle dépend aussi de l'interface de la bascule.
Les mémoires multiport les plus simples sont les mémoires double port, avec un port de lecture et un d'écriture. Il suffit de prendre des cellules à double port, avec un port de lecture et un d'écriture. Il suffit de connecter la sortie de lecture à un multiplexeur, et l'entrée d'écriture à un démultiplexeur.
On peut améliorer la méthode précédente pour augmenter le nombre de ports de lecture assez facilement : il suffit de connecter plusieurs multiplexeurs.
Les choses sont plus compliquées avec les cellules mémoires à une seule broche d'entrée-sortie, ou à celles connectées à une ligne de bit. Dans les mémoires vues précédemment, chaque cellule mémoire est reliée à bitline via un transistor, lui-même commandé par le décodeur. Chaque port a sa propre bitline dédiée, ce qui donne N bitlines pour une mémoire à N ports. Évidemment, cela demande d'ajouter des transistors de sélection, pour la connexion et la déconnexion. De plus, ces transistors sont dorénavant commandés par des décodeurs différents : un par port. Et on a autant de duplications que l'on a de ports : N ports signifie tout multiplier par N. Autant dire que ce n'est pas l'idéal en termes de consommation énergétique !
Cette solution pose toutefois un problème : que se passe-t-il lorsque des ports différents écrivent simultanément dans la même cellule mémoire ? Eh bien tout dépend de la mémoire : certaines donnent des résultats plus ou moins aléatoires et ne sont pas conçues pour gérer de tels accès, d'autres mettent en attente un des ports lors de l'accès en écriture. Sur ces dernières, il faut évidemment rajouter des circuits pour détecter les accès concurrents et éviter que deux ports se marchent sur les pieds.

### Le multiports à état partagé

Certaines mémoires ont besoin d'avoir un très grand nombre de ports de lecture. Pour cela, on peut utiliser une mémoire multiports à état dupliqué. Au lieu d'utiliser une seule mémoire de 20 ports de lecture, le mieux est d'utiliser 4 mémoires qui ont chacune 5 ports de lecture. Toutefois, ces quatre mémoires possèdent exactement le même contenu, chacune d'entre elles étant une copie des autres : toute donnée écrite dans une des mémoires l'est aussi dans les autres. Comme cela, on est certain qu'une donnée écrite lors d'un cycle pourra être lue au cycle suivant, quel que soit le port, et quelles que soient les conditions.

### Le multiports externe

D'autres mémoires multiports sont fabriquées à partir d'une mémoire à un seul port, couplée à des circuits pour faire l'interface avec chaque port.
Une première méthode pour concevoir ainsi une mémoire multiports est d'augmenter la fréquence de la mémoire mono-port sans toucher à celle du bus. À chaque cycle d'horloge interne, un port a accès au plan mémoire.
La seconde méthode est basée sur des stream buffers. Elle fonctionne bien avec des accès à des adresses consécutives. Dans ces conditions, on peut tricher en lisant ou en écrivant plusieurs blocs à la fois dans la mémoire interne mono-port : la mémoire interne a un port très large, capable de lire ou d'écrire une grande quantité de données d'un seul coup. Mais ces données ne pourront pas être envoyées sur les ports de lecture ou reçues via les ports d'écritures, nettement moins larges. Pour la lecture, il faut obligatoirement utiliser un circuit qui découpe les mots mémoires lus depuis la mémoire interne en données de la taille des ports de lecture, et qui envoie ces données une par une. Et c'est la même chose pour les ports d'écriture, si ce n'est que les données doivent être fusionnées pour obtenir un mot mémoire complet de la RAM interne.
Pour cela, chaque port se voit attribuer une mémoire qui met en attente les données lues ou à écrire dans la mémoire interne : le stream buffer. Si le transfert de données entre RAM interne et stream buffer ne prend qu'un seul cycle, ce n'est pas le cas pour les échanges entre ports de lecture et écriture et stream buffer : si le mot mémoire de la RAM interne est n fois plus gros que la largeur d'un port de lecture/écriture, il faudra envoyer le mot mémoire en n fois, ce qui donne n^cycles. Ainsi, pendant qu'un port accèdera à la mémoire interne, les autres ports seront occupés à lire le contenu de leurs stream buffers. Ces stream buffers sont gérés par des circuits annexes, pour éviter que deux stream buffers accèdent en même temps dans la mémoire interne.
La troisième méthode remplace les stream buffers par des caches, et utilise une mémoire interne qui ne permet pas de lire ou d'écrire plusieurs mots mémoires d'un coup. Ainsi, un port pourra lire le contenu de la mémoire interne pendant que les autres ports seront occupés à lire ou écrire dans leurs caches.
La méthode précédente peut être améliorée, en utilisant non pas une seule mémoire monoport en interne, mais plusieurs banques monoports. Dans ce cas, il n'y a pas besoin d'utiliser de mémoires caches ou de stream buffers : chaque port peut accéder à une banque tant que les autres ports n'y touchent pas. Évidemment, si deux ports veulent lire ou écrire dans la même banque, il y a un conflit d'accès aux banques. Un choix devra être fait et un des deux ports devra être mis en attente.

## Les mémoires à détection et correction d'erreur

La performance et la capacité ne sont pas les deux seules caractéristiques importantes des mémoires. On attend d'elles qu'elles soient fiables, qu'elles stockent des données sans erreur. Si on stocke un 0 dans une cellule mémoire, on ne souhaite pas qu'une lecture ultérieure renvoie un 1 ou une valeur illisible. Malheureusement, ce n'est pas toujours le cas et quelques erreurs mineures peuvent survenir. Les erreurs en question se traduisent le plus souvent par l'inversion d'un bit : un bit censé être à 0 passe à 1, ou inversement. Pour donner un exemple, on peut citer l'incident du 18 mai 2003 dans la petite ville belge de Schaerbeek. Lors d'une élection, la machine à voter électronique enregistra un écart de 4096 voix entre le dépouillement traditionnel et le dépouillement électronique. La faute à un rayon cosmique, qui avait modifié l'état d'un bit de la mémoire de la machine à voter.
La majorité de ces inversions de bits proviennent de l'interaction de particules à haute énergie avec le circuit. Les plus importantes sont les rayons cosmiques, des particules à haute énergie produites dans la haute atmosphère et qui traversent celle-ci à haute vitesse. Les secondes plus importantes sont les rayons alpha, provenant de la radioactivité naturelle qu'on trouve un peu partout. Et, ironie du sort, ces rayons alpha proviennent souvent du métal présent dans la puce elle-même ou de son packaging !
Les techniques pour détecter et corriger ces erreurs sont nombreuses, comme nous l'avions vu dans le chapitre dédié sur les circuits de correction d'erreur. Mais elles ne sont pas appliquées de manière systématique, seulement quand ça en vaut la peine. Pour ce qui est du processeur, les techniques sont très rarement utilisées et sont réservées à l'automobile, l'aviation, le spatial, etc. Pour les mémoires les techniques sont déjà plus fréquentes sur les ordinateurs personnels, bien que vous n'en ayez pas vraiment conscience.
La première raison à cela est que les mémoires sont plus sujettes aux erreurs. Historiquement, du fait de leur conception, les mémoires sont plus sensibles à l'action des rayons cosmiques ou des particules alpha. Leur plus grande densité, le fait qu'elles stockent des bits sur de longues périodes de temps, leur processus de fabrication différent, tout cela les rend plus fragiles. La seconde raison est qu'il existe des techniques assez simples et pratiques pour rendre les mémoires tolérantes aux erreurs, qui ne s'appliquent pas pour le processeur ou les autres circuits. Il s'agit ni plus ni moins que l'usage de codes ECC, que nous avions abordé au début du cours dans un chapitre dédié, mais que nous allons rapidement réexpliquer dans ce qui suit.

### Les mémoires ECC

Les codes de détection et de correction d'erreur ajoutent des bits de correction/détection d'erreur aux données mémorisées. A chaque byte, on rajoute quelques bits calculés à partir des données du byte, qui servent à détecter et éventuellement corriger une erreur. Plus le nombre de bits ajoutés est important, plus la fiabilité des données sera importante. Ils sont généralement assez simples à mettre en œuvre, pour un cout modéré en circuit et en performance.
Il existe différents codes de ce type. Le plus simple est le bit de parité mémoire, qui ajoute un bit au byte mémorisé, de manière à ce que le nombre de bits à 1 soit pair. En clair, si on compte les bits à 1 dans le byte, bit de parité inclus, alors le résultat est pair. Cela permet de détecter qu'une erreur a eu lieu, qu'un bit a été inversé, mais on ne peut pas corriger l'erreur. Un bit de parité indique qu'un bit a été modifié, mais on ne sait pas lequel.
Lorsqu'on lisait un byte dans la mémoire, le contrôleur mémoire calculait le bit de parité du byte lu. Le résultat était alors comparé au bit de parité stocké dans le byte. Si les deux concordent, on suppose qu'il n'y a pas eu d'erreurs. C'est possible qu'il y en ait eu, comme une double erreur qui inverse deux bits à la fois, mais de telles erreurs ne se voient pas avec un bit de parité. Par contre, si les deux bits de parité sont différents, alors on sait qu'il y a eu une erreur. Par contre, vu qu'on ne sait pas quel bit a été inversé, on sait que la donnée du byte est corrompue, sans pouvoir récupérer la donnée originale. Aussi, quand l'ordinateur détectait une erreur, il n'avait pas d'autre choix que de stopper l'ordinateur et d'afficher un écran bleu dans le pire des cas.
Les mémoires DRAM d'avant les années 1990 utilisaient systématiquement un bit de parité par byte, par octet. Les mémoires de l'époque étaient assez peu fiables, du fait de processus de fabrication pas encore perfectionnés, et l'usage d'un bit de parité permettait de compenser cela. Les tous premiers ordinateurs mémorisaient les bits de parité dans une mémoire séparée, adressée en parallèle de la mémoire principale. Mais depuis l'arrivée des barrettes de mémoire, les bits de parité sont stockés dans les byte eux-même, sur la barrette de mémoire. Depuis les années 1990, l'usage d'un bit de parité est tombé en désuétude avec l'amélioration de la fiabilité intrinsèque des DRAM.
Les mémoires ECC utilisent un code plus puissant qu'un simple bit de parité. Le code en question permet non seulement de détecter qu'un bit a été inversé, mais permettent aussi de déterminer lequel. Le code en question ajoute au minimum deux bits par byte. Nous avions vu quelques codes de ce genre dans le chapitre sur les circuits de correction d'erreur, nous ne ferons pas de rappels, qui seraient de toute façon inutiles dans ce chapitre. La majorité des codes utilisés sur les mémoires ECC permettent de corriger l'inversion d'un bit. De plus, ils permettent de détecter les situations où deux bits ont été inversés (deux erreurs simultanés) mais sans les corriger. Mais le cout en circuits est plus conséquent : il y a environ 4 bits d'ECC par octet.
Là encore, la détection/correction d'erreur est le fait de circuits spécialisés qui calculent les bits d'ECC à partir du byte lu, et comparent le tout aux bits d'ECC mémorisés dans la RAM. Les circuits d'ECC se situent généralement dans le contrôleur mémoire, mais se peut qu'ils soient intégrés dans la barrette mémoire. La différence entre les deux est une question de compatibilité. S'ils sont intégrés dans la barrette mémoire, la gestion de l'ECC est complétement transparente et est compatible avec n'importe quelle carte mère, peu importe le contrôleur mémoire utilisé. Par contre, si elle est le fait du contrôleur mémoire, alors il peut y avoir des problèmes de compatibilité. Une barrette non-ECC fonctionnera toujours, mais ce n'est pas le cas des barrettes ECC. Le contrôleur mémoire doit gérer l'ECC et être couplé à des barrettes ECC pour que le tout fonctionne. Si on branche une mémoire ECC sur un contrôleur mémoire qui ne gère pas l'ECC, l'ordinateur ne démarre même pas. Notons que de nos jours, le contrôleur mémoire est intégré dans le processeur : c'est ce dernier qui gère l'ECC.
L'usage de l'ECC sur les ordinateurs personnels est assez complexe à expliquer. Précisons d'abord qu'il est rare de trouver des mémoires ECC dans les ordinateurs personnels, alors qu'elles sont systématiquement présentes sur les serveurs. Par contre, les mémoires cache d'un processeur de PC utilisent systématiquement l'ECC. En effet, si les DRAM sont sensibles aux erreurs, mais que les SRAM le sont tout aussi ! Les caches aussi peuvent subir des erreurs, et ce d'autant plus que le processeur est miniaturisé. Et pour cela, les caches des CPU actuels incorporent soit des bits de parité, soit de la SRAM ECC. Tout dépend du niveau de cache, comme on le verra dans le chapitre sur le cache.

### Le memory scrubbing

La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à un byte, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le memory scrubbing, qui permet de résoudre le problème au prix d'un certain cout en performance.
L'idée est de vérifier chaque byte régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque byte toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Et évidemment, le memory scrubbing a un cout en performance, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats.
Précisons qu'il ne s'agit pas d'un rafraichissement mémoire, même si ça a un effet similaire. Disons que lors de chaque "pseudo-rafraichissement", le byte est purgé de ses erreurs, pas rafraichit. D'ailleurs, les mémoires SRAM peuvent incorporer du memory scrubbing, et de nombreuses mémoires cache ne s'en privent pas, comme on le verra dans le chapitre sur le cache. Cependant, sur les mémoires DRAM, le memory scrubbing peut se faire en même temps que le rafraichissement mémoire, afin de fortement limiter son cout en performance.
Le memory scrubbing peut compléter soit l'ECC, soit un bit de parité. Imaginons par exemple qu'on le combine avec un bit de parité. Le bit de parité permet de détecter qu'une erreur a eu lieu. Mais si deux erreurs ont lieu, le bit de parité ne pourra pas détecter la double erreur. Le bit de parité indiquera que la donnée est valide. Pour éviter cela, on utilise le memory scrubbing pour éviter que deux erreurs consécutives s'accumulent, permettant de détecter un problème dès la première erreur. On n'attend pas de lire la donnée invalide pour vérifier le bit de parité.
Le même raisonnement a lieu avec l'ECC, avec quelques différences. Au lieu d'attendre que deux erreurs aient lieu, ce que l'ECC peut détecter, mais pas corriger, on effectue des vérifications régulières. Si une vérification tombe entre deux erreurs, elle corrigera la première erreur avant que la seconde survienne. Au final, on a une mémoire non-corrompue : l'ECC corrige la première erreur, puis la suivante, au lieu de laisser deux erreurs s'accumuler et d'avoir un résultat détectable mais pas corrigeable.

## Les mémoires à tampon de ligne optimisées

Dans cette section, nous allons voir les optimisations rendues possibles sur les mémoires à tampon de ligne. Ce sont techniquement des mémoires à tampon de ligne. Pour rappel, elles sont organisées en lignes et colonnes. Elles sont composées d'une mémoire dont les bytes sont des lignes, d'un tampon de ligne pour mémoriser la ligne en cours de traitement, et d'un multiplexeur/démultiplexeur pour lire/écrire les mots mémoires adressés dans la ligne.

### L'implémentation du mode rafale

Diverses optimisations se basent sur la présence du tampon de ligne. L'implémentation du mode rafale est par exemple grandement facilitée sur ces mémoires. Une rafale permet de lire le contenu d'une ligne d'un seul bloc, idem pour les écritures. Pour une lecture, la ligne est copiée dans le tampon de ligne, puis la rafale démarre. Les mot mémoires à lire sont alors lus dans le tampon de ligne directement, un par un. Il suffit de configurer le multiplexeur pour passer d'une adresse à la suivante. Le compteur de rafale est relié au multiplexeur, sur son entrée, et est incrémenté à chaque cycle d'horloge du bus mémoire.
Il en est de même pour l'écriture, sauf qu'il y a une étape en plus. La ligne à écrire est copiée dans le tampon de ligne, puis l'écriture en rafale a lieu dans le tampon de ligne, mot mémoire par mot mémoire, et la ligne est ensuite recopiée du tampon de ligne vers la mémoire. Vous vous demandez sans doute pourquoi copier la ligne dans le tampon de ligne avant d'écrire dedans. La réponse est que la rafale ne fait pas forcément la taille d'une ligne. Par exemple, si une ligne fait 126 octets et que la rafale en seulement 8, il faut tenir compte des octets non-modifiés dans la ligne. Sachant qu'il n'y a pas de copie partielle du tampon de ligne dans la mémoire RAM, recopier la ligne pour la modifier est la meilleure solution.
Un défaut de cette implémentation est qu'une rafale ne put pas être à cheval sur deux lignes, sauf si la RAM incorpore des optimisations complémentaires. Les rafales doivent être alignées de manière à rentrer dans une ligne complète. Pour rendre l'alignement plus facile, la taille des lignes doit être un multiple de la longueur de la rafale. De plus, les rafales doivent être alignées, que ce soit en mode séquentiel ou linéaire. Par exemple, si une rafale lit/écrit 4 octets, alors les lignes doivent faire 8 * N octets. De plus, les rafales doivent commencer à une adresse multiple de 8 octets * 4 adresses consécutives = 32 octets. Pour le dire autrement, la rafale voit la mémoire comme des blocs qui peuvent être transmis en rafale. Mais impossible de lancer une rafale au beau milieu d'un bloc, sauf à utiliser le mode rafale linéaire pour revenir au début du bloc quand on atteint la fin.

### Les mémoires à cache de ligne intégré

Quelques modèles de RAM à tampon de ligne ont ajouté un cache qui mémorise les dernières lignes ouvertes, ce qui permet d'améliorer les performances. Les RAM en question sont les EDRAM (enhanced DRAM), ESDRAM (enhanced synchronous DRAM), Virtual Channel Memory RAM, et CDRAM (Cached DRAM). Elles demandaient pour certaines une modification de l'interface, avec des commandes pour copier le tampon de ligne dans le cache, en plus des traditionnelles commandes de lecture/écriture. L'idée était d'avoir plusieurs lignes ouvertes en même temps, ce qui améliorait les performances dans certains scénarios.

### Les optimisations des copies en mémoire

Une telle organisation en tampon de ligne permet d'implémenter facilement les accès en rafale, mais aussi d'autres opérations. L'une d'entre elle est la copie de données en mémoire. Il n'est pas rare que le processeur copie des blocs de données d'une adresse vers une autre. Par exemple, pour copier 12 kibioctets qui commencent à l'adresse X, vers un autre bloc de même taille, mais qui commence à l'adresse M. En théorie, la copie se fait mot mémoire par mot mémoire, mais la technologie row clone permet de faire la copie ligne par ligne.
L'idée est de lire une ligne, de la stocker dans le tampon de ligne, puis de l'écrire à la destination voulue. Pas de passage par le bus de données, les données ne sortent pas de la mémoire. L'avantage est que la copie des données est beaucoup plus rapide. De plus, elle consomme nettement moins d'énergie, car il n'y a pas de transmission sur le bus mémoire, sans compter qu'on n'a pas d'utilisation des multiplexeurs/démultiplexeurs.
L'implémentation demande d'ajouter des registres dans la mémoire pour mémoriser les adresses de départ/destination, mais surtout d'ajouter des commandes sur le bus mémoire pour déclencher ce genre de copie. Il faut ajouter une commande de copie, qui désigne la ligne originelle et la ligne de destination, des numéros de lignes doivent être transmis dans la commande et mémorisés par la mémoire, etc.
L'implémentation est plus compliquée sur les mémoires multi-banques, car il faut prévoir de quoi copier des données d'une banque à l'autre. L'optimisation précédente ne fonctionne alors pas du tout, mais on gagne quand même un peu en performance et en consommation d'énergie, vu qu'il n'y a pas de transmission sur le bus mémoire avec toutes les lenteurs que cela implique.