Test krytycznego kroku pamięci podręcznej procesora dający nieoczekiwane wyniki na podstawie typu dostępu
Zainspirowany przezto ostatnie pytanie dotyczące SO i udzielonych odpowiedzi, co sprawiło, że poczułem się bardzo nieświadomy, postanowiłem poświęcić trochę czasu, aby dowiedzieć się więcej oBuforowanie procesora i napisałem mały program, aby sprawdzić, czy dobrze sobie z tym radzę (najprawdopodobniej nie, obawiam się). Najpierw zapiszęzałożenia to leży u podstaw moich oczekiwań, więc możesz mnie tutaj zatrzymać, jeśli się mylą. Na podstawie tego, co przeczytałem,ogólnie:
Nan
asocjacyjna pamięć podręczna jest podzielona nas
zestawy, każdy zawierającyn
linie, każda linia ma stały rozmiarL
;Każdy główny adres pamięciA
można je zmapowaćkażdy zn
linie pamięci podręcznejjeden zestaw;Zestaw do którego adresuA
jest mapowany można znaleźć, dzieląc przestrzeń adresową na szczeliny o rozmiarze jednej linii pamięci podręcznej, a następnie obliczając indeksA
gniazdo (I = A / L
) i na koniec wykonać operację modulo, aby odwzorować indeks na zestaw docelowyT
(T = I % s
);Brak odczytu pamięci podręcznej powoduje większe opóźnienie niż brak zapisu w pamięci podręcznej, ponieważ procesor ma mniejsze prawdopodobieństwo, że zgaśnie i pozostanie bezczynny podczas oczekiwania na pobranie głównej linii pamięci.Moje pierwsze pytanie brzmi:czy te założenia są prawidłowe?
Zakładając, że tak jest, starałem się grać trochę z tymi pojęciami, abym mógł rzeczywiściewidzieć mają one konkretny wpływ na program. Napisałem prosty test, który przydziela bufor pamięciB
bajtów i wielokrotnie uzyskuje dostęp do lokalizacji tego bufora za pomocąstałe przyrosty danegokrok od początku bufora (co oznacza, że jeśliB
ma 14 lat, a krok 3, wielokrotnie odwiedzam tylko lokalizacje 0, 3, 6, 9 i 12 - i tak samo jest, jeśliB
wynosi 13, 14 lub 15):
int index = 0;
for (int i = 0; i < REPS; i++)
{
index += STEP;
if (index >= B) { index = 0; }
buffer[index] = ...; // Do something here!
}
Ze względu na powyższe założenia moje oczekiwania były następujące:
Podczas ustawianiaSTEP
równykrok krytyczny (tj. rozmiar linii pamięci podręcznej razy liczba zestawów w pamięci podręcznej lubL * s
), wydajność powinna byćznacznie gorzej niż kiedySTEP
jest ustawiony na, na przykład, (L * s) + 1
, ponieważ uzyskalibyśmy dostęp tylko do lokalizacji pamięci, które zostaną zmapowane wpodobnie ustaw, wymuszając częstsze wyrzucanie linii pamięci podręcznej z tego zestawu, co skutkuje wyższym współczynnikiem błędów pamięci podręcznej;GdySTEP
jest równy krytycznemu krokowi, wydajnościnie powinno być naruszone według wielkościB
bufora, o ile nie jest to zbyt małe (w przeciwnym razie odwiedziłoby to zbyt mało lokalizacji i byłoby mniej błędów w pamięci podręcznej); w przeciwnym razie wydajnośćpowinien zostać naruszony przezB
, ponieważ przy większym buforze mamy większe szanse na dostęp do lokalizacji, które są mapowane na różne zestawy (zwłaszcza jeśliSTEP
nie jest wielokrotnością 2);Wydajnośćutrata powinien być gorszy podczas czytaniai piszę do każda lokalizacja buforaniż gdy piszę tylko do tych miejsc: zapisywanie do miejsca w pamięci nie powinno wymagać oczekiwania na pobranie odpowiedniej linii, więc fakt dostępu do lokalizacji pamięci, które mapują się do tego samego zestawu (ponownie, przy użyciu kroku krytycznego jakoSTEP
) powinien mieć niewielki wpływ.Więc użyłemAnalizator pamięci RightMark aby dowiedzieć się parametrów mojej pamięci podręcznej danych procesora L1, dostroiłem rozmiary w moim programie i wypróbowałem go. Tak napisałem cykl główny (onlyWriteToCache
to flaga, którą można ustawić z linii poleceń):
...
for (int i = 0; i < REPS; i++)
{
...
if (onlyWriteToCache)
{
buffer[index] = (char)(index % 255);
}
else
{
buffer[index] = (char)(buffer[index] % 255);
}
}
Thewynik w skrócie:
Oczekiwania 1) i 2) zostały potwierdzone;Oczekiwanie 3) byłonie zatwardziały.Ten fakt uderza mnie i sprawia, że myślę, że jest coś, czego nie zrobiłem całkiem dobrze. GdyB
ma 256 MB iSTEP
jest równy krytycznemu krokowi, test (skompilowany z -O3 w GCC 4.7.1) pokazuje, że:
Moje drugie pytanie brzmi:dlaczego ta różnica? Spodziewałbym się, że utrata wydajności będzie wyższa podczas czytania i pisania niż podczas pisania.
Dla kompletności poniżej znajduje się program, który napisałem do wykonywania testów, w którym stałe odzwierciedlają parametry sprzętowe mojej maszyny: rozmiar 8-kierunkowej jednostki L1pamięć podręczna danych ma 32 KB i rozmiarL
każdej linii pamięci podręcznej wynosi 64 bajty, co daje w sumie 64 zestawy (procesor ma oddzielną 8-kierunkową pamięć podręczną L1 o tym samym rozmiarze i identycznym rozmiarze linii).
#include <iostream>
#include <ctime>
#include <cstdlib>
#include <iterator>
#include <algorithm>
using namespace std;
// Auxiliary functions
constexpr int pow(int base, int exp)
{
return ((exp == 0) ? 1 : base * pow(base, exp - 1));
}
int main(int argc, char* argv[])
{
//======================================================================
// Define behavior from command-line arguments
//======================================================================
bool useCriticalStep = false;
bool onlyWriteToCache = true;
size_t BUFFER_SIZE = pow(2, 28);
size_t REPS = pow(2, 27);
if (argc > 0)
{
for (int i = 1; i < argc; i++)
{
string option = argv[i];
if (option == "-c")
{
useCriticalStep = true;
}
else if (option == "-r")
{
onlyWriteToCache = false;
}
else if (option[1] == 's')
{
string encodedSizeInMB = option.substr(2);
size_t sizeInMB = atoi(encodedSizeInMB.c_str());
BUFFER_SIZE = sizeInMB * pow(2, 20);
}
else if (option[1] == 'f')
{
string encodedNumOfReps = option.substr(2);
size_t millionsOfReps = atoi(encodedNumOfReps.c_str());
REPS = millionsOfReps * pow(10, 6);
}
}
}
//======================================================================
// Machine parameters
//======================================================================
constexpr int CACHE_SIZE = pow(2, 15);
constexpr int CACHE_LINE_SIZE = 64;
constexpr int CACHE_LINES_PER_SET = 8;
constexpr int SET_SIZE = CACHE_LINE_SIZE * CACHE_LINES_PER_SET;
constexpr int NUM_OF_SETS = CACHE_SIZE / SET_SIZE;
//======================================================================
// Print out the machine parameters
//======================================================================
cout << "CACHE SIZE: " << CACHE_SIZE / 1024 << " KB" << endl;
cout << "CACHE LINE SIZE: " << CACHE_LINE_SIZE << " bytes" << endl;
cout << "CACHE LINES PER SET: " << CACHE_LINES_PER_SET << endl;
cout << "SET SIZE: " << SET_SIZE << " bytes" << endl;
cout << "NUMBER OF SETS: " << NUM_OF_SETS << endl;
fill_n(ostream_iterator<char>(cout), 30, '='); cout << endl;
//======================================================================
// Test parameters
//======================================================================
const int STEP = NUM_OF_SETS * CACHE_LINE_SIZE + (useCriticalStep ? 0 : 1);
//======================================================================
// Print out the machine parameters
//======================================================================
cout << "BUFFER SIZE: " << BUFFER_SIZE / pow(2, 20) << " MB" << endl;
cout << "STEP SIZE: " << STEP << " bytes" << endl;
cout << "NUMBER OF REPS: " << REPS << endl;
fill_n(ostream_iterator<char>(cout), 30, '='); cout << endl;
//======================================================================
// Start the test
//======================================================================
char* buffer = new char[BUFFER_SIZE];
clock_t t1 = clock();
int index = 0;
for (size_t i = 0; i < REPS; i++)
{
index += STEP;
if (index >= BUFFER_SIZE)
{
index = 0;
}
if (onlyWriteToCache)
{
buffer[index] = (char)(index % 255);
}
else
{
buffer[index] = (char)(buffer[index] % 255);
}
}
clock_t t2 = clock();
//======================================================================
// Print the execution time (in clock ticks) and cleanup resources
//======================================================================
float executionTime = (float)(t2 - t1) / CLOCKS_PER_SEC;
cout << "EXECUTION TIME: " << executionTime << "s" << endl;
delete[] buffer;
}
Z góry dziękuję, jeśli udało się przeczytać to długie pytanie.