Насколько я понимаю, кеш L1 состоит в том, что выборка из памяти загружает строку кеша. Предполагая, что размер строки кэша составляет 64 байта, если я обращаюсь к памяти по адресу p
, он загружает в кеш весь блок от p
до p + 64
. Таким образом, лучше всего перебирать массив слева направо (а не справа налево), чтобы максимизировать локальность кеша.
Однако я написал пример кода C, который выделяет массив из 100 миллионов символов, записывает в него случайные значения и суммирует их (скопировано ниже для справки). Одна версия кода суммируется слева направо, а другая - справа налево. Когда я протестировал его, я получил очень похожие результаты (где «тактовые циклы» измеряются в единицах _ 4_. Код был скомпилирован без оптимизации.
Итак, мой вопрос: делают ли современные процессоры что-то еще, кроме «кэширования чтения + 64 байта»? Кэшируют ли они вперед и назад? Может ли компилятор «сказать» процессору, что код повторяется в обратном направлении?
Для справки, я работаю на Mac OS X 10.13.3
, использую gcc-7 (Homebrew GCC 7.2.0_1) 7.2.0
и процессор Intel x86-64 с линией кэша 64 байта.
Скамьи:
$ ./a.out
Backward Iterating...took 150101 clock cycles
$ ./a.out
Forward Iterating...took 146545 clock cycles
Я ожидал, что прямая итерация будет примерно в 64 раза быстрее, поскольку каждые 64 элемента должны быть попаданием в кеш, тогда как при обратной итерации каждый элемент должен быть промахом в кеше.
Итак, я назвал это cachegrind. И частота пропусков попаданий в кеш для обоих была примерно одинаковой:
# Left to right iteration
==21773==
==21773== I refs: 4,006,996,067
==21773== I1 misses: 5,183
==21773== LLi misses: 3,019
==21773== I1 miss rate: 0.00%
==21773== LLi miss rate: 0.00%
==21773==
==21773== D refs: 1,802,393,260 (1,401,627,925 rd + 400,765,335 wr)
==21773== D1 misses: 3,153,101 ( 1,588,104 rd + 1,564,997 wr)
==21773== LLd misses: 3,004,885 ( 1,440,660 rd + 1,564,225 wr)
==21773== D1 miss rate: 0.2% ( 0.1% + 0.4% )
==21773== LLd miss rate: 0.2% ( 0.1% + 0.4% )
==21773==
==21773== LL refs: 3,158,284 ( 1,593,287 rd + 1,564,997 wr)
==21773== LL misses: 3,007,904 ( 1,443,679 rd + 1,564,225 wr)
==21773== LL miss rate: 0.1% ( 0.0% + 0.4% )
# Right to left iteration
==21931==
==21931== I refs: 4,006,996,453
==21931== I1 misses: 5,198
==21931== LLi misses: 3,045
==21931== I1 miss rate: 0.00%
==21931== LLi miss rate: 0.00%
==21931==
==21931== D refs: 1,802,393,428 (1,401,628,038 rd + 400,765,390 wr)
==21931== D1 misses: 3,153,113 ( 1,588,079 rd + 1,565,034 wr)
==21931== LLd misses: 3,135,505 ( 1,571,219 rd + 1,564,286 wr)
==21931== D1 miss rate: 0.2% ( 0.1% + 0.4% )
==21931== LLd miss rate: 0.2% ( 0.1% + 0.4% )
==21931==
==21931== LL refs: 3,158,311 ( 1,593,277 rd + 1,565,034 wr)
==21931== LL misses: 3,138,550 ( 1,574,264 rd + 1,564,286 wr)
==21931== LL miss rate: 0.1% ( 0.0% + 0.4% )
Код:
#include <stdint.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#define BUF_SIZE 100000000
int main() {
srand(time(NULL));
uint8_t *buf1 = (uint8_t *)malloc(BUF_SIZE);
// Fill the buf with random data
for (size_t i = 0; i < BUF_SIZE; ++i) {
buf1[i] = rand();
}
#ifdef BACKWARDS
printf("Backward Iterating...");
#else
printf("Forward Iterating...");
#endif
uint64_t sum = 0;
clock_t start = clock();
#ifdef BACKWARDS
for (size_t i = BUF_SIZE - 1; i != ~0; --i) {
#else
for (size_t i = 0; i < BUF_SIZE; ++i) {
#endif
sum += buf1[i];
}
clock_t end = clock();
printf("took %lu clock cycles\n", end - start);
printf("sum: %llu\n", sum);
free(buf1);
}
(p & 0x3F)
до(p & 0x3F) + 0x3F
, при условии, чтоp
рассматривается какchar *
(вы не можете выполнять арифметические операции сvoid *
, кроме GCC и компиляторов, эмулирующих GCC). А это означает, что чтение в обратном направлении загружает строку кэша, когда вы начинаете с конца и работаете в обратном направлении, поэтому производительность не будет сильно отличаться от работы вперед. То, что было бы плохим в 2D-массиве, читало бы вниз по столбцам, а не по строкам; это действительно сильно ударит по кешу. - person Jonathan Leffler   schedule 14.02.2018100000000
кругов, другой считает от100000000
до нулевого цикла и доUINT_MAX
. Это не обязательно то же самое, чтоSIZE_MAX
. Не пытайтесь хитрить с поразрядными операторами. Кроме того,SIZE_MAX
не обязательно~(size_t)0
, поэтому все предположения изначально неверны. - person Lundin   schedule 14.02.2018