Passer au contenu principal

Résumé

En s’appuyant sur les fondements établis dans XBOX One par rapport à XBOX Series X|S, cette entrée aborde les problèmes de performance et les coûts associés aux différentes opérations de lecture/écriture dans la mémoire partagée entre les cœurs du processeur. Pour vous rafraîchir la mémoire sur l’architecture, les fonctionnalités et les performances des processeurs de la famille XBOX One et des consoles XBOX Series, nous vous suggérons de consulter l’entrée mentionnée. Introduction Données partagées Les coûts du partage de données Recommandations Annexe : Code

Introduction


XBOX One par rapport à XBOX Series X|S décrit le coût associé à un échec de cache L1. L’un des scénarios les plus courants d’échec de cache L1 est l’écriture à une adresse partagée entre des cœurs du processeur — par exemple, des données partagées entre plusieurs threads. Le but de cette entrée est de fournir des chiffres relatifs sur la pénalité associée au partage de données accessibles en écriture entre les cœurs. La principale conclusion de nos tests est qu’il est préférable de confiner les données des opérations de lecture/écriture à chaque cœur individuel, plutôt que de partager les données entre les cœurs et les threads. Le partage de données entre les cœurs ralentira le traitement de votre code plutôt que de l’accélérer. La famille XBOX One utilise un processeur Jaguar et les consoles XBOX Series utilisent un processeur Hercules. Ces deux processeurs utilisent le protocole MOESI - Wikipédia pour maintenir un modèle de mémoire fort et la cohérence du cache. Chaque ligne de cache peut se trouver dans l’un de cinq états.
  • Modified (modifié)
    • Ce processeur possède la seule copie valide de la ligne de cache et y a apporté des modifications.
    • La ligne de cache ne correspond pas à la mémoire.
  • Owned (possédé)
    • Ce processeur possède la seule copie valide et y a apporté des modifications.
    • D’autres processeurs peuvent avoir une copie en lecture seule.
  • Exclusive (exclusif)
    • Le processeur possède la seule copie de la ligne de cache.
    • Le contenu de la ligne de cache correspond à la mémoire.
  • Shared (partagé)
    • Ce processeur possède l’une des nombreuses copies de la ligne de cache.
    • Un autre processeur peut l’avoir à l’état Owned si des modifications ont été apportées
  • Invalid (non valide)
    • Cette ligne de cache n’est pas valide et doit être récupérée avant d’y accéder.
Consultez le document AMD64 Architecture Programmer’s Manual Volume 2: System Programming sur AMD Developer Central pour obtenir des informations plus détaillées sur l’implémentation d’AMD.

Données partagées


Le partage de données peut se produire de plusieurs façons. L’une des plus évidentes est le partage direct de variables, comme un compteur de références sur une structure. Cela peut facilement entraîner la modification des données lorsque chaque thread fait des copies au moyen d’un shared_ptr, par exemple, en tant que paramètre dans un appel de fonction. Cette opération à elle seule entraînera une contention des ressources. Un autre type de partage de données, appelé faux partage, est plus difficile à détecter. Le faux partage se produit lorsque deux adresses mémoire se résolvent à la même ligne de cache. La ligne de cache L1 des processeurs Jaguar et Hercules a une taille de 64 octets et est alignée sur 64 octets; deux entiers situés dans le même bloc de 64 octets sont donc considérés comme partagés. Prenons, par exemple, les deux structures de données suivantes :
Les données threadOne, threadTwo, threadThree et threadFour sont uniques pour chaque thread. Le thread un n’opère que sur les données threadOne, le thread deux uniquement sur les données threadTwo, et ainsi de suite. Entre ces deux structures de données, OuterClassSlow pourrait fonctionner jusqu’à vingt fois plus lentement qu’OuterClassFast lors du passage à un traitement multithread pour une simple opération d’écriture standard. Cela s’explique par le fait que les données d’OuterClassSlow partagent toutes la même ligne de cache en raison du faux partage. Elles sont adjacentes les unes aux autres en mémoire. Il s’agit d’un exemple de la différence entre un tableau de structures (AoS) et une structure de tableaux (SoA).

Les coûts du partage de données


Profil de test

