Les architectures x86 actuelles prennent-elles en charge des charges non temporelles (à partir de la mémoire «normale»)?

Je suis au courant de plusieurs questions sur ce sujet, cependant, je n’ai vu aucune réponse claire ni aucune mesure de référence. J’ai donc créé un programme simple qui fonctionne avec deux tableaux d’entiers. Le premier tableau a est très grand (64 Mo) et le second tableau b est petit pour tenir dans le cache L1. Le programme effectue une itération sur a et ajoute ses éléments aux éléments correspondants de b dans un sens modulaire (lorsque la fin de b est atteinte, le programme recommence depuis le début). Les nombres de mesures des échecs de cache L1 pour différentes tailles de b sont les suivants:

entrer la description de l'image ici

Les mesures ont été effectuées sur un processeur de type Haswell Xeon E5 2680v3 avec un cache de données L1 de 32 kiB. Par conséquent, dans tous les cas, b installé dans le cache L1. Cependant, le nombre de ratés a considérablement augmenté d’environ 16 kiB en termes d’empreinte mémoire. On peut s’y attendre, car les charges de a et de b provoquent l’invalidation des lignes de cache à partir du début de b à ce stade.

Il n’y a absolument aucune raison de conserver des éléments d’ a cache, ils ne sont utilisés qu’une seule fois. Je lance donc une variante de programme avec des chargements non temporels de données, mais le nombre de ratés n’a pas changé. Je lance également une variante avec prélecture non temporelle de données, mais avec les mêmes résultats.

Mon code de référence est le suivant (variante sans lecture préalable non temporelle):

 int main(int argc, char* argv[]) { uint64_t* a; const uint64_t a_bytes = 64 * 1024 * 1024; const uint64_t a_count = a_bytes / sizeof(uint64_t); posix_memalign((void**)(&a), 64, a_bytes); uint64_t* b; const uint64_t b_bytes = atol(argv[1]) * 1024; const uint64_t b_count = b_bytes / sizeof(uint64_t); posix_memalign((void**)(&b), 64, b_bytes); __m256i ones = _mm256_set1_epi64x(1UL); for (long i = 0; i < a_count; i += 4) _mm256_stream_si256((__m256i*)(a + i), ones); // load b into L1 cache for (long i = 0; i < b_count; i++) b[i] = 0; int papi_events[1] = { PAPI_L1_DCM }; long long papi_values[1]; PAPI_start_counters(papi_events, 1); uint64_t* a_ptr = a; const uint64_t* a_ptr_end = a + a_count; uint64_t* b_ptr = b; const uint64_t* b_ptr_end = b + b_count; while (a_ptr = b_ptr_end) b_ptr = b; } PAPI_stop_counters(papi_values, 1); std::cout << "L1 cache misses: " << papi_values[0] << std::endl; free(a); free(b); } 

Ce que je me demande, c’est si les fournisseurs de CPU prennent en charge ou vont prendre en charge les charges / prélecture non temporelles ou toute autre manière d’étiqueter certaines données comme étant non-en attente dans le cache (par exemple, les marquer comme LRU). Il existe des situations, par exemple, dans HPC, où des scénarios similaires sont courants dans la pratique. Par exemple, dans les solveurs linéaires / eigensolvers itératifs rares, les données de masortingce sont généralement très grandes (plus grandes que les capacités de cache), mais les vecteurs sont parfois assez petits pour tenir dans le cache L3 ou même L2. Ensuite, nous aimerions les garder à tout prix. Malheureusement, le chargement des données de masortingce peut entraîner l’invalidation de lignes de cache x-vectorielles, même si, dans chaque itération de solveur, les éléments de masortingce ne sont utilisés qu’une seule fois et qu’il n’y a aucune raison de les conserver.

METTRE À JOUR

Je viens de faire une expérience similaire sur un Intel Xeon Phi KNC, tout en mesurant le temps d’exécution au lieu de L1 (je n’ai pas trouvé le moyen de les mesurer de manière fiable; PAPI et VTune donnaient des mesures étranges).

entrer la description de l'image ici

La courbe orange représente les charges ordinaires et la forme attendue. La courbe bleue représente les charges avec indice d’éviction de l’appel (EH) défini dans le préfixe d’instruction et la courbe grise représente un cas où chaque ligne de cache a été expulsée manuellement; Ces deux astuces permises par KNC ont évidemment fonctionné comme nous le voulions pour plus de 16 Ko. Le code de la boucle mesurée est le suivant:

 while (a_ptr = b_ptr_end) b_ptr = b; } 

