当前的x86架构是否支持非临时负载(来自"正常"内存)?

Dan*_*ica 45 c c++ x86 caching prefetch

我知道关于这个主题的多个问题,但是,我没有看到任何明确的答案或任何基准测量.因此,我创建了一个简单的程序,它使用两个整数数组.第一个数组a非常大(64 MB),第二个数组b很小,适合L1缓存.程序迭代a并将其元素添加到b模块化意义上的相应元素中(当到达结束时b,程序从其开始再次开始).测量的不同大小的L1缓存未命中数b如下:

在此输入图像描述

测量是在具有32 kiB L1数据高速缓存的Xeon E5 2680v3 Haswell型CPU上进行的.因此,在所有情况下,都b适合L1缓存.然而,大约16 kiB的b内存占用量大大增加了未命中数.这可能因为两者的负载预期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);
}
Run Code Online (Sandbox Code Playgroud)

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

UPDATE

我刚刚在Intel Xeon Phi KNC上进行了类似的实验,同时测量了运行时间而不是L1未命中(我还没有找到一种如何可靠地测量它们的方法; PAPI和VTune提供了奇怪的指标.)结果如下:

在此输入图像描述

橙色曲线代表普通载荷,它具有预期的形状.蓝色曲线表示在指令前缀中设置了所谓的逐出提示(EH)的负载,灰色曲线表示每个高速缓存行a被手动驱逐的情况; KNC启用的这些技巧显然可以达到我们想要的b16 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;
}
Run Code Online (Sandbox Code Playgroud)

更新2

在Xeon Phi上,icpc为正常负载变量(橙色曲线)预取生成a_ptr:

400e93:       62 d1 78 08 18 4c 24    vprefetch0 [r12+0x80]
Run Code Online (Sandbox Code Playgroud)

当我手动(通过十六进制编辑可执行文件)修改为:

400e93:       62 d1 78 08 18 44 24    vprefetchnta [r12+0x80]
Run Code Online (Sandbox Code Playgroud)

我得到了所需的结果,甚至比蓝/灰曲线更好.但是,我无法强制编译器为我生成非临时prefetchnig,即使#pragma prefetch a_ptr:_MM_HINT_NTA在循环之前使用:(

Bee*_*ope 8

具体回答标题问题:

是的,最近的1 个主流英特尔 CPU 支持普通2内存上的非临时加载- 但仅通过非临时预取指令“间接”,而不是直接使用非临时加载指令,如movntdqa. 这与非临时存储相反,在非临时存储中您可以直接使用相应的非临时存储指令3

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

所有这一切的结果是,虽然在某种意义上支持非临时加载,但它们实际上只是部分非临时加载,这与您在任何缓存级别中都没有留下任何行踪迹的存储不同。非临时加载会造成一些缓存污染,但通常比常规加载少。确切的细节是特定于架构的,我在下面包含了现代英特尔的一些细节(您可以在这个答案中找到稍长的文章)。

天湖客户端

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

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

天湖服务器

唯一已知具有不同行为的最新英特尔芯片是Skylake 服务器(用于 Skylake-X、Skylake-SP 和其他一些产品线)。这对 L2 和 L3 架构进行了相当大的更改,并且 L3 不再包含更大的 L2。对于这种芯片,似乎prefetchnta跳过两者的L2和L3高速缓存,因此在此架构缓存污染被限制为L1。

用户 Mysticial 在评论中报告了此行为。正如这些评论中所指出的,不利的prefetchnta一面是,这会变得更加脆弱:如果您获得了预取距离或时间错误(当涉及超线程并且同级核心处于活动状态时尤其容易),并且数据会在您之前从 L1 中逐出使用,您将一直返回到主内存,而不是早期架构上的 L3。


1 最近在这里可能意味着过去十年左右的任何事情,但我并不是暗示早期的硬件不支持非临时预取:支持可能会回到引入prefetchnta但我没有硬件来检查,但找不到现有的可靠信息来源。

2 Normal这里指的就是WB(writeback)内存,也就是绝大多数时间在应用层处理的内存。

3具体而言,NT 存储指令适用movnti于通用寄存器,而movntd*movntp*系列则适用于 SIMD 寄存器。


Dan*_*ica 5

我回答了我自己的问题,因为我发现了英特尔开发者论坛的以下帖子,这对我来说很有意义。它的作者是约翰·麦卡尔平:

主流处理器的结果并不令人惊讶——在没有真正的“便签本”内存的情况下,尚不清楚是否有可能设计出一种不会遭受令人讨厌的意外的“非时间”行为的实现。 过去使用的两种方法是(1)加载高速缓存行,但将其标记为LRU而不是MRU,以及(2)将高速缓存行加载到组关联高速缓存的一个特定“组”中。在任何一种情况下,都相对容易产生缓存在处理器完成读取数据之前删除数据的情况。

在对数量较多的阵列进行操作的情况下,这两种方法都会面临性能下降的风险,并且在考虑超线程时,如果没有“陷阱”,那么实施起来就会变得更加困难。

在其他上下文中,我主张实施“加载多个”指令,这将保证高速缓存行的全部内容将被原子地复制到寄存器。我的理由是,硬件绝对保证高速缓存行以原子方式移动,并且将高速缓存行的其余部分复制到寄存器所需的时间非常短(额外的 1-3 个周期,具体取决于处理器代数),以至于它可以作为原子操作安全地实现。

从 Haswell 开始,核心可以在一个周期内读取 64 字节(2 256 位对齐的 AVX 读取),因此遭受意外副作用的风险变得更低。

从 KNL 开始,全缓存行(对齐)加载应该“自然”是原子的,因为从 L1 数据缓存到核心的传输是完整缓存行,并且所有数据都放入目标 AVX-512 寄存器中。(这并不意味着英特尔保证实现中的原子性!我们无法了解设计人员必须考虑的可怕的极端情况,但可以合理地得出结论,大多数时间都会发生对齐的 512 位负载原子地。)有了这种“自然”的 64 字节原子性,过去用于减少由于“非临时”负载而导致的缓存污染的一些技巧可能值得重新审视......


MOVNTDQA 指令主要用于从映射为“写组合”(WC) 的地址范围中读取数据,而不是从映射为“写回”(WB) 的普通系统内存中读取数据。SWDM 第 2 卷中的描述表示,实现“可能”使用 MOVNTDQA 对 WB 区域执行一些特殊操作,但重点是 WC 内存类型的行为。

“写组合”内存类型几乎从不用于“真实”内存——它几乎专门用于内存映射 IO 区域。

请参阅此处查看整篇文章:https://software.intel.com/en-us/forums/intel-isa-extensions/topic/597075

  • 我认为忽略关于从 WB 内存加载“movntdqa”的 NT 提示的主要原因之一是硬件或软件预取对于性能至关重要,但没有来自了解 NT 加载并与常规流分开跟踪这些流的硬件预取器的支持流,不做任何特别的事情更有意义。所以使用`prefetchnta` + `movdqa`。(或者不要使用 `prefetchnta`;它往往是“脆弱的”。如果你的预取距离错误,你将从 L3 加载,而不是 L2。或者在 SKX 上,L3 不包含在内,如果从主内存加载,如果L1d 在你到达之前就被驱逐了。) (2认同)