2016-09-26 3 views
9

Je travaille sur un projet où nous devons implémenter un algorithme dont la compatibilité avec le cache a été démontrée. En termes simples, si N est l'entrée et B est le nombre d'éléments qui sont transférés entre le cache et la RAM chaque fois que nous avons manqué un cache, l'algorithme nécessitera O(N/B) accès à la RAM.Pourquoi Perf et Papi attribuent-ils des valeurs différentes aux références et échecs du cache L3?

Je voudrais montrer que c'est effectivement le comportement en pratique. Pour mieux comprendre comment on peut mesurer différents compteurs matériels liés au cache, j'ai décidé d'utiliser différents outils. L'un est Perf et l'autre est la bibliothèque PAPI. Malheureusement, plus je travaille avec ces outils, moins je comprends ce qu'ils font exactement. J'utilise un processeur Intel (R) Core (TM) i5-3470 @ 3,20 GHz avec 8 Go de RAM, cache L1 256 Ko, cache L2 1 Mo, cache L3 6 Mo. La taille de la ligne de cache est de 64 octets. Je suppose que cela doit être la taille du bloc B.

Regardons l'exemple suivant:

#include <iostream> 

using namespace std; 

struct node{ 
    int l, r; 
}; 

int main(int argc, char* argv[]){ 

    int n = 1000000; 

    node* A = new node[n]; 

    int i; 
    for(i=0;i<n;i++){ 
     A[i].l = 1; 
     A[i].r = 4; 
    } 

    return 0; 
} 

Chaque nœud nécessite 8 octets, ce qui signifie qu'une ligne de cache peut s'adapter à 8 nœuds, donc je devrais être attendre environ 1000000/8 = 125000 cache L3 misses.

Sans optimisation (pas -O3), c'est la sortie de perf:

perf stat -B -e cache-references,cache-misses ./cachetests 

Performance counter stats for './cachetests': 

     162,813  cache-references            
     142,247  cache-misses    # 87.368 % of all cache refs  

    0.007163021 seconds time elapsed 

Il est assez proche de ce que nous attendons. Supposons maintenant que nous utilisions la bibliothèque PAPI.

#include <iostream> 
#include <papi.h> 

using namespace std; 

struct node{ 
    int l, r; 
}; 

void handle_error(int err){ 
    std::cerr << "PAPI error: " << err << std::endl; 
} 

int main(int argc, char* argv[]){ 

    int numEvents = 2; 
    long long values[2]; 
    int events[2] = {PAPI_L3_TCA,PAPI_L3_TCM}; 

    if (PAPI_start_counters(events, numEvents) != PAPI_OK) 
     handle_error(1); 

    int n = 1000000; 
    node* A = new node[n]; 
    int i; 
    for(i=0;i<n;i++){ 
     A[i].l = 1; 
     A[i].r = 4; 
    } 

    if (PAPI_stop_counters(values, numEvents) != PAPI_OK) 
     handle_error(1); 

    cout<<"L3 accesses: "<<values[0]<<endl; 
    cout<<"L3 misses: "<<values[1]<<endl; 
    cout<<"L3 miss/access ratio: "<<(double)values[1]/values[0]<<endl; 

    return 0; 
} 

Ceci est la sortie que je reçois:

L3 accesses: 3335 
L3 misses: 848 
L3 miss/access ratio: 0.254273 

Pourquoi une telle différence entre les deux outils?

+0

Avez glissement annuel essayé de compter les misses de données en utilisant PAPI_L3_DCA et PAPI_L3_DCM? – HazemGomaa

+0

seulement PAPI_L3_DCA est disponible et il semble donner autour des mêmes numéros – jsguy

Répondre

6

Vous pouvez passer par les fichiers sources des deux perf et PAPI pour savoir à quelle contre-performance qu'ils carte effectivement ces événements, mais il se trouve qu'ils sont les mêmes (en supposant Intel Core i ici): l'événement 2E avec umask 4F pour les références et 41 pour les miss. Dans le the Intel 64 and IA-32 Architectures Developer's Manual ces événements sont décrits comme:

2EH 4FH LONGEST_LAT_CACHE.REFERENCE Cet événement comptabilise les demandes provenant du noyau qui font référence à une ligne de cache dans le cache de dernier niveau.

2EH 41H LONGEST_LAT_CACHE.MISS Cet événement compte chaque condition d'absence de cache pour les références au cache de dernier niveau.

Cela semble être correct. Donc le problème est ailleurs. Voici mes nombres reproduits, seulement que j'ai augmenté la longueur du tableau par un facteur de 100. (J'ai remarqué de grandes fluctuations dans les résultats de chronométrage sinon et avec une longueur de 1 000 000, le tableau rentre presque dans votre cache L3). main1 Voici votre premier exemple de code sans PAPI et main2 votre deuxième avec PAPI.

$ perf stat -e cache-references,cache-misses ./main1 

Performance counter stats for './main1': 

     27.148.932  cache-references            
     22.233.713  cache-misses    # 81,895 % of all cache refs 

     0,885166681 seconds time elapsed 

$ ./main2 
L3 accesses: 7084911 
L3 misses: 2750883 
L3 miss/access ratio: 0.388273 

Celles-ci ne correspondent évidemment pas. Voyons où nous comptons réellement les références LLC. Voici les premières lignes de perf report après perf record -e cache-references ./main1:

31,22% main1 [kernel]   [k] 0xffffffff813fdd87                                 ▒ 
    16,79% main1 main1    [.] main                                     ▒ 
    6,22% main1 [kernel]   [k] 0xffffffff8182dd24                                 ▒ 
    5,72% main1 [kernel]   [k] 0xffffffff811b541d                                 ▒ 
    3,11% main1 [kernel]   [k] 0xffffffff811947e9                                 ▒ 
    1,53% main1 [kernel]   [k] 0xffffffff811b5454                                 ▒ 
    1,28% main1 [kernel]   [k] 0xffffffff811b638a            
    1,24% main1 [kernel]   [k] 0xffffffff811b6381                                 ▒ 
    1,20% main1 [kernel]   [k] 0xffffffff811b5417                                 ▒ 
    1,20% main1 [kernel]   [k] 0xffffffff811947c9                                 ▒ 
    1,07% main1 [kernel]   [k] 0xffffffff811947ab                                 ▒ 
    0,96% main1 [kernel]   [k] 0xffffffff81194799                                 ▒ 
    0,87% main1 [kernel]   [k] 0xffffffff811947dc 

Donc ce que vous pouvez voir ici est que en réalité que 16,79% des références de cache se produit réellement dans l'espace utilisateur, le reste sont dus au noyau.

Et voilà le problème. La comparaison avec le résultat PAPI est injuste, car PAPI ne compte par défaut que les événements de l'espace utilisateur. Cependant, Perf collecte par défaut les événements de l'espace utilisateur et du noyau.

Pour nous pouvons facilement perf réduire à la collecte de l'espace utilisateur seulement:

$ perf stat -e cache-references:u,cache-misses:u ./main1 

Performance counter stats for './main1': 

     7.170.190  cache-references:u           
     2.764.248  cache-misses:u   # 38,552 % of all cache refs  

     0,658690600 seconds time elapsed 

Ceux-ci semblent correspondre assez bien.

Edit:

permet de regarder un peu plus près ce que fait le noyau, cette fois-ci avec des symboles de débogage et cache manque au lieu de références:

59,64% main1 [kernel]  [k] clear_page_c_e 
    23,25% main1 main1   [.] main 
    2,71% main1 [kernel]  [k] compaction_alloc 
    2,70% main1 [kernel]  [k] pageblock_pfn_to_page 
    2,38% main1 [kernel]  [k] get_pfnblock_flags_mask 
    1,57% main1 [kernel]  [k] _raw_spin_lock 
    1,23% main1 [kernel]  [k] clear_huge_page 
    1,00% main1 [kernel]  [k] get_page_from_freelist 
    0,89% main1 [kernel]  [k] free_pages_prepare 

Comme on le voit le plus cache Misses arrive réellement dans clear_page_c_e. Ceci est appelé quand une nouvelle page est accessible par notre programme. Comme expliqué dans les commentaires, les nouvelles pages sont mises à zéro par le noyau avant de permettre l'accès, donc le cache cache se passe déjà ici.

Cela gâte votre analyse, car une bonne partie du cache manque dans l'espace noyau. Cependant, vous ne pouvez pas garantir dans quelles circonstances exactes le noyau accède réellement à la mémoire, ce qui pourrait être une déviation du comportement attendu par votre code. Pour éviter cela, créez une boucle supplémentaire autour de votre tableau remplissant celui-ci. Seule la première itération de la boucle interne entraîne les frais généraux du noyau. Dès que toutes les pages du tableau ont été accédées, il ne devrait plus y avoir de contribution. Voici mon résultat pour 100 répétition de la boucle extérieure:

$ perf stat -e cache-references:u,cache-references:k,cache-misses:u,cache-misses:k ./main1 

Performance counter stats for './main1': 

    1.327.599.357  cache-references:u           
     23.678.135  cache-references:k           
    1.242.836.730  cache-misses:u   # 93,615 % of all cache refs  
     22.572.764  cache-misses:k   # 95,332 % of all cache refs  

     38,286354681 seconds time elapsed 

La longueur du tableau était 100.000.000 avec 100 itérations et vous aurait donc prévu 1,250,000,000 erreurs de cache par votre analyse. C'est assez proche maintenant. L'écart provient principalement de la première boucle qui est chargée dans le cache par le noyau pendant la suppression de la page.

Avec PAPI quelques supplémentaires boucles d'échauffement peuvent être insérés avant que le compteur commence, et donc le résultat convient à l'attente encore mieux:

$ ./main2 
L3 accesses: 1318699729 
L3 misses: 1250684880 
L3 miss/access ratio: 0.948423 
+0

Hmm. Je vois aussi la différence dans les nombres, c'est exact, mais qu'est-ce qui pourrait causer autant de manques dans le noyau? Le programme est tout au sujet de jongler la mémoire dans l'espace utilisateur, sur mon système il utilise les mêmes 55 syscalls pour n de 1000000 et n de 100000000, si nous ne devons pas compter le programme chargeant la seule chose qu'il fait dans le noyau est de cartographier une région de la mémoire. Erreurs de page peut-être? Mais un si grand nombre juste pour ça? –

+2

@RomanKhimov Le symbole du noyau qui en constitue la plus grande partie est 'clear_page_c_e'. Donc je pense que c'est parce que chaque page est mise à zéro par le noyau avant d'être passée à l'espace utilisateur. Cela ne se produit probablement pas au moment de l'attribution, mais plutôt au premier accès.J'aurais pu me tromper là-bas. Je mettrai à jour ma réponse plus tard avec une analyse plus détaillée. – user4407569

+0

J'ai oublié de mettre à zéro la mémoire mmaped 'MAP_ANONYMOUS', c'est vrai, et ça explique tout. Il pourrait être intéressant de comparer les nombres avec le manuel 'mmap()' en utilisant 'MAP_UNINITIALIZED', qui devrait aussi montrer la différence entre le cache réchauffé par remise à zéro et le cache non initialisé froid. –