我知道有关此主题的多个问题,但是,我没有看到任何明确的答案或基准测试。因此,我创建了一个简单的程序,该程序可以使用两个整数数组。第一个a数组很大(64 MB),第二个b数组很小,可以放入L1缓存中。该程序在a上进行迭代,并以模块化的方式将其元素添加到b的相应元素中(当到达b的末尾时,程序将从头开始。)对于不同大小的b,L1高速缓存未命中的测量数量如下:

c++ - 当前的x86体系结构是否支持非临时负载(来自 “normal”内存)?-LMLPHP

测量是在具有32 kiB L1数据高速缓存的Xeon E5 2680v3 Haswell型CPU上进行的。因此,在所有情况下,b都装入L1缓存中。但是,未命中的数量大大增加了b内存足迹约16 kiB。这是可以预期的,因为ab的加载都将导致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);
}

我想知道的是CPU供应商是否支持或将要支持非临时性加载/预取或其他任何方式将某些数据标记为不在缓存中的数据(例如,将其标记为LRU)。在某些情况下(例如,在HPC中),在实践中通常会遇到类似的情况。例如,在稀疏迭代线性求解器/本征求解器中,矩阵数据通常非常大(大于高速缓存容量),但是 vector 有时足够小以适合L3甚至L2高速缓存。然后,我们希望不惜一切代价将它们保留在那里。不幸的是,即使在每次求解器迭代中,矩阵数据的加载都可能导致特别是x vector 高速缓存行的失效,即使矩阵元素仅使用一次,也没有理由在处理它们之后将其保留在高速缓存中。

更新

我只是在Intel Xeon Phi KNC上进行了类似的实验,同时测量运行时间而不是L1丢失(我还没有找到一种方法来可靠地测量它们; PAPI和VTune提供了奇怪的指标。)结果在这里:

c&#43;&#43; - 当前的x86体系结构是否支持非临时负载(来自 “normal”内存)?-LMLPHP

橙色曲线表示普通负载,具有预期的形状。蓝色曲线表示在指令前缀中设置了所谓的逐出提示(EH)的负载,灰色曲线表示手动逐出a的每个缓存行的情况; KNC启用的这两种技巧显然都可以奏效,就像我们想要的16 kb以上b一样。被测循环的代码如下:
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]

当我手动(通过对可执行文件进行十六进制编辑)将其修改为:
400e93:       62 d1 78 08 18 44 24    vprefetchnta [r12+0x80]

我得到了理想的结果,甚至比蓝色/灰色曲线还好。但是,即使在循环前使用#pragma prefetch a_ptr:_MM_HINT_NTA,我也无法强制编译器为我生成非临时prefetchnig:

最佳答案

要专门回答标题问题:

是的,最近的1个主流Intel CPU支持正常2内存上的非临时加载-但是只能通过非临时预取指令“间接”进行,而不是直接使用movntdqa这样的非临时加载指令。这与非临时存储相反,在非临时存储中,您可以直接使用相应的非临时存储说明3。

基本思想是,在任何常规加载之前向缓存行发出prefetchnta,然后照常进行加载。如果该行尚未在缓存中,它将以非临时方式加载。非时态方式的确切含义取决于体系结构,但通常的模式是将行装载到至少L1或更高的缓存级别中。实际上,要使预取具有任何用途,就需要使该行至少加载到某个缓存级别中,以供以后的加载使用。还可以在高速缓存中对行进行特殊处理,例如,将行标记为驱逐的高优先级或限制其放置方式。

所有这一切的结果是,尽管从某种意义上说支持非临时性装载,但它们实际上只是部分非临时性装载,与存储区不同,在存储区中,您在任何高速缓存级别中都不会留下任何痕迹。非临时负载将导致某些缓存污染,但通常少于常规负载。确切的细节是特定于体系结构的,我在下面为现代英特尔提供了一些细节(您可以找到稍长的写入in this answer)。

Skylake客户

根据in this answer测试,似乎prefetchnta Skylake的行为是正常地提取到L1缓存中,完全跳过L2,并以有限的方式提取到L3缓存中(可能只提取1或2种方式,因此总数量可用于nta预取的L3的数量有限)。

这已经在Skylake client上进行了测试,但是我相信这种基本行为可能会向后扩展到Sandy Bridge和更早的版本(基于英特尔优化指南中的措辞),并且还会转发到Kaby Lake和基于Skylake客户端的更高版本的体系结构。因此,除非您使用的是Skylake-SP或Skylake-X部件或非常老的CPU,否则这可能是prefetchnta可以预期的行为。

Skylake服务器

已知最新的唯一具有不同行为的英特尔芯片是Skylake server(用于Skylake-X,Skylake-SP和其他几行)。这大大改变了L2和L3体系结构,并且L3不再包含更大的L2。对于此芯片,prefetchnta似乎同时跳过了L2和L3高速缓存,因此在此体系结构上,高速缓存污染仅限于L1。

此行为是reported by user Mysticial in a comment。那些评论中指出的不利之处在于,这会使prefetchnta变得更加脆弱:如果您获得预取距离或计时错误(特别是在涉及到超线程且同级内核处于 Activity 状态时,这特别容易),并且数据在此之前已从L1撤出使用时,将一直返回主内存,而不是早期体系结构上的L3。

1这里的“最新”可能意味着过去十年左右的时间,但是我并不是要暗示较早的硬件不支持非临时预取:支持可能回到了prefetchnta的引入,但我不支持具有检查的硬件,并且找不到有关它的现有可靠信息源。

2“正常”在这里仅表示WB(写回)内存,这是绝大多数时候在应用程序级别处理的内存。

3具体来说,NT存储指令是用于通用寄存器的movnti和用于SIMD寄存器的movntd*movntp*系列。

08-06 07:57