Tout d'abord, un accès à la mémoire principale est très coûteux. Actuellement, un processeur à 2 GHz (le plus lent une fois) a 2G ticks (cycles) par seconde. Un CPU (noyau virtuel de nos jours) peut extraire une valeur de ses registres une fois par tick. Étant donné qu'un cœur virtuel se compose de plusieurs unités de traitement (ALU - unité arithmétique et logique, FPU, etc.), il peut en fait traiter certaines instructions en parallèle si possible.
Un accès à la mémoire principale coûte environ 70 ns à 100 ns (la DDR4 est légèrement plus rapide). Cette fois, il s'agit essentiellement de rechercher le cache L1, L2 et L3 et de frapper la mémoire (envoyer la commande au contrôleur de mémoire, qui l'envoie aux banques de mémoire), attendez la réponse et c'est fait.
100ns signifie environ 200 ticks. Donc, fondamentalement, si un programme manquait toujours les caches auxquels chaque accès en mémoire, le processeur passerait environ 99,5% de son temps (s'il ne lit que la mémoire) inactif à attendre la mémoire.
Afin d'accélérer les choses, il y a les caches L1, L2, L3. Ils utilisent une mémoire placée directement sur la puce et utilisant un type différent de circuits à transistors pour stocker les bits donnés. Cela prend plus de place, plus d'énergie et est plus coûteux que la mémoire principale car un processeur est généralement produit à l'aide d'une technologie plus avancée et une défaillance de production dans la mémoire L1, L2, L3 a la possibilité de rendre le processeur sans valeur (défaut). Les grands caches L1, L2, L3 augmentent le taux d'erreur, ce qui diminue le rendement, ce qui diminue directement le retour sur investissement. Il y a donc un énorme compromis en ce qui concerne la taille du cache disponible.
(actuellement, on crée plus de caches L1, L2, L3 afin de pouvoir désactiver certaines parties pour réduire le risque qu'un défaut de production réel soit les zones de mémoire cache rende le défaut CPU dans son ensemble).
Pour donner une idée de timing (source: coûts d'accès aux caches et à la mémoire )
- Cache L1: 1ns à 2ns (2-4 cycles)
- Cache L2: 3ns à 5ns (6-10 cycles)
- Cache L3: 12ns à 20ns (24-40 cycles)
- RAM: 60ns (120 cycles)
Puisque nous mélangeons différents types de CPU, ce ne sont que des estimations, mais donnent une bonne idée de ce qui se passe réellement lorsqu'une valeur de mémoire est récupérée et que nous pourrions avoir un succès ou un échec dans certaines couches de cache.
Ainsi, un cache accélère considérablement l'accès à la mémoire (60ns contre 1ns).
Récupérer une valeur, la stocker dans le cache pour avoir la chance de la relire, c'est bien pour les variables qui sont souvent accédées mais pour les opérations de copie mémoire, ce serait encore trop lent car on lit juste une valeur, écrit la valeur quelque part et ne lit jamais la valeur encore une fois ... pas de hits de cache, très lent (à côté de cela, cela peut arriver en parallèle car nous avons une exécution dans le désordre).
Cette copie mémoire est si importante qu'il existe différents moyens pour l'accélérer. Au début, la mémoire était souvent capable de copier de la mémoire en dehors du processeur. Elle était gérée directement par le contrôleur de mémoire, donc une opération de copie de mémoire n'a pas pollué les caches.
Mais à côté d'une copie de mémoire ordinaire, d'autres accès en série de la mémoire étaient assez courants. Un exemple est l'analyse d'une série d'informations. Avoir un tableau d'entiers et calculer la somme, la moyenne, la moyenne ou même plus simple trouver une certaine valeur (filtre / recherche) était une autre classe très importante d'algorithmes exécutés à chaque fois sur n'importe quel processeur à usage général.
Ainsi, en analysant le modèle d'accès à la mémoire, il est apparu que les données sont lues séquentiellement très souvent. Il y avait une forte probabilité que si un programme lit la valeur à l'index i, le programme lira également la valeur i + 1. Cette probabilité est légèrement supérieure à la probabilité que le même programme lise également la valeur i + 2 et ainsi de suite.
Donc, étant donné une adresse mémoire, c'était (et est toujours) une bonne idée de lire à l'avance et de récupérer des valeurs supplémentaires. C'est la raison pour laquelle il existe un mode boost.
L'accès à la mémoire en mode boost signifie qu'une adresse est envoyée et plusieurs valeurs sont envoyées séquentiellement. Chaque envoi de valeur supplémentaire ne prend que 10 ns supplémentaires (voire moins).
Un autre problème était une adresse. L'envoi d'une adresse prend du temps. Afin d'adresser une grande partie de la mémoire, de grandes adresses doivent être envoyées. Dans les premiers temps, cela signifiait que le bus d'adresses n'était pas assez grand pour envoyer l'adresse en un seul cycle (coche) et que plus d'un cycle était nécessaire pour envoyer l'adresse, ajoutant plus de retard.
Une ligne de cache de 64 octets par exemple signifie que la mémoire est divisée en blocs distincts (sans chevauchement) de mémoire d'une taille de 64 octets. 64 octets signifie que l'adresse de début de chaque bloc a les six bits d'adresse les plus bas pour être toujours des zéros. Il n'est donc pas nécessaire d'envoyer ces six bits de zéro à chaque fois en augmentant l'espace d'adressage 64 fois pour n'importe quel nombre de largeur de bus d'adresse (effet de bienvenue).
Un autre problème que la ligne de cache résout (à côté de la lecture anticipée et de la sauvegarde / libération de six bits sur le bus d'adresse) est la manière dont le cache est organisé. Par exemple, si un cache est divisé en blocs (cellules) de 8 octets (64 bits), il faut stocker l'adresse de la cellule de mémoire pour laquelle cette cellule de cache contient la valeur avec elle. Si l'adresse était également de 64 bits, cela signifie que la moitié de la taille du cache est consommée par l'adresse, ce qui entraîne une surcharge de 100%.
Étant donné qu'une ligne de cache est de 64 octets et qu'un processeur peut utiliser 64 bits - 6 bits = 58 bits (pas besoin de stocker les zéro bits trop à droite), nous pouvons mettre en cache 64 octets ou 512 bits avec une surcharge de 58 bits (11% de surcharge). En réalité, les adresses stockées sont encore plus petites que cela mais il y a des informations d'état (comme la ligne de cache est-elle valide et précise, sale et doit être réécrite en RAM, etc.).
Un autre aspect est que nous avons un cache associatif d'ensembles. Toutes les cellules de cache ne peuvent pas stocker une certaine adresse mais seulement un sous-ensemble de celles-ci. Cela rend les bits d'adresse stockés nécessaires encore plus petits, permet un accès parallèle au cache (chaque sous-ensemble peut être consulté une fois mais indépendamment des autres sous-ensembles).
Il y a plus particulièrement quand il s'agit de synchroniser l'accès cache / mémoire entre les différents cœurs virtuels, leurs multiples unités de traitement indépendantes par cœur et enfin plusieurs processeurs sur une même carte mère (dont il existe des cartes abritant jusqu'à 48 processeurs et plus).
C'est essentiellement l'idée courante pour laquelle nous avons des lignes de cache. L'avantage de la lecture à l'avance est très élevé et le pire des cas de lecture d'un seul octet sur une ligne de cache et de ne plus jamais relire le reste est très mince car la probabilité est très faible.
La taille de la ligne de cache (64) est un compromis judicieusement choisi entre des lignes de cache plus grandes, il est peu probable que le dernier octet de celui-ci soit lu également dans un proche avenir, la durée nécessaire pour récupérer la ligne de cache complète à partir de la mémoire (et pour l'écrire) et aussi la surcharge dans l'organisation du cache et la parallélisation du cache et l'accès à la mémoire.