Рассмотрим следующий простой код:
#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
) и, наконец, циклы над массивом в режиме "только для чтения", шаг за шагом кеш-строку (64 байта) и повторяет это 64 раза, так что каждый байт получает доступ один раз.
Если мы перейдем на предварительную выборку с 1 мы ожидаем, что это достигнет 100% на заданном уровне кеша, если size
вписывается в кеш, и в основном пропустить на этом уровне в противном случае.
Меня интересуют два события: l2_lines_out.silent
и l2_lines_out.non_silent
(а также l2_trans.l2_wb
- но значения в итоге совпадают с non_silent
), который подсчитывает строки, которые молча отбрасываются из l2, а какие нет.
Если мы запустим это с 16 KiB до 1 GiB и l2_lines_in.all
эти два события (плюс l2_lines_in.all
) только для финального цикла, получим:
Здесь y -a xis - это количество событий, нормированное на количество обращений в цикле. Например, тест 16 KiB выделяет область 16 KiB и делает 16 384 доступа к этому региону, и поэтому значение 0,5 означает, что в среднем на 0,5 доступа было зарегистрировано 0,5 счета данного события.
l2_lines_in.all
ведет себя почти так, как мы ожидали. Он начинается около нуля, и когда размер превышает размер L2, он достигает 1,0 и остается там: каждый доступ приводит к строке.
Две другие линии ведут себя странно. В регионе, где тест соответствует L3 (но не в L2), выселение почти все молчат. Однако, как только регион переходит в основную память, выселения не являются безмолвными.
Что объясняет это поведение? Трудно понять, почему выселения из L2 будут зависеть от того, подходит ли базовый регион в основной памяти.
Если вы делаете резервные копии вместо загрузок, почти все - это несимвольная обратная запись, как ожидалось, поскольку значение обновления должно распространяться на внешние кеши:
Мы также можем посмотреть, на каком уровне кеша попадают обращения, используя mem_inst_retired.l1_hit
и связанные с ним события:
Если вы проигнорируете счетчики попадания L1, которые кажутся невероятно высокими в нескольких точках (более 1 L1 попало за доступ?), Результаты выглядят более или менее, как и ожидалось: в основном L2 попадает, когда регион подходит к L2, в основном L3 для области L3 (до 6 MiB на моем процессоре), а затем промахивается на DRAM после этого.
Вы можете найти код на GitHub. Подробные сведения о создании и работе можно найти в файле README.
Я наблюдал это поведение на моем клиентском процессоре i7-6700HQ для Skylake. Тот же эффект, похоже, не существует на 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.