Le profil de test respectait les critères suivants :
  • Les instructions testées étaient une lecture brute, une écriture brute, un chargement atomique, un stockage atomique et une opération atomique de comparaison et stockage (CAS). Le code utilisé pour chaque ensemble d’instructions est fourni dans l’Annexe : Code.
  • Les tests ont été effectués avec un emplacement mémoire de 8 octets partagé par tous les threads (ligne de cache partagée) ou avec un emplacement mémoire unique de 8 octets pour chaque thread (ligne de cache unique).
  • Quarante mille opérations ont été exécutées dans une boucle serrée.
  • L’optimiseur a été configuré pour réduire la taille au minimum afin de désactiver le déroulement des boucles.
  • Au total, 100 exécutions ont été effectuées; la médiane de toutes les exécutions est présentée dans les tableaux qui suivent.

Tests sur un seul cœur

Comme point de référence, nous avons commencé par tester chaque opération sur un seul cœur. | Opération | XBOX One | XBOX One X | XBOX Series S - 3,4 GHz | XBOX Series X|S - 3,6 GHz | XBOX Series X - 3,8 GHz | |--------------|----------:|----------:|----------:|----------:|----------:| | Lecture brute | 68,78 us | 52,34 us | 11,81 us | 11,16 us | 10,56 us | | Chargement atomique | 68,78 us | 52,34 us | 11,81 us | 11,16 us | 10,56 us | | Écriture brute | 114,61 us | 104,64 us | 11,81 us | 11,16 us | 10,56 us | | Stockage atomique | 369,55 us | 278,99 us | 207,11 us | 195,61 us | 185,31 us | | CAS atomique | 668,79 us | 509,02 us | 203,43 us | 192,13 us | 182,03 us | Le premier constat est la différence de temps entre le processeur Jaguar et le processeur Hercules. Les performances sont jusqu’à quatre fois supérieures sur Hercules. Cela est attribuable à une combinaison d’une vitesse d’horloge plus élevée, d’une bande passante plus élevée et de la prise en charge d’un plus grand nombre d’opérations de mémoire simultanées en cours Si l’on compare 40 000 lectures directes à 40 000 chargements atomiques, il n’y a aucune différence de temps. Cela est dû au modèle de mémoire fort du processeur et au protocole MOESI. Le compilateur peut générer les mêmes instructions pour les deux opérations. Les opérations d’écriture brute et de stockage atomique de ce test tentent d’incrémenter une valeur en mémoire. Dans ce test, le coût double entre une écriture brute et un stockage atomique. C’est parce que le stockage atomique utilise l’instruction xchg, qui inclut implicitement le préfixe lock. Cela peut bloquer le processeur jusqu’à ce que les résultats aient été vidés dans le cache, car il ne peut pas réordonner les opérations de part et d’autre des instructions qui utilisent le préfixe lock. L’écriture brute utilise l’instruction mov. Celle-ci ne bloque pas le processeur, qui est libre de réordonner les opérations de part et d’autre de l’instruction mov. Cela nous amène à l’opération atomique de comparaison et stockage (CAS). L’opération de ce test est semblable à celle des tests d’écriture brute et de stockage atomique : tenter d’incrémenter une valeur en mémoire. Le préfixe lock est utilisé avec l’instruction cmpxchg pour ce test afin de le rendre comparable au test de stockage atomique. Sur le processeur Jaguar, l’opération CAS est plus coûteuse que l’instruction xchg mentionnée précédemment en raison de la comparaison. Toutefois, le processeur Hercules dispose d’une implémentation améliorée qui maintient un coût semblable à celui d’un stockage atomique.

Tests sur plusieurs cœurs

Les tests sur plusieurs cœurs utilisaient tous deux threads dans plusieurs configurations de cœurs.
  • Deux threads sur des cœurs physiques distincts, mais dans la même grappe.
  • Deux threads sur des cœurs physiques distincts et dans des grappes distinctes.
  • Lorsque le SMT était activé, deux threads s’exécutant sur les deux cœurs logiques d’un même processeur physique.
Nous avons effectué deux exécutions différentes pour chaque test. Lors de la première exécution, les deux threads partageaient la même adresse uint64_t. Lors de la deuxième, chaque thread avait sa propre adresse uint64_t unique, qui ne partageait pas de lignes de cache. Nous avons procédé ainsi pour faciliter l’observation de la pénalité de performance lorsque des lignes de cache sont partagées entre les processeurs. Pour chaque test, les threads ont été démarrés en même temps, et le chronométrage ne portait que sur l’itération de la boucle. Les threads ont également été réglés à une priorité élevée afin de réduire autant que possible les risques de changement de contexte. Cent exécutions ont été effectuées pour chaque test; les résultats médians sont présentés dans les tableaux qui suivent. Les chiffres ci-dessous sont tous relatifs au test de base sur un seul cœur : ils indiquent à quel point l’opération était plus coûteuse lorsqu’elle s’exécutait sur plusieurs cœurs.

