Тест критического шага кеша процессора, дающий неожиданные результаты в зависимости от типа доступа

Вдохновленныйэтот недавний вопрос о SO и ответы, данныечто заставило меня чувствовать себя очень невежественным, я решил, что япотратить некоторое время, чтобы узнать больше оКеширование процессора и написал небольшую программу, чтобы проверить, правильно ли я все это понимаю (скорее всего, нет, яБоюсь). Я'сначала напишудопущения это лежит в основе моих ожиданий, так что вы могли бы остановить меня здесь, если это не так. На основании чего япрочитал,в общем:

nАссоциативный кэш делится наs наборы, каждый из которых содержитn строки, каждая строка имеет фиксированный размер;LКаждый адрес основной памятиA может быть сопоставлен слюбой изn строки кэшаодин задавать;Набор на какой адресA Отображение сопоставления можно найти, разбив адресное пространство на слоты, каждый из которых имеет размер в одну строку кэша, а затем вычислив индекс 'Aслот (I = A / L) и, наконец, выполнение операции по модулю для сопоставления индекса с целевым набором (TT = I % s);Задержка чтения из кеша приводит к более высокой задержке, чем пропущенная запись в кеш, потому что ЦП с меньшей вероятностью будет зависать и бездействовать, ожидая выборки строки основной памяти.

Мой первый вопрос:эти предположения верны?

Предполагая, что они есть, я попытался немного поиграть с этими понятиями, чтобы я мог на самом делеувидеть они оказывают конкретное влияние на программу. Я написал простой тест, который выделяет буфер памятиB байты и неоднократно обращаются к местоположениям этого буфера сфиксированные приращения данногошаг с начала буфера (имеется в виду, что еслиB 14 и шаг 3, я неоднократно посещаю только места 0, 3, 6, 9 и 12 - и то же самое верно, еслиB 13, 14 или 15):

int index = 0;
for (int i = 0; i < REPS; i++)
{
    index += STEP;
    if (index >= B) { index = 0; }
    buffer[index] = ...; // Do something here!
}

Из-за вышеизложенных предположений мои ожидания были следующими:

При настройкеSTEP равныйкритический шаг (то есть размер строки кэша, умноженный на число наборов в кэше, илиL * s) производительность должна бытьзначительно хуже чем когдаSTEP устанавливается, например, (L * s) + 1потому что мы будем иметь доступ только к тем областям памяти, которые отображаются втак же установить, заставляя строку кеша чаще исключаться из этого набора и приводя к более высокой частоте пропадания кеша;когдаSTEP равно критическому шагу, производительностине должны быть затронуты по размеруB буфера, если он не слишком мал (в противном случае будет посещаться слишком мало мест и будет меньше пропусков кеша); в противном случае, производительностьдолжны быть затронуты отBпотому что с большим буфером у нас больше шансов получить доступ к местоположениям, которые отображаются в разных наборах (особенно еслиSTEP не кратно 2);Производительностьпотеря должно быть хуже при чтении иза также писать в каждое расположение буферачем когда только пишу в эти местоположения: запись в ячейку памяти не должна требовать ожидания соответствующей строки, поэтому факт доступа к ячейкам памяти, которые отображаются в один и тот же набор (опять же, с использованием критического шага какSTEP) должен иметь незначительное влияние.

Так что я использовалRightMark Memory Analyzer чтобы выяснить параметры моего кэша данных L1 CPU, настроил размеры в моей программе и опробовал его. Вот так я написал основной цикл (onlyWriteToCache это флаг, который можно установить из командной строки):

    ...
    for (int i = 0; i < REPS; i++)
    {
        ...
        if (onlyWriteToCache)
        {
            buffer[index] = (char)(index % 255);
        }
        else
        {
            buffer[index] = (char)(buffer[index] % 255);
        }
    }

исход короче:

Ожидания 1) и 2) подтвердились;Ожидание 3) былоне подтверждено.

Этот факт поражает меня и заставляет думать, что я не совсем правильно понял. когдаB 256 МБ иSTEP равен критическому шагу, тест (скомпилированный с -O3 в GCC 4.7.1) показывает, что:

Версия цикла только для записи страдает от среднего~ 6х потеря производительности (6,234 с против 1,078 с);Версия цикла чтения-записи страдает от среднего~ 1.3x потеря производительности (6,671 с против 5,25 с).

Итак, мой второй вопрос:почему эта разница? Я ожидаю, что потеря производительности будет выше при чтении и записи, чем при записи.

Для полноты ниже приведена программа, которую я написал для проведения тестов, где константы отражают аппаратные параметры моей машины: размер 8-стороннего ассоциативного L1кеш данных это 32 кб и размерL каждой строки кэша составляет 64 байта, что дает в общей сложности 64 набора (ЦП имеет отдельный 8-канальный кэш инструкций L1 того же размера и с идентичным размером строки).

#include 
#include 
#include 
#include 
#include 

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

Ответы на вопрос(3)

Ваш ответ на вопрос