2016-09-26 24 views
9

Pracuję nad projektem, w którym musimy wdrożyć algorytm sprawdzony w teorii jako przyjazny dla pamięci podręcznej. Mówiąc prościej, jeśli wejściem jest N i B jest liczbą elementów, które są przesyłane między pamięcią podręczną a pamięcią RAM za każdym razem, gdy brakuje pamięci podręcznej, algorytm będzie wymagał dostępu do pamięci RAM przez O(N/B).Dlaczego Perf i Papi podają różne wartości odniesień do pamięci podręcznej L3 i chybiają?

Chciałbym pokazać, że tak naprawdę jest to zachowanie w praktyce. Aby lepiej zrozumieć, jak można mierzyć różne liczniki sprzętowe związane z pamięcią podręczną, postanowiłem użyć różnych narzędzi. Jedna to Perf, a druga to biblioteka PAPI. Niestety im więcej pracuję z tymi narzędziami, tym mniej rozumiem, co dokładnie robią.

Używam procesora Intel (R) Core i5-3470 @ 3.20GHz z 8 GB pamięci RAM, pamięci podręcznej L1 256 KB, pamięci podręcznej L2 1 MB, pamięci podręcznej L3 6 MB. Rozmiar linii pamięci podręcznej wynosi 64 bajty. Przypuszczam, że musi to być rozmiar bloku B.

Spójrzmy na poniższy przykład:

#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; 
} 

Każdy węzeł wymaga 8 bajtów, co oznacza, że ​​linia cache zmieści 8 węzłów, więc należy oczekiwać około 1000000/8 = 125000 L3 cache tęskni.

Bez optymalizacji (bez -O3), to wyjście z 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 

Jest dość blisko tego, co oczekujemy. Teraz przypuśćmy, że używamy biblioteki 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; 
} 

Jest to wyjście, które mam:

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

Skąd taka duża różnica pomiędzy tymi dwoma narzędziami?

+0

Czy próbowałeś licząc rok do roku panien danych za pomocą PAPI_L3_DCA i PAPI_L3_DCM? – HazemGomaa

+0

tylko PAPI_L3_DCA jest dostępny i wydaje się, że daje około tej samej liczby – jsguy

Odpowiedz

6

można przejść plików źródłowych zarówno perf i PAPI, aby dowiedzieć się, do którego wydajność licznik rzeczywiście map tych wydarzeń, ale okazuje się, że są takie same (zakładając, Intel Core i tutaj): Event 2E z umask 4F dla referencji i 41 za chybione. W the Intel 64 and IA-32 Architectures Developer's Manual te wydarzenia są opisane jako:

2Eh 4FH LONGEST_LAT_CACHE.REFERENCE nalicza zdarzeń żądania pochodzące od rdzenia że odwoływać się do linii pamięci podręcznej w pamięci podręcznej ostatni poziom.

2EH 41H LONGEST_LAT_CACHE.MISS To wydarzenie zlicza każdy warunek chybienia pamięci podręcznej dla odwołań do pamięci podręcznej ostatniego poziomu.

To wydaje się być w porządku. Więc problem jest gdzie indziej.

Oto moje odtworzone liczby, tyle tylko, że zwiększyłem długość tablicy o współczynnik 100. (Zauważyłem duże wahania wyników czasowych w przeciwnym razie i przy długości 1 000 000 tablica prawie zawsze mieści się w pamięci podręcznej L3). main1 Oto twój pierwszy przykład kodu bez PAPI i main2 twój drugi z 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 

Te oczywiście nie pasują. Zobaczmy, gdzie właściwie liczymy referencje LLC. Oto kilka pierwszych linii perf report po 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 

Więc co można zobaczyć tutaj jest to, że faktycznie tylko 16,79% odniesień cache faktycznie zdarzyć w przestrzeni użytkownika, reszta to ze względu na jądro.

I tu leży problem. Porównanie tego z wynikiem PAPI jest niesprawiedliwe, ponieważ domyślnie PAPI uwzględnia tylko zdarzenia przestrzeni użytkownika. Perf jednak domyślnie zbiera zdarzenia użytkownika i jądra.

Dla perf łatwością możemy ograniczyć się tylko do użytkownika kolekcji kosmicznej:

$ 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 

Te wydają się pasuje całkiem dobrze.

Edit:

Spójrzmy nieco bliżej, co czyni jądro, tym razem z symbolami debugowania i cache strzela zamiast referencji:

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 

Jak widzimy najbardziej cache strzela faktycznie zdarzyć w clear_page_c_e. Jest to wywoływane, gdy nasz program uzyskuje dostęp do nowej strony. Jak wyjaśniono w komentarzach, nowe strony są zerowane przez jądro przed zezwoleniem na dostęp, dlatego tu już występuje brak pamięci podręcznej.

To psuje się z twoją analizą, ponieważ znaczna część braków w pamięci podręcznej może się zdarzyć w przestrzeni jądra. Nie można jednak zagwarantować, w jakich konkretnych okolicznościach jądro faktycznie uzyskuje dostęp do pamięci, co może oznaczać odchylenia od zachowania oczekiwanego przez kod.

Aby tego uniknąć, należy zbudować dodatkową pętlę wokół tablicy wypełniającej. Tylko pierwsza iteracja pętli wewnętrznej powoduje obciążenie jądra. Po uzyskaniu dostępu do każdej strony tablicy nie powinno już być wkładu. Oto mój wynik na 100 powtórzeniu pętli zewnętrznej:

$ 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 

Długość tablica była 100000000 100 iteracji i dlatego byś oczekiwać 1,250,000,000 cache tęskni autorem analizy. Teraz jest już blisko. Odchylenie to głównie od pierwszej pętli, która jest ładowana do pamięci podręcznej przez jądro podczas usuwania strony.

Z PAPI kilka dodatkowych pętle rozgrzewka może być wprowadzony przed rozpoczęciem licznik, a więc wynik pasuje do oczekiwań nawet lepiej:

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

Hmm. Widzę różnicę w liczbach, to prawda, ale co w jądrze może spowodować tak wiele braków w pamięci podręcznej? Program polega na żonglowaniu pamięcią w przestrzeni użytkownika, w moim systemie używa tych samych 55 systemów dla n równych 1000000 i n równych 100000000, jeśli nie mamy liczyć programu ładującego jedyną rzeczą, którą robi w jądrze jest mapowanie regionu pamięciowy. Może błędy strony? Ale tak duża liczba tylko dla tego? –

+2

@RomanKhimov Symbolem jądra stanowiącym największą ich część jest 'clear_page_c_e'. Myślę, że tak jest, ponieważ każda strona jest zerowana przez jądro przed przekazaniem do przestrzeni użytkownika. Prawdopodobnie nie nastąpi to w momencie przydzielania, ale raczej przy pierwszym dostępie.Mogłem się mylić. Zaktualizuję później odpowiedź, przeprowadzając bardziej szczegółową analizę. – user4407569

+0

Zapomniałem o wyzerowaniu pamięci mmAP "MAP_ANONYMOUS", prawda i to faktycznie wyjaśnia wszystko. Interesujące może być porównywanie liczb z ręcznym 'mmap()' przy użyciu 'MAP_UNINITIALIZED', które powinno również pokazywać różnicę między cache z zerowym buforowaniem a zimną niezainicjalizowaną pamięcią podręczną. –