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:
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:
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 :(