Passer au contenu principal

Résumé

S’appuyant sur les bases établies dans XBOX One et XBOX Series X|S, cette entrée traite des problèmes de performances et des coûts associés aux différentes opérations de lecture/écriture dans la mémoire partagée entre les cœurs de processeur. Pour un rappel sur l’architecture, les fonctionnalités et les performances des processeurs des consoles de la famille XBOX One et de la gamme 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 et XBOX Series X|S décrit le coût associé à un défaut de cache L1. L’un des schémas les plus courants de défaut de cache L1 est l’écriture à une adresse partagée entre des cœurs de processeur, par exemple des données partagées entre plusieurs threads. L’objectif 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. Le principal enseignement 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 ralentit le traitement de votre code au lieu 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 des cinq états suivants.
  • 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 copies de la ligne de cache.
    • Un autre processeur peut la détenir à 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 tout accès.
Pour plus d’informations sur l’implémentation d’AMD, consultez le manuel AMD64 Architecture Programmer’s Manual Volume 2: System Programming, disponible sur AMD Developer Central.

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 effectue des copies via un shared_ptr, par exemple en tant que paramètre d’un appel de fonction. Cette seule opération entraîne 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 correspondent à 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 propres à 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 peut fonctionner jusqu’à vingt fois plus lentement qu’OuterClassFast lors du passage à un traitement multithread, pour une simple opération d’écriture standard. En effet, les données d’OuterClassSlow partagent toutes la même ligne de cache en raison du faux partage. Elles sont adjacentes en mémoire. C’est 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 de 8 octets propre à 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 afin de désactiver le déroulage des boucles.
  • Au total, 100 exécutions ont été effectuées ; la médiane de toutes les exécutions est indiquée dans les tableaux qui suivent.

Test sur un seul cœur

Comme 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 µs | 52,34 µs | 11,81 µs | 11,16 µs | 10,56 µs | | Chargement atomique | 68,78 µs | 52,34 µs | 11,81 µs | 11,16 µs | 10,56 µs | | Écriture brute | 114,61 µs | 104,64 µs | 11,81 µs | 11,16 µs | 10,56 µs | | Stockage atomique | 369,55 µs | 278,99 µs | 207,11 µs | 195,61 µs | 185,31 µs | | CAS atomique | 668,79 µs | 509,02 µs | 203,43 µs | 192,13 µs | 182,03 µs | Le premier point est la différence de durée entre le processeur Jaguar et le processeur Hercules. Les performances sont jusqu’à quatre fois supérieures sur Hercules. Cela s’explique par la combinaison d’une fréquence 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 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 durée. Cela est dû au modèle de mémoire fort du processeur et au protocole MOESI. Le compilateur est en mesure de générer les mêmes instructions pour les deux opérations. Dans ce test, les opérations d’écriture brute et de stockage atomique tentent d’incrémenter une valeur en mémoire. Le coût double entre une écriture brute et un stockage atomique. En effet, 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éorganiser les opérations autour des instructions utilisant le préfixe lock. L’écriture brute utilise l’instruction mov. Celle-ci ne bloque pas le processeur, qui est libre de réorganiser les opérations autour de l’instruction mov. Nous en arrivons à l’opération atomique de comparaison et stockage (CAS). L’opération de ce test est similaire à celle des tests d’écriture brute et de stockage atomique : elle tente d’incrémenter une valeur en mémoire. Le préfixe lock est utilisé avec l’instruction cmpxchg dans ce test pour 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 similaire à celui d’un stockage atomique.

Test sur plusieurs cœurs