Lecture brute/chargement atomique

Il s’agit d’une lecture directe à partir d’un emplacement mémoire. Il n’y a aucune contention entre les threads, car rien ne modifie les données : chaque thread possède sa propre copie des données dans son cache L1. Par conséquent, les temps de lecture des données partagées et des données uniques sont les mêmes. En raison du modèle de mémoire fort, le compilateur peut générer le même code pour un chargement atomique que pour une lecture normale. Les temps sont donc les mêmes pour chaque opération. | Test | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - sans SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Un seul cœur | 1,00 | 1,00 | 1,00 | 1,00 | | Même cœur physique, partagé | S.O. | S.O. | 1,97 | S.O. | | Même grappe, partagé | 1,00 | 1,00 | 1,00 | 1,00 | | Grappes différentes, partagé | 1,00 | 1,00 | 1,00 | 1,00 | | Même cœur physique, unique | S.O. | S.O. | 1,97 | S.O. | | Même grappe, unique | 1,00 | 1,00 | 1,00 | 1,00 | | Grappes différentes, unique | 1,00 | 1,00 | 1,00 | 1,00 | Ce tableau permet de tirer deux conclusions principales. La première est que la simple lecture de la mémoire n’a aucun effet sur les performances, même lorsque plusieurs cœurs lisent tous à partir du même emplacement. Chaque processeur a une copie valide dans son cache, et sa ligne de cache est à l’état Shared puisque plus d’un processeur en possède une copie. La deuxième est le doublement du temps pour la configuration où les deux threads s’exécutent sur le même cœur physique. Lorsque le SMT est activé, les ressources du cœur sont partagées entre deux threads. Cela peut entraîner un gain de performance, car souvent un thread n’est pas en mesure d’utiliser toutes les ressources disponibles. Toutefois, ce test est une boucle très serrée qui monopolise l’unité de chargement/stockage et le cache L1. Un seul thread peut utiliser tous les emplacements de l’unité de chargement/stockage; les deux threads doivent donc se les partager à tour de rôle. L’effet net est que chaque thread prend deux fois plus de temps, mais que le cœur effectue deux fois plus de travail. Dans l’ensemble, le cœur effectue la même quantité de travail dans le même laps de temps.

Écriture brute

