51 votes

Les architectures x86 actuelles prennent-elles en charge les charges non temporelles (de la mémoire "normale")?

Je suis conscient de multiples questions sur ce sujet, cependant, je n'ai pas vu de réponse claire ni de point de référence pour les mesures. 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 deuxième tableau, b est petit pour tenir dans le cache L1. Le programme parcourt a et ajoute ses éléments correspondants à des éléments de l' b modulables en un sens (à quand la fin de l' b est atteint, le programme commence à partir de son début). L'mesurée en nombre de L1 cache pour les différentes tailles d' b est comme suit:

enter image description here

Les mesures ont été effectuées sur un Xeon E5 2680v3 Haswell CPU de type avec 32 ko cache de données L1. Par conséquent, dans tous les cas, b intégrée dans le cache L1. Cependant, le nombre de manque a considérablement augmenté d'environ 16 kio de b empreinte mémoire. On pourrait s'y attendre puisque les charges des deux a et b des causes d'invalidation de lignes de cache à partir du début de l' b à ce stade.

Il n'y a absolument aucune raison de conserver des éléments de a dans le cache, ils sont utilisés qu'une seule fois. J'ai donc exécuter un programme variante avec des non-temporelle des charges de l' a de données, mais le nombre de manque n'a pas changé. Je dirige également une variante avec des non-temporel pré-chargement des a données, mais toujours avec les mêmes résultats.

Ma référence en matière de code est comme suit (variante w/o non-temporel pré-chargement):

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 < a_ptr_end) {
#ifndef NTLOAD
      __m256i aa = _mm256_load_si256((__m256i*)a_ptr);
#else
      __m256i aa = _mm256_stream_load_si256((__m256i*)a_ptr);
#endif
      __m256i bb = _mm256_load_si256((__m256i*)b_ptr);
      bb = _mm256_add_epi64(aa, bb);
      _mm256_store_si256((__m256i*)b_ptr, bb);

      a_ptr += 4;
      b_ptr += 4;
      if (b_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 est de savoir si le CPU fournisseurs de soutien ou sont en passe de soutien non-temporelle des charges / pré-chargement ou de toute autre manière la façon d'étiquette de certaines données comme non-être dans le cache (par exemple, de les marquer comme LRU). Il y a des situations, par exemple, dans le HPC, où les scénarios similaires sont courants dans la pratique. Par exemple, dans éparses itératif solveurs linéaires / eigensolvers, de la matrice de données sont généralement très volumineux (plus grand que le cache de capacités), mais les vecteurs sont parfois assez petit pour tenir dans L3 ou même de cache L2. Puis, nous tenons à les y maintenir à tout prix. Malheureusement, le chargement de la matrice de données peut entraîner l'invalidation de surtout x-vecteur de lignes de cache, même si dans chaque solveur itération, les éléments de matrice sont utilisés qu'une seule fois et il n'y a aucune raison de les garder dans le cache après qu'ils ont été traités.

Mise à JOUR

J'ai juste fait une expérience similaire sur un processeur Intel Xeon Phi KNC, lors de la mesure d'exécution au lieu de L1 de justesse (je n'ai pas trouver un moyen de les mesurer de façon fiable; PAPI et VTune a donné bizarre mesures.) Voici les résultats:

enter image description here

La courbe orange représente ordinaires charges et il a la forme attendue. La courbe bleue représente des charges-appel d'expulsion de l'indice (EH) définie dans l'instruction préfixe et la courbe grise représente un cas où chaque ligne de cache de a a été manuellement expulsés; ces deux astuces activé par KNC évidemment travaillé comme nous voulions pour b plus de 16 ko. Le code de la mesure de la boucle est comme suit:

while (a_ptr < a_ptr_end) {
#ifdef NTLOAD
   __m512i aa = _mm512_extload_epi64((__m512i*)a_ptr,
      _MM_UPCONV_EPI64_NONE, _MM_BROADCAST64_NONE, _MM_HINT_NT);
#else
   __m512i aa = _mm512_load_epi64((__m512i*)a_ptr);
#endif
   __m512i bb = _mm512_load_epi64((__m512i*)b_ptr);
   bb = _mm512_or_epi64(aa, bb);
   _mm512_store_epi64((__m512i*)b_ptr, bb);

#ifdef EVICT
   _mm_clevict(a_ptr, _MM_HINT_T0);
#endif

   a_ptr += 8;
   b_ptr += 8;
   if (b_ptr >= b_ptr_end)
       b_ptr = b;
}

Mise à JOUR 2

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

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

Quand j'ai manuellement (par hex-édition de l'exécutable) modifié ainsi:

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

J'ai souhaité affichage des trésors., même mieux que le bleu/gris courbes. Cependant, je n'étais pas en mesure de forcer le compilateur à générer des non-temporel prefetchnig pour moi, même en utilisant #pragma prefetch a_ptr:_MM_HINT_NTA avant la boucle :(

8voto

BeeOnRope Points 3617

Pour répondre plus précisément le titre de la question:

Oui, récente1 intégrer les Processeurs Intel de soutien non-temporelle des charges sur normal 2 mémoire - mais seulement "indirectement" par des non-temporel prefetch instructions, plutôt que d'utiliser directement la non-temporelle des instructions de chargement comme movntdqa. Ceci est en contraste à la non-temporelle des magasins où vous pouvez simplement utiliser la correspondante de non-temporelle de stocker des instructions3 directement.

L'idée de base est que vous émettez un prefetchnta de la ligne de cache avant tout aux charges normales, et puis le problème des charges que la normale. Si la ligne n'était pas déjà dans le cache, il sera chargé dans un non-temporel de la mode. La signification exacte de non-temporel de la mode dépend de l'architecture, mais la tendance générale est que la ligne est chargé dans au moins la L1 et peut-être certains plus de niveaux de cache. En effet, pour une récupération pour être d'une quelconque utilité, il doit provoquer la ligne de chargement au moins dans certains de cache de niveau de consommation par une de le charger plus tard. La ligne peut également être traitée spécialement dans le cache, par exemple en le marquant comme prioritaire pour l'expulsion ou de restreindre les façons dont il peut être placé.

Le résultat de tout cela est que, tandis que les non-temporelle des charges sont pris en charge dans un sens, ils sont vraiment seulement en partie non-temporel à la différence des magasins où vous avez vraiment ne laisser aucune trace de la ligne dans l'un des niveaux de cache. Non-temporelle des charges sera la cause de certains de la pollution du cache, mais généralement moins que les charges. Les détails exacts sont spécifiques à l'architecture, et j'ai inclus quelques détails ci-dessous pour les modernes Intel (vous pouvez en trouver un peu plus de description dans cette réponse).

Skylake Client

Sur la base des essais dans cette réponse, il semble que le comportement de prefetchnta Skylake est de récupérer normalement dans le cache L1, pour passer en L2 entièrement, et va chercher de façon limitée dans le cache L3 (probablement 1 ou 2 manières de sorte que le montant total de la L3 disponible à l' nta prélectures est limité).

Cela a été testé sur Skylake client, mais je crois que ce comportement de base probablement s'étend vers l'arrière probablement à Sandy Bridge et antérieures (basé sur le libellé de l'Intel guide d'optimisation), et transmet également à Kaby Lac et, plus tard, des architectures basées sur des Skylake client. Donc, sauf si vous utilisez un Skylake-SP ou Skylake-X, ou un très vieux CPU, c'est probablement le comportement que vous pouvez attendre d' prefetchnta.

Skylake Serveur

La récente puce Intel connu pour avoir un comportement différent est Skylake serveur (utilisé dans les Skylake-X, Skylake-SP et quelques autres lignes). Cela a considérablement changé L2 et L3 de l'architecture, et la L3 n'est plus inclusive de la beaucoup plus grande L2. Pour cette puce, il semble qu' prefetchnta saute à la fois la L2 et la L3 cache, donc, sur cette architecture, la pollution du cache est limité à la L1.

Ce comportement a été signalé par l'utilisateur Mysticial dans un commentaire. Le revers de la médaille, comme l'a souligné dans les commentaires, c'est que cela rend prefetchnta beaucoup plus fragile: si vous obtenez le prefetch de distance ou de timing (particulièrement facile lorsque l'hyperthreading, et la sœur de base est active), et les données sont expulsés de L1 avant de l'utiliser, vous allez tout le chemin du retour à la mémoire principale, plutôt que de la L3 sur les anciennes architectures.


1Récente ici signifie probablement quelque chose dans la dernière décennie ou ainsi, mais je ne veux pas laisser entendre que plus tôt le matériel ne prend pas en charge non-temporel prefetch: il est possible que le soutien va droit à l'introduction de l' prefetchnta mais je n'ai pas le matériel pour vérifier et ne peut pas trouver de source d'information fiable sur elle.

2Normal ici signifie simplement WB (écriture différée) de la mémoire, qui est la mémoire traitant au niveau de l'application de l'écrasante majorité du temps.

3 plus Précisément, le NT stocker des instructions sont - movnti pour les registres et l' movntd* et movntp* familles pour SIMD registres.

3voto

Daniel Langr Points 841

Je réponds à ma propre question car j'ai trouvé le post suivant de l'Intel Developer Forum, qui fait sens pour moi. Il a été écrit par John McCalpin:

Les résultats pour la masse, les processeurs ne sont pas surprenants -- en l'absence de véritable "bloc-notes" de mémoire, il n'est pas clair qu'il est possible de concevoir une mise en œuvre de la "non-temporel" un comportement qui n'est pas soumis à de mauvaises surprises. Deux approches ont été utilisées dans le passé sont (1) le chargement de la ligne de cache, mais en marquant LRU, au lieu de MRU, et (2) chargement de la ligne de cache dans un "set" de la set-associative cache. 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 termine la lecture.

Ces deux approches du risque de dégradation des performances dans les cas d'exploitation sur plus d'un petit nombre de tableaux, et sont beaucoup plus difficiles à mettre en œuvre sans "problèmes" lors de l'HyperThreading est considéré comme.

Dans d'autres contextes, j'ai plaidé pour la mise en œuvre de "charger plusieurs" instructions de garantir que l'intégralité du contenu d'une ligne de cache sera copié dans les registres de manière atomique. Mon raisonnement est que le matériel absolument garantit que la ligne de cache est déplacé de manière atomique et que le temps nécessaire pour copier le reste de la ligne de cache pour les registres était si petit (1-3 supplémentaires cycles, selon la génération de processeurs) qu'il pourrait être mis en œuvre en toute sécurité comme une opération atomique.

Départ avec Haswell, le noyau peut lire 64 Octets en un seul cycle (2 256 bits alignés AVX lit), de sorte que l'exposition à des effets secondaires indésirables devient encore plus faible.

En commençant par KNL, plein de cache-ligne (alignés) charges doit être "naturellement" atomique, depuis les transferts de la L1 Cache de Données de la base sont plein de lignes de cache et toutes les données sont placées dans la cible AVX-512 registre. (Cela ne signifie pas que Intel garantit l'atomicité dans la mise en œuvre! Nous n'avons pas de visibilité sur l'horrible cas du coin que les concepteurs ont à tenir compte, mais il est raisonnable de conclure que la plupart du temps aligné 512 bits des charges se fera automatiquement.) Avec cette "naturelle" de 64 Octets atomicité, certains des astuces utilisées dans le passé pour réduire la pollution du cache pour cause de "non-temporelle des charges" pourrait avoir un autre regard....


Le MOVNTDQA instruction est destiné principalement pour la lecture de plages d'adresses qui sont mappés comme "Écriture Combinant" (WC), et pas pour la lecture de la normale de la mémoire système qui est mappé "Write-Back" (WB). La description dans le Volume 2 de la SWDM dit qu'une mise en œuvre "peut" faire quelque chose de spécial avec MOVNTDQA pour WB régions, mais l'accent est mis sur le comportement pour le WC, type de mémoire.

L'Écriture Combinant" type de mémoire n'est presque jamais utilisée pour les "vrais" de la mémoire --- il est utilisé presque exclusivement pour les Mappés en Mémoire IO régions.

Cliquez ici pour voir le post entier: https://software.intel.com/en-us/forums/intel-isa-extensions/topic/597075

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X