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:

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

Wersja cyklu tylko do zapisu cierpi na średnią~ 6x utrata wydajności (6.234s vs 1.078s);Wersja cyklu odczytu i zapisu cierpi na średnią~ 1,3x spadek wydajności (6,671s vs 5,25s).

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.

questionAnswers(3)

yourAnswerToTheQuestion