2014-11-21 3 views
6

Recentemente ho usato perf e ho ottenuto alcuni risultati che non riesco a capire. In particolare il numero oi carichi e i negozi ritirati non corrispondono alle mie aspettative.Capire il numero di carichi e negozi ritirati in un micro-benchmark x86

feci codice molto semplice micro-benchmark per vedere se i risultati hanno senso in un caso molto semplice:

#include <stdio.h> 

#define STREAM_ARRAY_SIZE 10000000 

static double a[STREAM_ARRAY_SIZE], 
    b[STREAM_ARRAY_SIZE], 
    c[STREAM_ARRAY_SIZE]; 


int main(){ 

    ssize_t j; 

    for (j=0; j<STREAM_ARRAY_SIZE; j++) { 
     a[j] = 1.0; 
     b[j] = 2.0; 
     c[j] = 0.0; 
    } 

    return 0; 
} 

ho compilato con gcc 4.6.3:

gcc -Wall -O benchmark.c -o benchmark 

e lo fa compilare un semplice pezzo di montaggio (ottenuto con -d objdump) per i principali:

00000000004004b4 <main>: 
    4004b4: b8 00 00 00 00   mov $0x0,%eax 
    4004b9: 48 be 00 00 00 00 00 movabs $0x3ff0000000000000,%rsi 
    4004c0: 00 f0 3f 
    4004c3: 48 b9 00 00 00 00 00 movabs $0x4000000000000000,%rcx 
    4004ca: 00 00 40 
    4004cd: ba 00 00 00 00   mov $0x0,%edx 
    4004d2: 48 89 34 c5 40 10 60 mov %rsi,0x601040(,%rax,8) 
    4004d9: 00 
    4004da: 48 89 0c c5 40 c4 24 mov %rcx,0x524c440(,%rax,8) 
    4004e1: 05 
    4004e2: 48 89 14 c5 40 78 e9 mov %rdx,0x9e97840(,%rax,8) 
    4004e9: 09 
    4004ea: 48 83 c0 01    add $0x1,%rax 
    4004ee: 48 3d 80 96 98 00  cmp $0x989680,%rax 
    4004f4: 75 dc     jne 4004d2 <main+0x1e> 
    4004f6: b8 00 00 00 00   mov $0x0,%eax 
    4004fb: c3      retq 
    4004fc: 90      nop 
    4004fd: 90      nop 
    4004fe: 90      nop 
    4004ff: 90      nop 

I tre mo vs dovrebbe corrispondere al negozio a tre diversi vettori in memoria. Mi aspetto che il numero di negozi si ritiri molto vicino a 30 milioni e praticamente non carichi dal momento che sto inizializzando solo tre array. Tuttavia questo è il risultato che ottengo su una macchina Sandy Bridge:

$ perf stat -e L1-dcache-loads,L1-dcache-stores ./benchmark 

Performance counter stats for './benchmark': 

     46,017,360 L1-dcache-loads            
     75,985,205 L1-dcache-stores 

E questo è per una macchina Nehalem:

$ perf stat -e L1-dcache-loads,L1-dcache-stores ./benchmark 

Performance counter stats for './benchmark':                                                 

     45,255,731 L1-dcache-loads                                                   
     60,164,676 L1-dcache-stores 

carichi come sono in pensione e negozi hanno rappresentato tutte le operazioni MOV che colpiscono la memoria ? Come mai ci sono così tanti carichi anche se nessun dato viene effettivamente letto dalla memoria?

+0

Hai provato a ridimensionare le dimensioni della matrice e controllare come influisce sui risultati? Se non cambia, potrebbe essere solo un overhead non correlato – Leeor

+0

Il rapporto tra i carichi e lo store rimane lo stesso, quindi mi manca sicuramente qualcosa su come riconciliare i mov con carichi e negozi. – igon

+0

Come punto dati, sul mio AMD FX8350, i valori sono nell'ordine dei carichi di '10 * STREAM_ARRAY_SIZE', e solo gli archivi' STREAM_ARRAY_SIZE' (ovvero un terzo del previsto). Questo è ancora più strano :) – Jester