Les tests multicœurs ont tous utilisé deux threads dans plusieurs configurations de cœurs.
  • Deux threads sur des cœurs physiques distincts, mais dans le même cluster.
  • Deux threads sur des cœurs physiques distincts et dans des clusters distincts.
  • 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 seconde, chaque thread disposait de sa propre adresse uint64_t unique, qui ne partageait pas de lignes de cache. Nous avons procédé ainsi pour mieux faire apparaître la pénalité de performances lorsque des lignes de cache sont partagées entre processeurs. Pour chaque test, les threads ont été démarrés en même temps, la mesure ne portant que sur l’itération de la boucle. Les threads ont également été définis en priorité élevée afin de réduire autant que possible le risque de changement de contexte. Cent exécutions ont été effectuées pour chaque test ; les résultats médians sont indiqués dans les tableaux qui suivent. Les chiffres ci-dessous sont tous relatifs au test de référence sur un seul cœur : ils indiquent dans quelle mesure l’opération est plus coûteuse lorsqu’elle s’exécute 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 dispose de sa propre copie des données dans son cache L1. Par conséquent, les durées de lecture des données partagées et des données uniques sont identiques. En raison du modèle de mémoire fort, le compilateur est autorisé à générer le même code pour un chargement atomique que pour une lecture normale. Les durées sont donc identiques 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é | N/A | N/A | 1,97 | N/A | | Même cluster, partagé | 1,00 | 1,00 | 1,00 | 1,00 | | Clusters différents, partagé | 1,00 | 1,00 | 1,00 | 1,00 | | Même cœur physique, unique | N/A | N/A | 1,97 | N/A | | Même cluster, unique | 1,00 | 1,00 | 1,00 | 1,00 | | Clusters différents, unique | 1,00 | 1,00 | 1,00 | 1,00 | Ce tableau appelle deux remarques 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 au même emplacement. Chaque processeur dispose d’une copie valide dans son cache, et sa ligne de cache est à l’état Shared puisque plusieurs processeurs en possèdent une copie. La seconde est le doublement de la durée pour la configuration dans laquelle 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 performances, car il arrive souvent qu’un thread ne soit 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 est capable d’utiliser tous les emplacements de l’unité de chargement/stockage ; les deux threads doivent donc se les partager à tour de rôle. Au final, chaque thread prend deux fois plus de temps, mais le cœur effectue deux fois plus de travail. Globalement, 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 les cœurs est nettement plus lente qu’une écriture effectuée 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 présentes sur les autres cœurs. Ce test particulier consiste à incrémenter une valeur en mémoire partagée. Cela signifie que si le cœur ne dispose pas d’une 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 second cœur lit les données à partir du premier et les met à jour, puis le premier cœur doit relire les données à partir 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 due à 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é | N/A | N/A | 10,69 | N/A | | Même cluster, partagé | 10,54 | 8,73 | 24,36 | 23,59 | | Clusters différents, partagé | 13,61 | 12,78 | 18,89 | 23,57 | | Même cœur physique, unique | N/A | N/A | 1,98 | N/A | | Même cluster, unique | 1,05 | 0,93 | 1,03 | 1,03 | | Clusters différents, unique | 1,01 | 0,84 | 1,04 | 1,03 | Ce tableau appelle plusieurs remarques. La première concerne la différence d’impact relatif sur les performances entre les processeurs Jaguar et Hercules. Sur Hercules, le partage d’une ligne de cache est plus coûteux 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 est dû à la hiérarchie de cache plus profonde d’Hercules, qui comporte un cache L3 absent de Jaguar. La deuxième concerne la différence entre l’utilisation d’un emplacement mémoire partagé et celle d’un emplacement unique. Ici, l’opération consiste à incrémenter la valeur actuelle ; il s’agit d’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 le montre le tableau, il peut être jusqu’à vingt-cinq fois plus élevé. La troisième concerne 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 à partir d’un autre cœur : elle est déjà locale. Toutefois, le transfert stockage-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 de la durée pour la configuration dans laquelle 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 performances, car il arrive souvent qu’un thread ne soit 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 est capable d’utiliser tous les emplacements de l’unité de chargement/stockage ; les deux threads doivent donc se les partager à tour de rôle. Au final, chaque thread prend deux fois plus de temps, mais le cœur effectue deux fois plus de travail. Globalement, 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 concernée. 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 vient du fait que le cœur ne peut pas réorganiser les opérations autour des instructions xchg. Le dernier coût survient si un autre cœur a modifié les données. Dans ce cas, les données doivent être récupérées à partir de ce cœur. Tous les autres cœurs doivent 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 similaire ; toutefois, il est ici plus prononcé et affecte même le test sur un seul cœur. Le processeur ne peut pas exécuter de manière 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é | N/A | N/A | 2,38 | N/A | | Même cluster, partagé | 6,84 | 6,85 | 4,17 | 4,04 | | Clusters différents, partagé | 10,64 | 13,40 | 3,72 | 3,56 | | Même cœur physique, unique | N/A | N/A | 0,98 | N/A | | Même cluster, unique | 1,00 | 1,00 | 1,00 | 1,00 | | Clusters différents, unique | 1,01 | 1,00 | 1,00 | 1,00 | Les données de ce tableau appellent deux remarques. Les durées relatives lors de l’utilisation d’une adresse partagée sont moins extrêmes pour un stockage atomique que pour une écriture brute. En effet, 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 reste deux à quatre fois plus élevé que celui d’une écriture brute sur Hercules, et jusqu’à treize fois plus élevé sur Jaguar. La seconde est qu’Hercules dispose d’une implémentation améliorée des opérations lock, ce qui, dans ce cas, fait que les tests sur le même cluster et sur des clusters différents prennent approximativement le même temps. Le processeur Jaguar peut être jusqu’à deux fois plus coûteux lorsque les données sont partagées entre clusters et que des opérations lock sont utilisées.

