На Skylake (SKL), почему в рабочей нагрузке только для чтения есть обратные записи L2, превышающие размер L3? - PullRequest
0 голосов
/ 29 сентября 2018

Рассмотрим следующий простой код:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#include <err.h>

int cpu_ms() {
    return (int)(clock() * 1000 / CLOCKS_PER_SEC);
}

int main(int argc, char** argv) {
    if (argc < 2) errx(EXIT_FAILURE, "provide the array size in KB on the command line");

    size_t size = atol(argv[1]) * 1024;
    unsigned char *p = malloc(size);
    if (!p) errx(EXIT_FAILURE, "malloc of %zu bytes failed", size);

    int fill = argv[2] ? argv[2][0] : 'x'; 
    memset(p, fill, size);

    int startms = cpu_ms();
    printf("allocated %zu bytes at %p and set it to %d in %d ms\n", size, p, fill, startms);

    // wait until 500ms has elapsed from start, so that perf gets the read phase
    while (cpu_ms() - startms < 500) {}
    startms = cpu_ms();

    // we start measuring with perf here
    unsigned char sum = 0;
    for (size_t off = 0; off < 64; off++) {
        for (size_t i = 0; i < size; i += 64) {
            sum += p[i + off];
        }
    }

    int delta = cpu_ms() - startms;
    printf("sum was %u in %d ms \n", sum, delta);

    return EXIT_SUCCESS;
}

Это выделяет массив из size байтов (который передается в командной строке в KiB), устанавливает все байты в одно и то же значение (memset call), и, наконец, циклически перебирает массив только для чтения с шагом в одну строку кэша (64 байта) и повторяет это 64 раза, чтобы каждый байт был доступен один раз.

Если мыотключив предварительную выборку 1 , мы ожидаем, что она достигнет 100% при заданном уровне кеша, если size поместится в кеш, и в большинстве случаев пропустит этот уровень в противном случае.

Меня интересуют два события l2_lines_out.silent и l2_lines_out.non_silent (а также l2_trans.l2_wb - но значения в конечном итоге идентичны non_silent), которые подсчитывают строки, которые молча отбрасываются от l2 и

Если мы выполним это с 16 КиБ до 1 ГиБ и измерим эти два события (плюс l2_lines_in.all) только для последнего цикла, мы получим:

L2 lines in/out

Ось Y - это количество событий, нормализованное к числу обращений в цикле.Например, тест 16 КиБ выделяет область 16 КиБ и делает 16 384 доступа к этой области, и поэтому значение 0,5 означает, что в среднем 0,5 счета данного события произошло за доступ .

l2_lines_in.all ведет себя почти так, как мы ожидали.Он начинается примерно с нуля, а когда размер превышает размер L2, он поднимается до 1,0 и остается там: каждый доступ приносит строку.

Две другие строки ведут себя странно.В регионе, где тест соответствует L3 (но не в L2), выселение почти все молчат.Однако, как только регион перемещается в основную память, все выселения становятся немыми.

Чем объясняется такое поведение?Трудно понять, почему выселения из L2 будут зависеть от того, вписывается ли основной регион в основную память.

Если вы сохраняете данные вместо загрузок, почти все происходит без обратной записи, так какожидается, так как значение обновления должно быть распространено на внешние кэши:

stores

Мы также можем посмотреть, к какому уровню кеша обращаютсяс помощью mem_inst_retired.l1_hit и связанных событий:

cache hit ratios

Если вы игнорируете счетчики попаданий L1, которые кажутся невероятно высокими в пареточки (более 1 попадания L1 за доступ?), результаты выглядят более или менее ожидаемыми: в основном попадания L2, когда регион точно соответствует L2, в основном попадания L3 для области L3 (до 6 МБ на моем ЦП),и затем пропускает к DRAM.

Вы можете найти код на GitHub .Подробная информация о сборке и запуске может быть найдена в файле README.

Я наблюдал такое поведение на моем клиентском процессоре Skylake i7-6700HQ.Похоже, такого же эффекта нет в Haswell 2 .На Skylake-X поведение совершенно другое , как и ожидалось, поскольку дизайн кэша L3 изменился и стал чем-то вроде кэша жертвы для L2.


1 Вы можете сделать это на последних Intel с wrmsr -a 0x1a4 "$((2#1111))".На самом деле график почти точно такой же, как и при предварительной загрузке, поэтому его отключение в основном просто для устранения мешающего фактора.

2 См. комментарии для более подробной информации, но кратко l2_lines_out.(non_)silent там не существует, но l2_lines_out.demand_(clean|dirty), который, кажется, имеет аналогичное определение.Что еще более важно, l2_trans.l2_wb, который в основном отражает non_silent на Skylake, существует также на Haswell и, кажется, отражает demand_dirty, и это также не оказывает влияния на Haswell.

...