risposta

6

Quindi mi sono un po 'incuriosito e ho fatto qualche ricerca. Principalmente per vedere quanto sia più utile il framework perf ora dall'ultima volta che l'ho usato, ha fatto crash del kernel su una macchina condivisa e altri 25 sviluppatori erano abbastanza insoddisfatti dei miei esperimenti.

prima cosa, verificare che vedo quello che si vede:

$ cc -O -o xx xx.c && perf stat -e L1-dcache-loads,L1-dcache-stores ./xx 

Performance counter stats for './xx': 

     58,764,160 L1-dcache-loads 
     81,640,635 L1-dcache-stores 

Yup. Numeri ancora più grandi. Quindi che sta succedendo? Diamo registrare e analizzare questo un po 'meglio:

$ cc -O -o xx xx.c && perf record -e L1-dcache-loads,L1-dcache-stores ./xx 
[... blah blah ...] 
$ perf report --stdio 
[... blah blah ...] 
# Samples: 688 of event 'L1-dcache-loads' 
# Event count (approx.): 56960661 
# 
# Overhead Command  Shared Object Symbol 
# ........ ....... ................. ........ 
# 
    95.80%  xx [kernel.kallsyms] [k] 0xffffffff811176ee 
    4.20%  xx xx     [.] main 


# Samples: 656 of event 'L1-dcache-stores' 
# Event count (approx.): 80623804 
# 
# Overhead Command  Shared Object Symbol 
# ........ ....... ................. ........ 
# 
    61.72%  xx [kernel.kallsyms] [k] 0xffffffff811176ee 
    38.28%  xx xx     [.] main 

Aha, in modo che il kernel è responsabile per la maggior parte di quei carichi e negozi. I contatori di cui teniamo conto sono gli accessi alla cache che fanno sia il kernel che l'utente.

Che cosa sta succedendo è che le pagine fisiche del programma (compresi il segmento di dati e bss) non sono mappate o addirittura assegnate quando si avvia il programma. Il kernel li disturba quando li tocchi per la prima volta (o in futuro se sono stati eliminati). Possiamo vedere questo con:

$ cc -O -o foo foo.c && perf stat -e faults ./xx 

Performance counter stats for './xx': 

      58,696 faults 

In realtà stiamo eseguendo errori di pagina 58.7k solo durante quella corsa. Dato che la dimensione della pagina è 4096 byte otteniamo 58696*4096=240418816 che è approssimativamente il 240000000 byte per gli array e il resto è il programma, lo stack e tutti i tipi di posta indesiderata in libc e ld.so richiesti per l'esecuzione.

Quindi ora possiamo capire i numeri. Diamo prima un'occhiata ai negozi, perché dovrebbero essere i più facili da capire. 80623804*0.3828=30862792.1712, quindi ha senso. Ci aspettavamo 30 milioni di negozi e abbiamo ottenuto il 30,9. Dal momento che i contatori delle prestazioni si campionano e non sono perfettamente accurati, ciò è previsto. Alcuni dei carichi che il kernel ha riversato e sono stati resi conto del programma.In altre esecuzioni ho ottenuto meno di 30 milioni di conteggi per l'area utente.

Allo stesso modo in userland vengono caricati 2,4 M. Sospetto che quelli non siano effettivamente carichi in userland ma per qualche motivo alcuni accessi che il kernel fa quando ritornano dalle trappole che vengono contabilizzate nel tuo programma. O qualcosa di simile. Non ne sono sicuro, non mi piacciono, ma vediamo se possiamo rimuovere quel rumore e controllare la teoria che ha qualcosa a che fare con i dati inutili causati da errori di pagina.

Ecco una versione aggiornata del test:

#include <stdio.h> 

#define STREAM_ARRAY_SIZE 10000000 

static double a[STREAM_ARRAY_SIZE], 
    b[STREAM_ARRAY_SIZE], 
    c[STREAM_ARRAY_SIZE]; 

void 
setup(void) 
{ 
    memset(a, 0, sizeof a); 
    memset(b, 0, sizeof b); 
    memset(c, 0, sizeof c); 
} 

