Inhibition du cache du processeur

Supposons que je dispose du processeur x86 standard de facto avec 3 niveaux de caches, L1 / L2 privé et L3 partagé entre les cœurs. Existe-t-il un moyen d’allouer de la mémoire partagée dont les données ne seront pas mises en cache sur les caches privés L1 / L2, mais ne le seront que sur L3? Je ne souhaite pas extraire les données de la mémoire (cela coûte trop cher), mais j’aimerais expérimenter les performances avec et sans la collecte des données partagées dans des caches privés.

L’hypothèse est que la couche L3 est partagée entre les cœurs (le cache probablement indexé physiquement) et n’entraînera donc pas de faux partage ni d’invalidation de la ligne de cache pour les données partagées fortement utilisées.

Toute solution (si elle existe) devrait être réalisée par programme, en utilisant C et / ou l’assemblage pour les processeurs basés sur Intel (architectures Xeon relativement modernes (skylake, broadwell), exécutant un système d’exploitation basé sur Linux.

Modifier:

J’ai un code sensible à la latence qui utilise une forme de mémoire partagée pour la synchronisation. Les données seront dans L3, mais lorsqu’elles seront lues ou écrites, elles iront dans L1 / L2 en fonction de la politique d’inclusivité du cache. Par implication du problème, les données devront être invalidées en ajoutant un impact de performance inutile (je pense). J’aimerais voir s’il est possible de simplement stocker les données, via une stratégie de page ou des instructions spéciales uniquement en L3.

Je sais qu’il est possible d’utiliser le registre de mémoire spécial pour empêcher la mise en cache pour des raisons de sécurité, mais cela nécessite le privilège CPL0.

Edit2:

Je traite avec des codes parallèles qui fonctionnent sur des systèmes hautes performances pendant des mois. Les systèmes sont des systèmes à grand nombre de cœurs (p. Ex. 40 à 160 + cœurs) qui effectuent périodiquement une synchronisation qui doit être exécutée en usec.

x86 n’a aucun moyen de faire un magasin qui contourne ou écrit via L1D / L2 mais pas L3. Il existe des magasins NT qui contournent tout le cache. Tout ce qui oblige une ré-écriture sur L3 force également l’écriture sur toute la mémoire. (par exemple une instruction clwb ). Celles-ci sont conçues pour les cas d’utilisation de RAM non volatile ou pour les DMA non cohérents, où il est important d’obtenir des données validées dans la RAM réelle.

Il n’y a également aucun moyen de faire une charge qui contourne L1D (sauf de la mémoire USWC avec SSE4.1 movntdqa , mais ce n’est pas “spécial” sur d’autres types de mémoire). prefetchNTA peut contourner L2, selon le manuel d’optimisation d’Intel.

La lecture anticipée sur le cœur qui effectue la lecture devrait être utile pour déclencher une réécriture de la part de l’autre kernel dans la N3 et son transfert dans votre propre N1D. Mais ce n’est utile que si vous avez l’adresse prête avant de faire le chargement. (Des dizaines de cycles pour que cela soit utile.)

Les processeurs Intel utilisent un cache L3 inclusif partagé comme protection pour la cohérence du cache sur puce. 2-socket doit surveiller l’autre socket, mais les Xeons prenant en charge plus de 2P ont des filtres de surveillance pour suivre les lignes de cache qui se déplacent.

Lorsque vous lisez une ligne récemment écrite par un autre kernel, elle est toujours non valide dans votre L1D. La balise L3 inclut les balises, et ses balises ont des informations supplémentaires permettant de suivre le kernel contenant la ligne. (Ceci est vrai même si la ligne est à l’état M dans un L1D quelque part, ce qui nécessite qu’elle soit invalide dans L3, selon le MESI normal .) Ainsi, après que votre cache-cache vérifie les balises L3, il déclenche une demande au L1 qui a la ligne pour le réécrire dans le cache L3 (et peut-être pour l’envoyer directement au kernel que voulu).

Skylake-X (Skylake-AVX512) n’a pas de L3 inclusif (il a un L2 privé plus grand et un L3 plus petit), mais il a toujours une structure incluant les balises permettant de suivre le kernel ayant une ligne. Il utilise également un maillage au lieu d’un anneau et la latence L3 semble être bien pire que Broadwell.


Peut-être utile: mappez la partie critique de la latence de votre région de mémoire partagée avec une stratégie de cache en écriture directe. IDK si ce correctif a déjà été intégré au kernel Linux principal, mais consultez ce correctif de HP: Mappage de prise en charge de l’écriture sur support sur x86 . (La politique normale est WB.)

Voir aussi: Performances de la mémoire principale et du cache d’Intel Sandy Bridge et du bulldozer AMD , parsing approfondie de la latence et de la bande passante sur le connecteur à 2 sockets, pour les lignes de cache dans différents états de départ.

Pour plus d’informations sur la bande passante mémoire sur les processeurs Intel, reportez-vous à la section Enhanced REP MOVSB ​​for memcpy , en particulier à la section Latency Bound Platforms. (N’avoir que 10 LFB limite la bande passante single-core).


En relation: quels sont les coûts de latence et de débit du partage producteur / consommateur d’un emplacement de mémoire entre hyper-frères et soeurs et non hyper-frères et soeurs? a des résultats expérimentaux pour avoir un thread thread écrit dans un emplacement alors qu’un autre thread le lit.

Notez que le cache raté n’est pas le seul effet. Vous obtenez également beaucoup de machine_clears.memory_ordering à une spéculation erronée dans le kernel effectuant la charge. (Le modèle de mémoire de x86 est fortement ordonné, mais les vrais processeurs se chargent de manière spéculative tôt et abandonnent dans les rares cas où la ligne de cache devient invalide avant que le chargement ne soit censé avoir eu lieu.

Vous ne trouverez pas de bons moyens pour désactiver l’utilisation de L1 ou de L2 pour les processeurs Intel: en effet, en dehors de quelques scénarios spécifiques tels que les zones de mémoire UC couvertes dans la réponse de Peter (qui tue vos performances étant donné qu’ils n’utilisent pas non plus la L3) , la L1 en particulier est fondamentalement impliquée dans les lectures et les écritures.

Ce que vous pouvez faire, cependant, est d’utiliser le comportement de cache relativement bien défini de L1 et L2 pour forcer les expulsions de données que vous souhaitez uniquement habiter dans L3. Sur les architectures Intel récentes, L1 et L2 se comportent toutes deux comme des caches pseudo-LRU “associatifs standard”. Par “associative standard”, j’entends la structure de cache dont vous avez parlé sur wikipedia ou dans votre cours matériel 101 où un cache est divisé en 2 ^ N ensembles qui ont M entrées (pour un cache associatif M -way) et N bits consécutifs de l’adresse sont utilisés pour rechercher l’ensemble.

Cela signifie que vous pouvez prédire exactement quelles lignes de cache se retrouveront dans le même ensemble. Par exemple, Skylake a une L1D 32K à 8 voies et une L2 à 256K. Cela signifie que les lignes de cache distantes de 64 Ko tomberont dans le même ensemble sur les L1 et L2. En règle générale, le fait que les valeurs fortement utilisées tombent dans la même ligne de cache pose un problème (les conflits de jeu de cache peuvent donner l’impression que votre cache est beaucoup plus petit qu’il ne l’est réellement), mais vous pouvez l’utiliser à votre avantage!

Lorsque vous souhaitez expulser une ligne de L1 et L2, il vous suffit de lire ou d’écrire 8 valeurs ou plus sur d’autres lignes espacées de 64 Ko de votre ligne cible. En fonction de la structure de votre repère (ou de l’application sous-jacente), vous n’aurez peut-être même pas besoin de l’écriture factice: dans votre boucle interne, vous pouvez simplement utiliser, par exemple, utiliser 16 valeurs espacées de 64 Ko et ne pas revenir à la première valeur tant que vous ne l’aurez pas visitée. l’autre 15. De cette façon, chaque ligne serait “naturellement” expulsée avant que vous ne l’utilisiez.

Notez que les écritures factices ne doivent pas obligatoirement être identiques sur chaque cœur: chaque kernel peut écrire dans des lignes fictives “privées”, de sorte que vous n’ajoutez pas de conflit pour les écritures factices.

Quelques complications:

  • Les adresses dont nous discutons ici (lorsque nous disons des choses comme “64K loin de l’adresse cible”) sont des adresses physiques . Si vous utilisez des pages 4K, vous pouvez expulser de la L1 en écrivant avec des décalages de 4K, mais pour que cela fonctionne avec L2, vous avez besoin de compensations physiques de 64K – mais vous ne pouvez pas obtenir cela de manière fiable, car chaque fois que vous traversez une page 4K limite que vous écrivez à une page physique arbitraire. Vous pouvez résoudre ce problème en vous assurant que vous utilisez des pages volumineuses de 2 Mo pour les lignes de cache concernées.
  • J’ai dit “8 ou plus ” lignes de cache doivent être lues / écrites. En effet, les caches utiliseront probablement une sorte de pseudo-LRU plutôt que le LRU exact. Vous devrez tester: vous constaterez peut-être que le pseudo-LRU fonctionne exactement comme le LRU exact pour le motif que vous utilisez, ou vous avez peut-être besoin de plus de 8 écritures pour expulser de manière fiable.

Quelques autres notes:

  • Vous pouvez utiliser les compteurs de performance exposés par perf pour déterminer la fréquence à laquelle vous frappez réellement en L1, L2 et L3, afin de vous assurer que votre tour fonctionne.
  • Le L3 n’est généralement pas un “cache associatif standard”: on recherche plutôt l’ensemble en hachant plus de bits de l’adresse qu’un cache typique. Le hachage signifie que vous n’utiliserez que quelques lignes dans la L3: votre ligne cible et vos lignes factices devraient être bien réparties autour de la L3. Si vous constatez que vous utilisez un L3 non haché, cela devrait quand même fonctionner (car le L3 est plus grand, vous allez tout de même se répandre parmi les ensembles de caches), mais vous devrez également faire plus attention aux éventuelles expulsions de L3.

Intel a récemment annoncé une nouvelle instruction qui semble être pertinente pour cette question. L’instruction s’appelle CLDEMOTE. Il déplace les données des caches de niveau supérieur vers un cache de niveau inférieur. (Probablement de L1 ou L2 à L3, bien que la spécification ne soit pas précise sur les détails.) “Cela pourrait accélérer les access ultérieurs à la ligne par d’autres cœurs …”

https://software.intel.com/sites/default/files/managed/c5/15/architecture-instruction-set-extensions-programming-reference.pdf

Je crois que vous ne devriez pas (et probablement pas) vous en soucier, et espérez que la mémoire partagée est en L3. BTW, le code C de l’ espace utilisateur s’exécute dans un espace d’adressage virtuel et vos autres cœurs peuvent (et le font souvent) exécuter un autre processus non lié .

Le matériel et la MMU (configurée par le kernel) garantissent que L3 est correctement partagé.

mais j’aimerais expérimenter les performances avec et sans la mise en cache des données partagées.

Autant que je sache (assez mal) le matériel Intel récent, cela n’est pas possible (du moins pas en mode utilisateur).

Vous pourriez peut-être envisager l’instruction machine PREFETCH et la __builtin_prefetch intégrée __builtin_prefetch GCC (qui fait l’opposé de ce que vous voulez, elle amène les données dans des caches plus proches). Voir ceci et cela .

En passant, le kernel effectue une planification préventive afin que les changements de contexte puissent avoir lieu à tout moment (souvent plusieurs centaines de fois par seconde). Lorsque (au moment du changement de contexte) un autre processus est planifié sur le même kernel, la MMU doit être reconfigurée (car chaque processus a son propre espace d’adressage virtuel et les caches sont à nouveau “froids”).

Vous pourriez être intéressé par l’ affinité du processeur . Voir sched_setaffinity (2) . Lisez à propos de Linux en temps réel . Voir sched (7) . Et voir numa (7) .

Je ne suis pas du tout sûr que la baisse de performance dont vous avez peur soit perceptible (et je pense que cela ne peut pas être évité dans l’espace utilisateur).

Vous pourriez peut-être envisager de déplacer votre code sensible dans l’espace du kernel (donc avec le privilège CPL0), mais cela nécessite probablement des mois de travail et ne vaut probablement pas la peine. Je ne vais même pas essayer.

Avez-vous envisagé d’autres approches complètement différentes (par exemple, la réécrire dans OpenCL pour votre GPGPU) dans votre code sensible à la latence?