我正在尝试向量化一个循环,计算大浮点向量的点积。我正在并行计算它,利用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()的示例和解释,何时使用什么地址和什么命中。你可以帮忙吗?
答案 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流预取程序很好地处理。还有一些你可以尝试或考虑的额外的,更硬的代码:
_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个字节。