MISE À JOUR 2

Sur Xeon Phi, icpc généré pour la variante de charge normale (courbe orange), pré- a_ptr pour a_ptr :

 400e93: 62 d1 78 08 18 4c 24 vprefetch0 [r12+0x80] 

Lorsque j’ai manuellement (en éditant l’exécutable), j’ai modifié ceci en:

 400e93: 62 d1 78 08 18 44 24 vprefetchnta [r12+0x80] 

J’ai obtenu les résultats souhaités, encore mieux que les courbes bleu / gris. Cependant, je ne suis pas parvenu à forcer le compilateur à générer de la prefetchnig non-temporelle pour moi, même en utilisant #pragma prefetch a_ptr:_MM_HINT_NTA avant la boucle 🙁

Pour répondre spécifiquement à la question du titre:

Oui , les processeurs Intel récents 1 supportent des charges non temporelles dans la mémoire normale 2 – mais seulement “indirectement” via des instructions de movntdqa non temporelles, plutôt que d’utiliser directement des instructions de charge non temporelles comme movntdqa . Ceci contraste avec les magasins non temporels où vous pouvez simplement utiliser directement les instructions de stockage non temporelles correspondantes 3 .

L’idée de base est que vous émettez une prefetchnta à la ligne de cache avant toute charge normale, puis que vous émettez les charges normalement. Si la ligne n’était pas déjà dans le cache, elle sera chargée de manière non temporelle. La signification exacte du mode non temporel dépend de l’architecture, mais le schéma général est que la ligne est chargée dans au moins le niveau de cache L1 et peut-être certains niveaux de cache supérieurs. En effet, pour qu’une lecture anticipée soit d’une quelconque utilité, il faut que la ligne soit chargée au moins dans un certain niveau de cache pour être consommée par un chargement ultérieur. La ligne peut également être traitée spécialement dans le cache, par exemple en la signalant comme hautement prioritaire pour l’expulsion ou en limitant les possibilités de placement.

Le résultat de tout ceci est que, bien que les charges non temporelles soient supscopes dans un sens, elles ne sont en réalité que partiellement non temporelles contrairement aux magasins où vous ne laissez vraiment aucune trace de la ligne dans aucun des niveaux de cache. Les charges non temporelles causeront une certaine pollution de la mémoire cache, mais généralement moins que les charges régulières. Les détails exacts sont spécifiques à l’architecture, et j’ai inclus quelques détails ci-dessous pour Intel moderne (vous pouvez trouver une version légèrement plus longue de cette réponse ).

Skylake Client

Sur la base des tests de cette réponse, il semble que le comportement de prefetchnta Skylake consiste à aller chercher normalement dans le cache L1, à ignorer complètement la L2 et à effectuer des recherches limitées dans le cache L3 (probablement de 1 ou 2 manières). la quantité totale de L3 disponible pour nta prefetches est limitée).

Cela a été testé sur le client Skylake , mais je crois que ce comportement de base va probablement vers Sandy Bridge et les versions antérieures (basées sur les termes du guide d’optimisation Intel), et également vers Kaby Lake et les architectures ultérieures basées sur le client Skylake. Donc, sauf si vous utilisez une pièce Skylake-SP ou Skylake-X, ou un processeur extrêmement ancien, c’est probablement le comportement que vous pouvez attendre de prefetchnta .

Skylake Server

La seule puce Intel récente connue pour avoir un comportement différent est le serveur Skylake (utilisé dans Skylake-X, Skylake-SP et quelques autres lignes). Cela a une architecture considérablement modifiée L2 et L3, et le L3 ne comprend plus la L2 beaucoup plus grande. Pour cette puce, il semble que prefetchnta ignore à la fois les caches L2 et L3, donc sur cette architecture, la pollution du cache est limitée à la L1.

