Я знаю несколько вопросов по этой теме, однако я не видел никаких четких ответов и каких-либо контрольных измерений. Таким образом, я создал простую программу, которая работает с двумя массивами целых чисел. Первый массив a очень велик (64 МБ), а второй массив b мал, чтобы вписаться в кеш L1. Программа выполняет итерацию над a и добавляет ее элементы к соответствующим элементам b в модульном смысле (когда достигнут конец b, программа начинается с ее начала снова). Измеренные числа промахов кэша L1 для разных размеров b следующие:
Измерения проводились на процессоре Xeon E5 2680v3 Haswell типа с кэшем данных L1 объемом 32 kiB. Поэтому во всех случаях b устанавливается в кеш L1. Однако количество промахов значительно увеличилось примерно на 16 килобайт от b объема памяти. Это можно было бы ожидать, так как нагрузки как a, так и b приводят к недействительности строк кэша с начала b в этот момент.
Нет никаких оснований хранить элементы a в кеше, они используются только один раз. Поэтому я запускаю программный вариант с невременными нагрузками данных a, но количество промахов не изменилось. Я также запускаю вариант с невременной предварительной выборкой данных a, но все же с теми же результатами.
Мой контрольный код выглядит следующим образом (вариант без показания предварительной выборки):
int main(int argc, char* argv[])
{
   uint64_t* a;
   const uint64_t a_bytes = 64 * 1024 * 1024;
   const uint64_t a_count = a_bytes / sizeof(uint64_t);
   posix_memalign((void**)(&a), 64, a_bytes);
   uint64_t* b;
   const uint64_t b_bytes = atol(argv[1]) * 1024;
   const uint64_t b_count = b_bytes / sizeof(uint64_t);
   posix_memalign((void**)(&b), 64, b_bytes);
   __m256i ones = _mm256_set1_epi64x(1UL);
   for (long i = 0; i < a_count; i += 4)
       _mm256_stream_si256((__m256i*)(a + i), ones);
   // load b into L1 cache
   for (long i = 0; i < b_count; i++)
       b[i] = 0;
   int papi_events[1] = { PAPI_L1_DCM };
   long long papi_values[1];
   PAPI_start_counters(papi_events, 1);
   uint64_t* a_ptr = a;
   const uint64_t* a_ptr_end = a + a_count;
   uint64_t* b_ptr = b;
   const uint64_t* b_ptr_end = b + b_count;
   while (a_ptr < a_ptr_end) {
#ifndef NTLOAD
      __m256i aa = _mm256_load_si256((__m256i*)a_ptr);
#else
      __m256i aa = _mm256_stream_load_si256((__m256i*)a_ptr);
#endif
      __m256i bb = _mm256_load_si256((__m256i*)b_ptr);
      bb = _mm256_add_epi64(aa, bb);
      _mm256_store_si256((__m256i*)b_ptr, bb);
      a_ptr += 4;
      b_ptr += 4;
      if (b_ptr >= b_ptr_end)
         b_ptr = b;
   }
   PAPI_stop_counters(papi_values, 1);
   std::cout << "L1 cache misses: " << papi_values[0] << std::endl;
   free(a);
   free(b);
}
Интересно, поддерживают ли процессоры ЦП или поддерживают невременную загрузку/предварительную выборку или каким-либо другим способом, как маркировать некоторые данные как несобственные в кеше (например, маркировать их как LRU). Бывают ситуации, например, в HPC, где подобные сценарии распространены на практике. Например, в разреженных итеративных линейных решателях /eigensolvers матричные данные обычно очень большие (больше емкости кэша), но векторы иногда достаточно малы, чтобы вписаться в L3 или даже кэш L2. Тогда мы хотели бы сохранить их там любой ценой. К сожалению, загрузка матричных данных может привести к недействительности особенно строк x-векторного кэша, хотя в каждой итерации решателя матричные элементы используются только один раз, и нет причин держать их в кеше после их обработки.
UPDATE
Я только что сделал аналогичный эксперимент на Intel Xeon Phi KNC, измеряя время выполнения вместо промахов L1 (я не нашел способа их надежного измерения, PAPI и VTune дали странные показатели). Результаты здесь:
Оранжевая кривая представляет собой обычные нагрузки и имеет ожидаемую форму. Синяя кривая представляет нагрузку с подсказкой выключения вызова (EH), установленной в префиксе инструкций, а серая кривая представляет случай, когда каждая строка кэша a была выведена вручную; оба этих трюка, включенные KNC, очевидно, работали так, как мы хотели для b более 16 kiB. Код измеряемого контура выглядит следующим образом:
while (a_ptr < a_ptr_end) {
#ifdef NTLOAD
   __m512i aa = _mm512_extload_epi64((__m512i*)a_ptr,
      _MM_UPCONV_EPI64_NONE, _MM_BROADCAST64_NONE, _MM_HINT_NT);
#else
   __m512i aa = _mm512_load_epi64((__m512i*)a_ptr);
#endif
   __m512i bb = _mm512_load_epi64((__m512i*)b_ptr);
   bb = _mm512_or_epi64(aa, bb);
   _mm512_store_epi64((__m512i*)b_ptr, bb);
#ifdef EVICT
   _mm_clevict(a_ptr, _MM_HINT_T0);
#endif
   a_ptr += 8;
   b_ptr += 8;
   if (b_ptr >= b_ptr_end)
       b_ptr = b;
}
ОБНОВЛЕНИЕ 2
В Xeon Phi, icpc, сгенерированном для варианта стандартной загрузки (оранжевая кривая) для a_ptr:
400e93:       62 d1 78 08 18 4c 24    vprefetch0 [r12+0x80]
Когда я вручную (путем hex-редактирования исполняемого файла) изменил это значение на:
400e93:       62 d1 78 08 18 44 24    vprefetchnta [r12+0x80]
Я получил желаемые результаты, даже лучше, чем синие/серые кривые. Тем не менее, я не смог заставить компилятор генерировать для вас невременную предварительную подготовку, даже используя #pragma prefetch a_ptr:_MM_HINT_NTA перед циклом: (