Une simple écriture dans une mémoire partagée entre des cœurs sera beaucoup plus lente qu’une écriture par un seul cœur dans un emplacement mémoire non partagé. L’opération d’écriture amène le cœur à mettre à jour la ligne de cache et à invalider toutes les copies sur les autres cœurs. Ce test en particulier consiste à incrémenter une valeur dans la mémoire partagée. Cela signifie que si le cœur ne possède pas de copie valide dans son cache, il doit demander les données à la mémoire ou au dernier cœur ayant écrit à cette adresse. Cela peut créer un effet de ping-pong entre les deux cœurs : un cœur met à jour les données, le deuxième cœur lit les données du premier cœur et les met à jour, puis le premier cœur doit lire les données de l’autre cœur avant de pouvoir les mettre à jour. Ce va-et-vient se poursuit à chaque itération de la boucle de test.. La surcharge causée par l’effet de ping-pong est visible dans ce tableau. | Test | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - sans SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Un seul cœur | 1,00 | 1,00 | 1,00 | 1,00 | | Même cœur physique, partagé | S.O. | S.O. | 10,69 | S.O. | | Même grappe, partagé | 10,54 | 8,73 | 24,36 | 23,59 | | Grappes différentes, partagé | 13,61 | 12,78 | 18,89 | 23,57 | | Même cœur physique, unique | S.O. | S.O. | 1,98 | S.O. | | Même grappe, unique | 1,05 | 0,93 | 1,03 | 1,03 | | Grappes différentes, unique | 1,01 | 0,84 | 1,04 | 1,03 | Ce tableau permet de tirer plusieurs conclusions. La première est la différence d’impact relatif sur les performances entre les processeurs Jaguar et Hercules. Sur Hercules, le coût du partage d’une ligne de cache est plus élevé que sur Jaguar. Dans ce cas, Jaguar a un coût de 17 cycles pour demander les données à l’autre cœur, alors que la même opération coûte 90 cycles sur Hercules. Cela s’explique par la hiérarchie de cache plus profonde d’Hercules, soit la présence d’un cache L3 absent de Jaguar. La deuxième est la différence entre l’utilisation d’un emplacement mémoire partagé et celle d’un emplacement unique. Dans ce cas, l’opération est une incrémentation de la valeur actuelle, soit une opération de lecture/modification/écriture. L’opération de lecture doit être satisfaite par le cœur qui a écrit le plus récemment à cet emplacement mémoire. S’il ne s’agit pas du même cœur, le coût de la lecture est considérablement plus élevé. Comme on peut le voir dans le tableau, cela pourrait être jusqu’à vingt-cinq fois plus coûteux. La troisième est la différence relative plus faible lorsque les deux threads partagent un même cœur physique avec le SMT activé. La ligne de cache mise à jour n’a pas besoin d’être récupérée d’un autre cœur, puisqu’elle est déjà locale. Toutefois, le transfert du stockage vers le chargement (store-to-load forwarding) ne peut pas être appliqué. Lorsque le thread A met à jour la valeur, celle-ci doit être vidée dans le cache avant que le thread B puisse l’utiliser. La quatrième est le doublement du temps pour la configuration où les deux threads s’exécutent sur le même cœur physique, mais opèrent sur une mémoire unique. Lorsque le SMT est activé, les ressources du cœur sont partagées entre deux threads. Cela peut entraîner un gain de performance, car souvent un thread n’est pas en mesure d’utiliser toutes les ressources disponibles. Toutefois, ce test est une boucle très serrée qui monopolise l’unité de chargement/stockage et le cache L1. Un seul thread peut utiliser tous les emplacements de l’unité de chargement/stockage; les deux threads doivent donc se les partager à tour de rôle. L’effet net est que chaque thread prend deux fois plus de temps, mais que le cœur effectue deux fois plus de travail. Dans l’ensemble, le cœur effectue la même quantité de travail dans le même laps de temps.

Stockage atomique

Le stockage atomique utilise l’instruction xchg, qui signale implicitement l’indicateur lock. L’instruction xchg doit lire, puis écrire à l’adresse mémoire en question. Pendant ce temps, la ligne de cache est verrouillée, ce qui empêche les autres cœurs d’y accéder. Cela peut bloquer une opération en attente jusqu’à ce que l’instruction xchg ait écrit les données dans le cache. Un autre coût est que le cœur ne peut pas réordonner les opérations de part et d’autre des instructions xchg. Le dernier coût survient si un autre cœur a modifié les données. Dans ce cas, les données devront être récupérées de ce cœur. Tous les autres cœurs devront attendre pendant toute cette séquence lorsqu’ils opèrent sur la ligne de cache partagée. Comme pour le test d’écriture brute, le test de stockage atomique peut présenter un effet de ping-pong semblable; toutefois, dans ce cas, il est plus prononcé et touche même le test sur un seul cœur. Le processeur ne peut pas exécuter de façon spéculative au-delà d’une itération de la boucle. Le thread A exécute l’instruction xchg pendant que le thread B attend l’accès. Dès que le thread A a terminé, le thread B peut continuer et demande immédiatement une copie avant que le thread A puisse demander l’accès pour l’itération suivante de la boucle. Ce tableau présente le coût relatif de l’instruction xchg par rapport à un seul cœur effectuant l’opération. | Test | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - sans SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Un seul cœur | 1,00 | 1,00 | 1,00 | 1,00 | | Même cœur physique, partagé | S.O. | S.O. | 2,38 | S.O. | | Même grappe, partagé | 6,84 | 6,85 | 4,17 | 4,04 | | Grappes différentes, partagé | 10,64 | 13,40 | 3,72 | 3,56 | | Même cœur physique, unique | S.O. | S.O. | 0,98 | S.O. | | Même grappe, unique | 1,00 | 1,00 | 1,00 | 1,00 | | Grappes différentes, unique | 1,01 | 1,00 | 1,00 | 1,00 | Les données de ce tableau permettent de tirer deux conclusions. Les temps relatifs lors de l’utilisation d’une adresse partagée ne sont pas aussi extrêmes pour un stockage atomique que pour une écriture brute. La raison en est qu’une grande partie du coût de ce test a déjà été payée lors du test sur un seul cœur. Toutefois, le coût global du stockage atomique demeure de deux à quatre fois plus élevé que celui d’une écriture brute sur Hercules, et jusqu’à treize fois plus élevé sur Jaguar. La deuxième est qu’Hercules dispose d’une implémentation améliorée des opérations lock, ce qui, dans ce cas, fait en sorte que les tests dans la même grappe et entre grappes différentes prennent approximativement le même temps. Le processeur Jaguar peut être jusqu’à deux fois plus coûteux lorsque des données sont partagées entre des grappes et que des opérations lock sont utilisées.

