Почему время доступа к памяти увеличивается при превышении размеров кэша ЦП - PullRequest
0 голосов
/ 04 февраля 2019

При рассмотрении проблем производительности, связанных с большим количеством обращений за пределами размеров кэша ЦП, я провел тест, который «случайным образом» увеличивает доступ к памяти при увеличении размеров блоков.Я вижу ожидаемые изменения по сравнению с размерами блоков кэша L1,2,3, но был удивлен, увидев, что время доступа продолжает уменьшаться далеко за пределы возможностей кэша.

Например, произошло сокращение времени доступа вдвое от перебивания блока 256 МБв блоке 4 ГБ.От 50 операций чтения / записи в США до 25 операций чтения / записи в США.Уменьшение продолжается до предела системной памяти.Я оставил 8 ГБ (или 4 ГБ) дополнительного для других приложений и ОС.

Кэш-память L3 составляет 8 МБ, поэтому я ожидал бы очень небольшого влияния на кэш для блоков большего размера.

В алгоритме используется примитивполиномы для «случайного» адреса каждого 64-битного слова.Это эффективно обращается к адресам довольно случайным образом, но гарантирует, что все адреса, кроме индекса 0, доступны ровно один раз за проход.После достаточного количества проходов, чтобы каждый из них занимал секунду или около того, результаты заносятся в таблицу.

Я затрудняюсь объяснить это продолжающееся уменьшение времени доступа далеко за пределы кэша.Любые объяснения?

Вот результаты с 3 разных компьютеров с Windows 10:

        | Memory block (bytes)
        |         | 64 bit words incremented per us
-- desktop I7 980 24GB --     -- Surface Book 16GB --      --HP Envy 8GB --
      128    544.80              128    948.43              128    774.22
      256    554.01              256   1034.15              256    715.50
      512    560.12              512    993.28              512    665.23
    1.02k    512.93            1.02k    944.24            1.02k    665.19
    2.05k    527.47            2.05k    947.09            2.05k    664.84
    4.10k    517.41            4.10k    931.48            4.10k    664.94
    8.19k    517.55            8.19k    939.61            8.19k    666.40
   16.38k    518.30           16.38k    941.18           16.38k    666.88
   32.77k    518.10           32.77k    938.77           32.77k    663.33
   65.54k    505.93           65.54k    889.42           65.54k    645.61
  131.07k    501.91          131.07k    855.01          131.07k    577.49
  262.14k    495.61          262.14k    882.75          262.14k    507.57
  524.29k    356.98          524.29k    774.23          524.29k    445.47
    1.05m    281.87            1.05m    695.35            1.05m    417.13
    2.10m    240.41            2.10m    650.26            2.10m    366.45
    4.19m    210.10            4.19m    229.06            4.19m    129.21
    8.39m    158.72            8.39m    114.95            8.39m     77.27
   16.78m     99.08           16.78m     84.95           16.78m     62.47
   33.55m     79.12           33.55m     60.14           33.55m     54.94
   67.11m     68.22           67.11m     34.56           67.11m     49.89
  134.22m     56.17          134.22m     22.52          134.22m     39.66
  268.44m     50.03          268.44m     23.81          268.44m     35.16
  536.87m     46.24          536.87m     39.66          536.87m     32.50
 1073.74m     43.29         1073.74m     30.33         1073.74m     25.28
 2147.48m     33.33         2147.48m     25.19         2147.48m     15.94
 4294.97m     24.85         4294.97m     10.83         4294.97m     13.18
 8589.93m     19.96         8589.93m      9.61
17179.87m     17.05

Вот код c ++:

// Memory access times for randomly distributed read/writes

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <chrono>
#include <array>

using namespace std;

// primitive polynomials over gf(2^N)
// these form simple shift registers that cycle through all possible numbers in 2^N except for 0
const array<uint32_t, 28> gf = {
    0x13, 0x25, 0x67, 0xcb,                        0x1cf, 0x233, 0x64f, 0xbb7,
    0x130f, 0x357f, 0x4f9f, 0x9e47,                0x11b2b, 0x2df4f, 0x472f3, 0xdf6af,
    0x16b04f, 0x2e0fd5, 0x611fa7, 0xa81be1,        0x11f21c7, 0x202d219, 0x67833df, 0xbc08c6b,
    0x123b83c7, 0x2dbf7ea3, 0x6268545f, 0xe6fc6257
};

int main()
{
    typedef uint64_t TestType;
    printf("        | Memory block (bytes)\n        |         | %d bit words incremented per us\n", 8 * (int)sizeof(TestType));
    TestType *const memory = new TestType[0x8000'0000u];
    for (int N = 4; N < 32-0; N++)
    {
        const uint32_t gfx = gf[N - 4];
        const uint32_t seg_size = 1 << N;
        int repCount=1+static_cast<int>(gf[25]/(static_cast<float>(seg_size)));
        fill(&memory[1], &memory[seg_size], 0);
        chrono::high_resolution_clock::time_point timerx(chrono::high_resolution_clock::now());
        for (int rep = 0; rep < repCount; rep++)
        {
            uint32_t start = 1;
            for (uint32_t i = 0; i < seg_size - 1; i++) { // cycles from 1 back to 1 includes all values except 0
                ++memory[start];
                start <<= 1;
                if (start & seg_size)
                    start ^= gfx;
            }
            if (start != 1)
            {
                cout << "ERROR\n";
                exit(-1);
            }
        }
        auto time_done = chrono::duration<double>(chrono::high_resolution_clock::now()-timerx).count();
        auto x = find_if_not(&memory[1], &memory[seg_size], [repCount](auto v) {return v == static_cast<TestType>(repCount); });
        if (x != &memory[seg_size])
        {
            printf("Failed at memory offset %lld\n", x - &memory[0]);
            return -1;
        }
        long long int blksize = 4ll << N;
        if ((sizeof(TestType) << N) < 1000)
            printf("%9.0f    %6.2f\n", 1.0*(sizeof(TestType) << N), (seg_size - 1)*repCount / (time_done * 1'000'000));
        else if ((sizeof(TestType) << N) < 1000'000)
            printf("%8.2fk    %6.2f\n", .001*(sizeof(TestType) << N), (seg_size - 1)*repCount / (time_done * 1'000'000));
        else
            printf("%8.2fm    %6.2f\n", .000001*((long long int)sizeof(TestType) << N), (seg_size - 1.)*repCount /(time_done * 1'000'000));
    }
    cout << "Done\n";
    return 0;
}

1 Ответ

0 голосов
/ 04 февраля 2019

Пропускная способность продолжает уменьшаться, поскольку время перехода по страницам увеличивается на элемент по мере увеличения общего количества элементов.То есть количество времени, затрачиваемое на заполнение TLB, не зависит от количества элементов.Вы можете наблюдать это, используя счетчик производительности DTLB_LOAD_MISSES.WALK_DURATION и другие счетчики, связанные с оборудованием для просмотра страниц.Это ожидается, потому что, когда количество обращающихся к 4К страницам увеличивается, глубина и широта таблицы страниц, которая отображает рабочий набор, также становится больше, и, следовательно, с меньшей вероятностью найти требуемые записи таблицы страниц на уровнях памяти, более близких кядро.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...