void 
bench(void) 
{ 
    ssize_t j; 

    for (j = 0; j < STREAM_ARRAY_SIZE; j++) { 
     a[j] = 1.0; 
     b[j] = 2.0; 
     c[j] = 0.0; 
    } 
} 

int 
main(int argc, char **argv) 
{ 
    setup(); 
    bench(); 
    return 0; 
} 

faccio in modo di ottenere tutti i page fault durante setup e poi tutti i contatori che si rovesciano durante bench dovrebbero avere molto poco rumore del kernel in loro.

$ cc -O -o xx xx.c && perf record -e faults,L1-dcache-loads,L1-dcache-stores ./xx 
[...] 
$ perf report --stdio 
[...] 
# Samples: 468 of event 'faults' 
# Event count (approx.): 58768 
# 
# Overhead Command  Shared Object    Symbol 
# ........ ....... ................. ................. 
# 
    99.20%  xx libc-2.12.so  [.] __memset_sse2 
    0.69%  xx ld-2.12.so   [.] do_lookup_x 
    0.08%  xx ld-2.12.so   [.] dl_main 
    0.02%  xx ld-2.12.so   [.] _dl_start 
    0.01%  xx ld-2.12.so   [.] _start 
    0.01%  xx [kernel.kallsyms] [k] 0xffffffff8128f75f 


# Samples: 770 of event 'L1-dcache-loads' 
# Event count (approx.): 61518838 
# 
# Overhead Command  Shared Object    Symbol 
# ........ ....... ................. ................. 
# 
    96.14%  xx [kernel.kallsyms] [k] 0xffffffff811176ee 
    3.86%  xx libc-2.12.so  [.] __memset_sse2 


# Samples: 866 of event 'L1-dcache-stores' 
# Event count (approx.): 98243116 
# 
# Overhead Command  Shared Object    Symbol 
# ........ ....... ................. ................. 
# 
    53.69%  xx [kernel.kallsyms] [k] 0xffffffff811176ee 
    30.62%  xx xx     [.] bench 
    15.69%  xx libc-2.12.so  [.] __memset_sse2 

E ce l'hai. Gli errori di pagina si sono verificati durante la chiamata a memset e alcuni durante il collegamento dinamico, il rumore che era in precedenza in main ora si verifica durante memset, bench stesso non ha carichi e circa 30 milioni di negozi. Proprio come ci aspettavamo. Una nota divertente qui è che memset sa come essere efficiente su questa macchina e ha fatto solo metà degli archivi rispetto al test per riempire la stessa quantità di memoria. "Sse2" in __memset_sse2 è un buon suggerimento su come.

Ho appena realizzato che una cosa potrebbe non essere chiara e non so dove metterla, quindi la lascio qui. I contatori delle prestazioni contano accuratamente gli eventi, ma per quanto ne so, se vuoi sapere dove avvengono quegli eventi, la CPU può generare una trappola solo una volta ogni X eventi registrati. Quindi gli strumenti non sanno esattamente dove avvengono gli eventi (sarebbe troppo lento per funzionare in quel modo), invece aspettiamo che arrivi la trappola e rendiamo conto di tutti gli eventi X a quell'istruzione/funzione. Penso, ma non sono sicuro che X sia almeno 10000. Quindi se la funzione bench tocca lo stack una sola volta e ciò accade per generare il trap Lill-dcache-load spill, dovrai considerare 10000 letture a quella lettura di lo stack. Inoltre, per quanto ne so, i fallimenti di TLB (di cui si otterrà circa 58593) nella funzione bench vengono risolti anche attraverso la cache L1 e verranno considerati a tale scopo. Quindi, qualunque cosa tu faccia, non otterrai mai esattamente i numeri che ti aspetti qui.

+0

Per ottenere memoria allocata * e commit * su Linux, è possibile 'mmap()' l'array con 'MAP_POPULATE'. Questo dovrebbe consentire di compilare il benchmark con '-O3' senza rischiare il compilatore, ottimizzando tutto. – EOF

Problemi correlati