Comparaison et stockage atomique (CAS)

L’opération atomique de comparaison et stockage (CAS) de ce test utilise l’instruction cmpxchg avec le préfixe lock; cela signifie que, pendant l’exécution de l’opération, la ligne de cache est verrouillée et ne peut pas être accessible aux autres processeurs. C’est de la même façon que fonctionne l’instruction xchg dans le test de stockage atomique. Dans ce test, l’opération CAS effectue le même calcul que l’opération de stockage atomique. Elle tente d’incrémenter la valeur en mémoire, qu’elle soit partagée ou unique. La seule différence est que l’opération CAS n’écrit à l’emplacement mémoire que si celui-ci est égal à une troisième valeur spécifiée. | Test | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - sans SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Un seul cœur | 1,00 | 1,00 | 1,00 | 1,00 | | Même cœur physique, partagé | S.O. | S.O. | 2,91 | S.O. | | Même grappe, partagé | 4,32 | 4,32 | 4,01 | 4,24 | | Grappes différentes, partagé | 10,12 | 14,80 | 3,52 | 3,45 | | Même cœur physique, unique | S.O. | S.O. | 0,91 | S.O. | | Même grappe, unique | 1,00 | 1,00 | 1,00 | 1,00 | | Grappes différentes, unique | 1,00 | 1,00 | 1,00 | 1,00 | Comme les tests de stockage atomique et de comparaison et stockage atomique utilisent tous deux le préfixe lock, leurs coûts relatifs sont les mêmes, et pour les mêmes raisons. Les temps relatifs entre les opérations à un seul thread et multithreads utilisant une adresse partagée ne sont pas aussi extrêmes, car une grande partie du coût de ce test a déjà été payée lors du test sur un seul cœur. Toutefois, le coût global de l’opération CAS demeure de deux à quatre fois plus élevé lorsque les données sont partagées sur Hercules, et jusqu’à quatorze fois plus élevé sur Jaguar. La deuxième conclusion est qu’Hercules dispose d’une implémentation améliorée des opérations lock, ce qui, dans ce cas, fait en sorte que les tests dans la même grappe et entre grappes différentes prennent approximativement le même temps. Le processeur Jaguar peut être jusqu’à deux fois plus coûteux lorsque des données sont partagées entre des grappes et que des opérations lock sont utilisées.

Recommandations


Il existe plusieurs modèles clés que vous pouvez suivre pour éviter de partager des données accessibles en écriture entre les cœurs :
  • Structure de données : passez d’un tableau de structures (AoS) à une structure de tableaux (SoA) où chaque thread opère sur une structure unique.
  • Séparez les données en lecture seule des données en lecture/écriture.
  • Remplissage : vous pouvez remplir une structure selon un multiple de la taille d’une ligne de cache (64 octets). Cela éliminera le faux partage si chaque thread opère sur une structure.
  • Comptage de références (shared_ptr, weak_ptr, etc.) : ils ont tous deux une seule variable de compteur de références qui est partagée entre toutes les instances de l’objet.
  • Soyez très clair quant à la propriété des données. Lorsque des données sont transmises à un autre thread pour traitement, ce thread est désormais propriétaire des données. Évitez d’effectuer quoi que ce soit sur les données tant que ce thread n’en a pas terminé.
  • File de tâches partagée : essayez d’éviter d’utiliser une seule file de tâches partagée par plusieurs threads, surtout si les tâches sont petites. Envisagez plutôt d’avoir une file de tâches unique pour chaque thread. Vous pouvez utiliser l’algorithme de vol de travail (work stealing) pour aider à équilibrer la charge entre les threads.

Annexe : Code


Lecture brute

Chargement atomique

Écriture brute

Stockage atomique

CAS atomique

Dernière modification le 6 octobre 2026