如何正确使用预取指令?

时间:2018-02-26 18:04:32

标签: caching x86 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.

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

1 个答案:

答案 0 :(得分:11)

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

尽管如此,可能你可以通过软件预取加快速度,如果你想尝试,这里有理论和一些细节......

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

它适用于缓存行粒度 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);
}

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

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

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

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

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

我还强烈推荐这篇由5部分组成的博客文章disabling some/all of the hardware prefetchers,其中介绍了优化与您的问题非常相似的问题,并详细介绍了预取(它提供了很大的推动)。现在这是完全不同的硬件(AMD Opteron),它可能与更近期的硬件(特别是英特尔硬件,如果你正在使用的那些)有所不同 - 但改进的过程是关键,作者是该领域的专家。

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

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

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

4 请注意&#34;单位&#34;由于我计算地址的方式,PF_DIST值为PF_DIST,即16个字节。