Рассмотрим следующий простой код:
#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
) только для последнего цикла, мы получим:
Ось Y - это количество событий, нормализованное к числу обращений в цикле.Например, тест 16 КиБ выделяет область 16 КиБ и делает 16 384 доступа к этой области, и поэтому значение 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 МБ на моем ЦП),и затем пропускает к 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.