Je comprends que le processeur introduit les données dans le cache via des lignes de cache, qui - par exemple, sur mon processeur Atom du processeur] - génère environ 64 octets à la fois, quelle que soit la taille des données réelles. lis.
Ma question est:
Imaginez que vous ayez besoin de lire un octet de la mémoire, quels 64 octets seront placés dans le cache?
Les deux possibilités que je vois sont les suivantes: soit les 64 octets commencent à la limite de 64 octets la plus proche en dessous de l’octet d’intérêt, soit les 64 octets sont répartis autour de l’octet de manière prédéterminée (par exemple, moitié inférieur, moitié supérieur, ou tout ce qui précède).
Lequel est-ce?
Si la ligne de cache contenant l'octet ou le mot que vous chargez n'est pas déjà présente dans la mémoire cache, votre CPU demandera les 64 octets commençant à la limite de la ligne de cache (l'adresse la plus grande en dessous de celle dont vous avez besoin est multiple de 64). .
Les modules de mémoire modernes sur PC transfèrent 64 bits (8 octets) à la fois, en une rafale de huit transferts , ainsi une commande déclenche la lecture ou l'écriture d'une ligne de cache complète à partir de la mémoire. (La taille du transfert en rafale SDRAM DDR1/2/3/4 est configurable jusqu'à 64 Go; les CPU sélectionnent la taille du transfert en rafale pour correspondre à la taille de leur ligne de cache, mais 64B est commun)
En règle générale, si le processeur ne peut pas prévoir un accès à la mémoire (et le pré-extraire), le processus de récupération peut prendre environ 90 nanosecondes ou environ 250 cycles d'horloge (de la CPU connaissant l'adresse à la CPU recevant les données).
En revanche, un hit dans le cache L1 a une latence d'utilisation de la charge de 3 ou 4 cycles et un rechargement de magasin a une latence de transfert de magasin de 4 ou 5 cycles sur les CPU x86 modernes. Les choses sont similaires sur d'autres architectures.
Pour en savoir plus: Ulrich Drepper's Ce que tout programmeur devrait savoir sur la mémoire . Le conseil de pré-extraction de logiciel est un peu dépassé: les pré-récupérateurs matériels modernes sont plus intelligents, et l'hyperthreading est bien meilleur que dans les jours P4 (un thread de pré-extraction est donc un gaspillage). En outre, le wiki x86 comporte de nombreux liens de performances pour cette architecture.
Si les lignes de cache ont une largeur de 64 octets, elles correspondent alors à des blocs de mémoire commençant par des adresses divisibles par 64. Les 6 bits les moins significatifs d'une adresse sont un décalage dans la ligne de cache.
Ainsi, pour tout octet donné, la ligne de cache à récupérer peut être trouvée en effaçant les six bits les moins significatifs de l'adresse, ce qui correspond à l'arrondi à l'adresse la plus proche divisible par 64.
Bien que cela soit fait par le matériel, nous pouvons montrer les calculs en utilisant des définitions de macro de référence C:
#define CACHE_BLOCK_BITS 6
#define CACHE_BLOCK_SIZE (1U << CACHE_BLOCK_BITS) /* 64 */
#define CACHE_BLOCK_MASK (CACHE_BLOCK_SIZE - 1) /* 63, 0x3F */
/* Which byte offset in its cache block does this address reference? */
#define CACHE_BLOCK_OFFSET(ADDR) ((ADDR) & CACHE_BLOCK_MASK)
/* Address of 64 byte block brought into the cache when ADDR accessed */
#define CACHE_BLOCK_ALIGNED_ADDR(ADDR) ((ADDR) & ~CACHE_BLOCK_MASK)
Tout d'abord, un accès à la mémoire principale coûte très cher. Actuellement, un processeur à 2 GHz (le plus lent une fois) a 2G ticks (cycles) par seconde. Un processeur (noyau virtuel de nos jours) peut extraire une valeur de ses registres une fois par tick. Dans la mesure où un cœur virtuel se compose de plusieurs unités de traitement (ALU - unité arithmétique et logique, FPU, etc.), il peut traiter certaines instructions en parallèle, si possible.
Un accès à la mémoire principale coûte environ 70 ns à 100 ns (DDR4 est légèrement plus rapide). Cette heure est essentiellement une recherche des caches L1, L2 et L3, puis frappe la mémoire (commande d’envoi au contrôleur de mémoire, qui l’envoie aux banques de mémoire), attend la réponse et termine.
100ns signifie environ 200 ticks. Donc, fondamentalement, si un programme manque toujours les caches auxquels chaque mémoire a accès, le processeur passe environ 99,5% de son temps (s’il ne lit que de la mémoire) en attente de la mémoire.
Pour accélérer les choses, il y a les caches L1, L2, L3. Ils utilisent la mémoire directement placée sur la puce et utilisent un type différent de circuits à transistors pour stocker les bits donnés. Cela prend plus de place, plus d'énergie et coûte plus cher que la mémoire principale puisqu'un processeur est généralement produit avec une technologie plus avancée et qu'un échec de production dans la mémoire L1, L2, L3 a la chance de rendre le processeur sans valeur (défaut), donc les caches L1, L2, L3 importants 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 de cache disponible.
(actuellement, on crée plus de caches L1, L2, L3 afin de pouvoir désactiver certaines parties afin de réduire le risque qu'un défaut de production réel soit lié aux zones de mémoire cache rendent le défaut de la CPU dans son ensemble).
Pour donner une idée du temps (source: coûts d’accès aux caches et à la mémoire )
Étant donné que 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 extraite et que nous pourrions avoir un succès ou un manque dans certaines couches de cache.
Ainsi, un cache accélère considérablement l’accès à la mémoire (60ns contre 1ns).
Extraire une valeur, le stocker dans le cache pour le relire est une bonne chose pour les variables qui sont souvent consultées, mais pour les opérations de copie en mémoire, il serait encore trop lent car il suffit de lire une valeur, d'écrire la valeur quelque part et de ne jamais lire la valeur. encore une fois ... pas de cache cache, mort lent (à côté de cela peut se produire en parallèle puisque nous avons une exécution en panne).
Cette copie en mémoire est si importante qu'il existe différents moyens de l'accélérer. Au début, la mémoire était souvent capable de copier de la mémoire en dehors de la CPU. Il a été géré directement par le contrôleur de mémoire, de sorte qu'une opération de copie de mémoire n'a pas pollué les caches.
Mais à part une copie en mémoire simple, un autre accès série à la mémoire était assez courant. Un exemple est l'analyse d'une série d'informations. Avoir un tableau d’entiers et calculer somme, moyenne, moyenne ou encore plus simple, trouver une certaine valeur (filtre/recherche) constituait une autre classe très importante d’algorithmes exécutés à chaque fois sur n’importe quel processeur polyvalent.
Ainsi, en analysant le modèle d'accès à la mémoire, il était évident que les données sont lues séquentiellement très souvent. Il y avait une forte probabilité que si un programme lisait la valeur à l'indice i, il lirait aussi 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, etc.
Donc, étant donné une adresse mémoire, il était (et reste toujours) une bonne idée de lire à l’avance et d’extraire 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 que plusieurs valeurs sont envoyées de manière séquentielle. Chaque envoi de valeur supplémentaire ne prend qu’environ 10 ns (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. Au début, cela signifiait que le bus d'adresses n'était pas assez grand pour envoyer l'adresse en un seul cycle (tick) et qu'il fallait plus d'un cycle pour envoyer l'adresse en ajoutant plus de retard.
Une ligne de cache de 64 octets, par exemple, signifie que la mémoire est divisée en blocs de mémoire distincts (ne se chevauchant pas) d'une taille de 64 octets. 64 octets signifie que l'adresse de début de chaque bloc contient les six bits d'adresse les plus bas (toujours des zéros). Il n'est donc pas nécessaire d'envoyer ces six bits zéro à chaque fois, ce qui augmente l'espace d'adressage de 64 fois pour un nombre quelconque de largeurs de bus d'adresses (effet de bienvenue).
Un autre problème que la ligne de cache résout (en plus de lire à l’avance et de sauvegarder/libérer six bits sur le bus d’adresses) réside dans la manière dont la mémoire cache est organisée. Par exemple, si une mémoire cache est divisée en blocs (cellules) de 8 octets (64 bits), il est nécessaire de stocker l'adresse de la cellule mémoire pour laquelle cette cellule cache contient la valeur. Si l'adresse est également de 64 bits, cela signifie que l'adresse consomme la moitié de la taille du cache, ce qui entraîne une surcharge de 100%.
Puisqu'une ligne de cache est de 64 octets et qu'un processeur peut utiliser 64 bits - 6 bits = 58 bits (nul besoin de stocker les bits nuls trop à droite), cela signifie que 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 existe des informations sur le statut (la ligne de cache est valide et exacte, sale et doit être écrite en RAM, etc.).
Un autre aspect est que nous avons un cache associatif. Toutes les cellules de cache ne peuvent pas stocker une adresse donnée 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 à la mémoire cache (chaque sous-ensemble est accessible 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 unités de traitement multiples indépendantes par cœur et enfin plusieurs processeurs sur une carte mère (des cartes hébergeant jusqu'à 48 processeurs et plus).
C’est fondamentalement l’idée actuelle pour laquelle nous avons des lignes de cache. L'avantage de lire à l'avance est très élevé et le pire cas de lire un seul octet dans une ligne de cache et de ne plus jamais lire le reste est très mince, car la probabilité est très faible.
La taille de la ligne de cache (64) est un compromis judicieux entre de grandes lignes de cache, il est donc peu probable que le dernier octet soit lu dans un avenir proche, la durée nécessaire pour récupérer la ligne de cache complète de la mémoire (et de le réécrire) ainsi que des frais généraux liés à l’organisation du cache et à la parallélisation des accès au cache et à la mémoire.
Les processeurs peuvent avoir des caches à plusieurs niveaux (L1, L2, L3), et leur taille et leur vitesse diffèrent.
Cependant, pour comprendre le contenu exact de chaque cache, vous devrez étudier le prédicteur de branche utilisé par ce processeur spécifique et voir comment les instructions/données de votre programme se comportent en conséquence.
En savoir plus sur prédicteur de branche , cache de la CP et règles de remplacement .
Ce n'est pas une tâche facile. Si à la fin de la journée tout ce que vous voulez, c'est un test de performance, vous pouvez utiliser un outil tel que Cachegrind . Cependant, comme il s’agit d’une simulation, son résultat peut différer d’un certain degré.
Je ne peux pas affirmer avec certitude que chaque matériel est différent, mais il s’agit en général de "64 octets au-dessous de la limite de 64 octets la plus proche", car il s’agit d’une opération très simple et rapide pour le processeur.