Comparaison et stockage atomiques (CAS)

Dans ce test, l’opération atomique de comparaison et stockage (CAS) utilise l’instruction cmpxchg avec le préfixe lock, ce qui signifie que pendant l’exécution de l’opération, la ligne de cache est verrouillée et inaccessible aux autres processeurs. C’est de la même manière 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 s’il 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é | N/A | N/A | 2,91 | N/A | | Même cluster, partagé | 4,32 | 4,32 | 4,01 | 4,24 | | Clusters différents, partagé | 10,12 | 14,80 | 3,52 | 3,45 | | Même cœur physique, unique | N/A | N/A | 0,91 | N/A | | Même cluster, unique | 1,00 | 1,00 | 1,00 | 1,00 | | Clusters différents, unique | 1,00 | 1,00 | 1,00 | 1,00 | Comme les tests de stockage atomique et de comparaison et stockage atomiques utilisent tous deux le préfixe lock, leurs coûts relatifs sont identiques, et pour les mêmes raisons. Les durées relatives entre les opérations monothread et multithread utilisant une adresse partagée sont moins 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 reste deux à quatre fois plus élevé lorsque les données sont partagées sur Hercules, et jusqu’à quatorze fois plus élevé sur Jaguar. La seconde est qu’Hercules dispose d’une implémentation améliorée des opérations lock, ce qui, dans ce cas, fait que les tests sur le même cluster et sur des clusters différents prennent approximativement le même temps. Le processeur Jaguar peut être jusqu’à deux fois plus coûteux lorsque les données sont partagées entre clusters et que des opérations lock sont utilisées.

Recommandations


Vous pouvez suivre plusieurs schémas clés pour éviter de partager des données accessibles en écriture entre les cœurs :
  • Structure des données : passez d’un tableau de structures (AoS) à une structure de tableaux (SoA) dans laquelle 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 compléter une structure par un multiple de la taille d’une ligne de cache (64 octets). Cela élimine le faux partage si chaque thread opère sur une structure.
  • Comptage de références (shared_ptr, weak_ptr, etc.) : ils disposent tous deux d’une seule variable de compteur de références, partagée entre toutes les instances de l’objet.
  • Définissez très clairement la propriété des données. Lorsque des données sont transmises à un autre thread pour traitement, ce thread en devient propriétaire. Évitez toute opération sur ces données tant que ce thread n’en a pas terminé.
  • File d’attente de tâches partagée : essayez d’éviter d’utiliser une seule file d’attente de tâches partagée par plusieurs threads, en particulier si les tâches sont petites. Envisagez plutôt une file d’attente de tâches propre à chaque thread. Vous pouvez utiliser l’algorithme de vol de travail (work stealing) pour é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