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

Posted

技术标签:

【中文标题】当前的 x86 架构是不是支持非临时负载(来自“正常”内存)?【英文标题】:Do current x86 architectures support non-temporal loads (from "normal" memory)?当前的 x86 架构是否支持非临时负载(来自“正常”内存)? 【发布时间】:2017-02-27 01:22:47 【问题描述】:

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

测量是在具有 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);

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

更新

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

橙色曲线代表普通载荷,它具有预期的形状。蓝色曲线表示在指令前缀中设置了所谓的驱逐提示(EH)的负载,灰色曲线表示a的每个缓存行被手动驱逐的情况;对于超过 16 kiB 的b,KNC 启用的这两个技巧显然都像我们想要的那样工作。实测循环代码如下:

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 上,为 a_ptr 的正常负载变体(橙色曲线)预取生成 icpc

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 :(

【问题讨论】:

好东西。您能否发布或分享(例如在 GitHub 上)完整代码,包括带有预取功能的变体? @BeeOnRope:见github.com/DanielLangr/ntload 太棒了。将您的问题表述为一个问题可能是值得的。就目前而言,这只是研究,但你想知道什么问题?如果我理解正确,您想知道以下内容:“当前的 x86 架构是否支持非临时负载?”。我认为您可以省略预取部分,因为它确实包含在“加载”中 - load 数据的方法确实是为了确保它被预取。 因为我在任何地方都看不到这个链接:这个微基准的想法来自:software.intel.com/en-us/forums/intel-isa-extensions/topic/… 这很难,因为 SKL 在只运行内存绑定代码时决定自行降频,但这会影响内存带宽。 【参考方案1】:

具体回答标题问题:

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

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

所有这一切的结果是,虽然在某种意义上支持非临时加载,但它们实际上只是部分非临时加载,不像商店,你真的不会在任何地方留下任何线路的痕迹缓存级别。非临时加载会导致一些缓存污染,但通常少于常规加载。确切的细节是特定于架构的,我在下面包含了现代英特尔的一些细节(你可以找到稍长的文章in this answer)。

Skylake 客户端

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

这是在Skylake client 上测试过的,但我相信这个基本行为可能会向后延伸到 Sandy Bridge 和更早的版本(基于英特尔优化指南中的措辞),并且还会转发到基于 Skylake 客户端的 Kaby Lake 和更高版本的架构.因此,除非您使用 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。正如这些 cmets 所指出的那样,不利的一面是,这会使prefetchnta 变得更加脆弱:如果预取距离或时间错误(在涉及超线程并且同级内核处于活动状态时尤其容易),并且数据会从在使用 L1 之前,您将一直回到主内存,而不是早期架构上的 L3。


1Recent 这里可能意味着过去十年左右的任何事情,但我并不是说早期的硬件不支持非临时预取:支持可能会直接追溯到 prefetchnta 的引入,但我没有硬件来检查这一点,也找不到现有的可靠信息来源。

2Normal这里只表示WB(回写)内存,也就是绝大多数时候在应用层处理的内存。

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

【讨论】:

【参考方案2】:

我回答了我自己的问题,因为我从英特尔开发人员论坛中找到了以下帖子,这对我来说很有意义。它是由 John McCalpin 编写的:

主流处理器的结果并不令人惊讶——在没有真正的“暂存器”内存的情况下,尚不清楚是否有可能设计出一种不会令人讨厌的“非临时”行为的实现. 过去使用的两种方法是 (1) 加载缓存行,但将其标记为 LRU 而不是 MRU,以及 (2) 将缓存行加载到集合关联缓存的一个特定“集合”中。无论哪种情况,都比较容易产生缓存在处理器完成读取数据之前丢弃数据的情况。

这两种方法都存在在运行于少量阵列的情况下性能下降的风险,并且在考虑超线程时,如果没有“陷阱”,实施起来会变得更加困难。

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

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

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


MOVNTDQA 指令主要用于从映射为“Write-Combining”(WC)的地址范围读取,而不是用于从映射为“Write-Back”(WB)的普通系统内存中读取。 SWDM 第 2 卷中的描述说,实现“可能”对 WB 区域使用 MOVNTDQA 做一些特殊的事情,但重点是 WC 内存类型的行为。

“Write-Combining”内存类型几乎从不用于“真实”内存 --- 它几乎专门用于 Memory-Mapped IO 区域。

查看全文:https://software.intel.com/en-us/forums/intel-isa-extensions/topic/597075

【讨论】:

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

以上是关于当前的 x86 架构是不是支持非临时负载(来自“正常”内存)?的主要内容,如果未能解决你的问题,请参考以下文章

SOS 不支持当前目标架构

如何在 x86 上读取过时的值

关于16路及以上的X86服务器架构

非临时负载和硬件预取器,它们可以一起工作吗?

非临时负载和硬件预取器,它们可以一起工作吗?

QNX Hypervisor 2.2 用户手册1.2 支持的架构硬件和访客OS