Ce comportement a été signalé par l’utilisateur Mysticial dans un commentaire . L’inconvénient, comme indiqué dans ces commentaires, est que cela rend beaucoup plus fragile prefetchnta : si la distance de lecture anticipée ou la synchronisation est incorrecte (particulièrement facile lorsque l’hyperthreading est impliqué et que le kernel est actif), les données sont exclues de L1. vous utilisez, vous retournez à la mémoire principale plutôt qu’au L3 sur les architectures antérieures.


1 Récemment , cela signifie probablement quelque chose au cours de la dernière décennie, mais je ne veux pas dire que le matériel antérieur ne prenait pas en charge la pré-lecture non temporelle: il est possible que le support avoir le matériel nécessaire pour vérifier cela et ne pas trouver une source d’information fiable existante.

2 Normal signifie simplement WB (writeback) mémoire, qui est la mémoire traitant au niveau de l’application la grande majorité du temps.

3 Spécifiquement, les instructions du magasin NT sont movnti pour les registres à usage général et les movntd* et movntp* pour les registres SIMD.

Je réponds à ma propre question depuis que j’ai trouvé le post suivant d’Intel Developer Forum, ce qui est logique pour moi. Il a été écrit par John McCalpin:

Les résultats pour les processeurs traditionnels ne sont pas surprenants – en l’absence de véritable mémoire “scratchpad”, il n’est pas évident qu’il soit possible de concevoir une implémentation de comportement “non temporel” qui ne soit pas sujet à de mauvaises sursockets. Deux approches ont été utilisées dans le passé: (1) charger la ligne de cache, mais le marquer LRU au lieu de MRU, et (2) charger la ligne de cache dans un “ensemble” spécifique du cache associatif. Dans les deux cas, il est relativement facile de générer des situations dans lesquelles le cache supprime les données avant que le processeur ne termine sa lecture.

Ces deux approches risquent d’entraîner une dégradation des performances dans des cas impliquant plus d’un petit nombre de tableaux, et sont rendues beaucoup plus difficiles à implémenter sans les «pièges» lorsque l’hyperThreading est pris en compte.

Dans d’autres contextes, j’ai plaidé pour l’implémentation d’instructions “load multiple” qui garantiraient que le contenu entier d’une ligne de cache serait copié de manière atomique dans les registres. Mon raisonnement est que le matériel garantit absolument que la ligne de cache est déplacée de manière atomique et que le temps requirejs pour copier le rest de la ligne de cache dans les registres était si faible (1 à 3 cycles supplémentaires, selon la génération du processeur) être mis en œuvre en toute sécurité comme une opération atomique.

À partir de Haswell, le cœur peut lire 64 octets en un seul cycle (2 lectures AVX alignées sur 256 bits), de sorte que l’exposition aux effets secondaires imprévus devient encore plus faible.

À partir de KNL, les charges de ligne de cache complètes (alignées) doivent être “naturellement” atomiques, car les transferts du cache de données L1 vers le cœur sont des lignes de cache complètes et toutes les données sont placées dans le registre AVX-512 cible. (Cela ne signifie pas qu’Intel garantit l’atomicité de l’implémentation! Nous n’avons pas de visibilité sur les horribles cas de coin que les concepteurs doivent prendre en compte, mais il est raisonnable de conclure que des chargements de 512 bits alignés se produisent la plupart du temps atomiquement.) Avec cette atomicité “naturelle” de 64 octets, certaines des astuces utilisées dans le passé pour réduire la pollution de la mémoire cache due à des charges “non temporelles” peuvent mériter un autre regard ….


L’instruction MOVNTDQA est principalement destinée à la lecture des plages d’adresses mappées en tant que “Write-Combining” (WC) et non à la lecture de la mémoire système normale mappée “Write-Back” (WB). La description dans le volume 2 du SWDM indique qu’une implémentation “peut” faire quelque chose de spécial avec MOVNTDQA pour les régions WB, mais l’accent est mis sur le comportement du type de mémoire WC.

Le type de mémoire “Write-Combining” n’est presque jamais utilisé pour la “vraie” mémoire – il est utilisé presque exclusivement pour les régions IO mappées en mémoire.

Voir ici pour le post complet: https://software.intel.com/en-us/forums/intel-isa-extensions/topic/597075