如何正确使用预取指令?

xak*_*p35 4 x86 caching sse prefetch dot-product

我试图矢量化循环,计算大浮点矢量的点积.我正在并行计算它,利用CPU拥有大量XMM寄存器的事实,如下所示:

__m128* A, B;
__m128 dot0, dot1, dot2, dot3 = _mm_set_ps1(0);
for(size_t i=0; i<1048576;i+=4) {
    dot0 = _mm_add_ps( dot0, _mm_mul_ps( A[i+0], B[i+0]);
    dot1 = _mm_add_ps( dot1, _mm_mul_ps( A[i+1], B[i+1]);
    dot2 = _mm_add_ps( dot2, _mm_mul_ps( A[i+2], B[i+2]);
    dot3 = _mm_add_ps( dot3, _mm_mul_ps( A[i+3], B[i+3]);
}
... // add dots, then shuffle/hadd result.
Run Code Online (Sandbox Code Playgroud)

我听说使用预取指令可以帮助加速,因为它可以"在后台"获取更多数据,同时执行muls并添加缓存中的数据.但是我没有找到关于如何使用_mm_prefetch()的示例和解释,何时使用什么地址和什么命中.你可以帮忙吗?

Bee*_*ope 11

可能适用于像你这样完美线性流循环的简短答案可能是:根本不使用它们,让硬件预取器完成工作.

不过,这是可能的,你可以加快速度与软件预取,这里是理论和一些细节,如果你想尝试...

基本上你_mm_prefetch()在将来的某个时候打电话给你需要的地址.它在某些方面类似于从内存中加载一个值并且不对它做任何事情:两者都将该行带入L1高速缓存2中,但是预取内在函数(在封面下发出特定的预取指令)具有一些优点,使其适用用于预取.

它适用于缓存行粒度1:您只需要为每个缓存行发出一个预取:更多只是浪费.这意味着通常,您应该尝试展开循环,以便每个缓存行只能发出一个预取.在16字节__m128值的情况下,这意味着至少展开4(你已经完成了,所以你在那里很好).

然后简单地PF_DIST在当前计算之前预取每个访问流一段距离,例如:

for(size_t i=0; i<1048576;i+=4) {
    dot0 = _mm_add_ps( dot0, _mm_mul_ps( A[i+0], B[i+0]);
    dot1 = _mm_add_ps( dot1, _mm_mul_ps( A[i+1], B[i+1]);
    dot2 = _mm_add_ps( dot2, _mm_mul_ps( A[i+2], B[i+2]);
    dot3 = _mm_add_ps( dot3, _mm_mul_ps( A[i+3], B[i+3]);
    _mm_prefetch(A + i + PF_A_DIST, HINT_A);
    _mm_prefetch(B + i + PF_B_DIST, HINT_B);
}
Run Code Online (Sandbox Code Playgroud)

PF_[A|B]_DIST是在当前迭代之前预取的距离,HINT_是使用的时间提示.我不是试图从第一原理计算正确的距离值,而是简单地确定PF_[A|B]_DIST实验的良好值4.为了减少搜索空间,您可以先将它们设置为相等,因为逻辑上类似的距离可能是理想的.您可能会发现只预取两个流中的一个是理想的.

理想情况PF_DIST 取决于硬件配置,这一点非常重要.不仅在CPU模型上,而且在内存配置上,包括诸如多插槽系统的窥探模式之类的细节.例如,同一CPU系列的客户端和服务器芯片上的最佳值可能会大不相同.因此,您应该尽可能在您定位的实际硬件上运行调整实验.如果您针对各种硬件,您可以在所有硬件上进行测试,并希望找到一个对所有硬件都有好处的值,或者甚至考虑编译时或运行时调度,具体取决于CPU类型(并不总是如上所述)或基于CPU类型在运行时测试上.现在只是依靠硬件预取开始听起来好多了,不是吗?

你可以使用相同的方法找到最好的,HINT因为搜索空间很小(只有4个值可以尝试) - 但是你应该知道,不同提示之间的差异(特别是_MM_HINT_NTA)可能只显示为代码中的性能差异此循环之后运行,因为它们会影响与此内核无关的数据保留在缓存中.

您可能还会发现这种预取根本没有用,因为您的访问模式是完全线性的,并且可能由L2流预取程序很好地处理.还有一些你可以尝试或考虑的额外的,更硬的代码:

  • 您可以调查是否仅在4K页面边界的开头处进行预取有助于3.这将使您的循环结构复杂化:您可能需要一个嵌套循环来分隔"页面的近边缘"和"页面深处"的情况,以便仅在页面边界附近发出预取.您还希望将输入数组也进行页面对齐,否则会变得更加复杂.
  • 您可以尝试禁用部分/全部硬件预取程序.这对于整体性能来说通常很糟糕,但是通过软件预取的高度调整负载,您可以通过消除硬件预取的干扰来获得更好的性能.选择禁用预取还为您提供了一个重要的关键工具,可以帮助您了解正在发生的事情,即使您最终启用了所有预取程序.
  • 确保你使用的是大页面,因为对于像这样的大型连续块,它们是有意义的.
  • 在主计算循环的开始和结束时预取有问题:在开始时,您将错过在每个数组的开头(在初始PF_DIST窗口内)预取所有数据,并且在循环结束时您将会预取额外的数据并PF_DIST 超出数组的末尾.这些废物获取和指令带宽最多,但它们也可能导致(最终丢弃)可能影响性能的页面错误.你可以通过特殊的介绍和outro循环来解决这些问题.

我还强烈推荐这篇由5部分组成的博客文章" 优化AMD Opteron内存带宽",该文章介绍了如何优化与您的问题非常相似的问题,并详细介绍了预取(它给了大量的提升).现在这是完全不同的硬件(AMD Opteron),它可能与更近期的硬件(特别是英特尔硬件,如果你正在使用的那样)表现不同 - 但改进的过程是关键,作者是该领域的专家.


1它实际上可能工作在2-cache-line粒度之类,具体取决于它与相邻缓存行预取器的交互方式.在这种情况下,您可以通过发出预取数量的一半来逃避:每128个字节一个.

2在软件预取的情况下,您还可以使用时间提示选择其他级别的缓存.

3有一些迹象表明即使有完美的流媒体负载,并且尽管现代英特尔硬件中存在"下一页预取器",页面边界仍然是硬件预取的障碍,可以通过软件预取来部分缓解.也许是因为软件预取提供了一个更强的暗示:"是的,我将读入此页面",或者因为软件预取工作在虚拟地址级别并且必然涉及翻译机制,而L2预取在物理级别工作.

4请注意,由于我计算地址的方式,PF_DIST值的"单位" 是sizeof(__mm128)16个字节.

  • 我的印象是,DRAM 映射和细粒度内存控制器行为对于随机或半随机访问比线性访问重要得多,因为后者在默认情况下基本上运行良好,并且无需任何技巧即可实现接近 DRAM 带宽(例如,NT 商店),尽管在较旧的硬件上可能有所不同。 (2认同)