问题 使用SSE内在函数将4个点产品存储到C中的连续数组中的最有效方法


我正在使用SSE内在函数为英特尔x86 Nehalem微架构优化一些代码。

我的程序的一部分计算4个点产品,并将每个结果添加到阵列的连续块中的先前值。进一步来说,

tmp0 = _mm_dp_ps(A_0m, B_0m, 0xF1);
tmp1 = _mm_dp_ps(A_1m, B_0m, 0xF2);
tmp2 = _mm_dp_ps(A_2m, B_0m, 0xF4);
tmp3 = _mm_dp_ps(A_3m, B_0m, 0xF8);

tmp0 = _mm_add_ps(tmp0, tmp1);
tmp0 = _mm_add_ps(tmp0, tmp2);
tmp0 = _mm_add_ps(tmp0, tmp3);
tmp0 = _mm_add_ps(tmp0, C_0n);

_mm_storeu_ps(C_2, tmp0);

请注意,我通过使用4个临时xmm寄存器来保存每个点积的结果。在每个xmm寄存器中,结果相对于其他临时xmm寄存器放入一个唯一的32位,这样最终结果如下所示:

tmp0 = R0-零 - 零

tmp1 =零-R1-零

tmp2 =零 - 零 - 零

tmp3 =零 - 零 - R3

我将每个tmp变量中包含的值组合成一个xmm变量,并将它们与以下说明相加:

tmp0 = _mm_add_ps(tmp0, tmp1);
tmp0 = _mm_add_ps(tmp0, tmp2);
tmp0 = _mm_add_ps(tmp0, tmp3);

最后,我将包含点积的所有4个结果的寄存器添加到数组的连续部分,以便数组的索引以点积递增,如此(C_0n是当前要更新的数组中的4个值) ; C_2是指向这4个值的地址):

tmp0 = _mm_add_ps(tmp0, C_0n);
_mm_storeu_ps(C_2, tmp0);

我想知道是否有一种更少的,更有效的方法来获取点积的结果并将它们添加到阵列的连续块中。通过这种方式,我在寄存器之间进行了3次添加,其中只有1个非零值。似乎应该有一个更有效的方法来解决这个问题。

我感谢所有的帮助。谢谢。


4997
2017-11-13 06:08


起源



答案:


对于这样的代码,我喜欢存储A和B的“转置”,以便{A_0m.x,A_1m.x,A_2m.x,A_3m.x}存储在一个向量中等等。然后你可以做只使用乘法和加法的点积,当你完成后,你在一个矢量中有所有4个点积,没有任何改组。

这在光线追踪中经常使用,以一次测试4条光线对着平面(例如,当穿过kd树时)。但是,如果您无法控制输入数据,那么进行转置的开销可能不值得。该代码也将在SSE4之前的机器上运行,尽管这可能不是问题。


关于现有代码的小效率说明:而不是这个

tmp0 = _mm_add_ps(tmp0, tmp1);
tmp0 = _mm_add_ps(tmp0, tmp2);
tmp0 = _mm_add_ps(tmp0, tmp3);
tmp0 = _mm_add_ps(tmp0, C_0n);

这样做可能稍微好一些:

tmp0 = _mm_add_ps(tmp0, tmp1);  // 0 + 1 -> 0
tmp2 = _mm_add_ps(tmp2, tmp3);  // 2 + 3 -> 2
tmp0 = _mm_add_ps(tmp0, tmp2);  // 0 + 2 -> 0
tmp0 = _mm_add_ps(tmp0, C_0n);

作为前两个 mm_add_ps现在完全独立了。另外,我不知道添加与改组的相对时间,但这可能会稍快一些。


希望有所帮助。


6
2017-11-13 09:12





也可以使用SSE3 hadd。在一些简单的测试中,它比使用_dot_ps更快。 这将返回可添加的4个点产品。

static inline __m128 dot_p(const __m128 x, const __m128 y[4])
{
   __m128 z[4];

   z[0] = x * y[0];
   z[1] = x * y[1];
   z[2] = x * y[2];
   z[3] = x * y[3];
   z[0] = _mm_hadd_ps(z[0], z[1]);
   z[2] = _mm_hadd_ps(z[2], z[3]);
   z[0] = _mm_hadd_ps(z[0], z[2]);

   return z[0];
}

3
2017-12-17 11:03





您可以尝试将点积结果保留为低位字,并使用标量存储操作 _mm_store_ss 将每个m128寄存器中的一个浮点数保存到数组的相应位置。 Nehalem的存储缓冲区应该在同一行上累积连续写入并批量刷新到L1。

专业方法是celion的转置方法。 MSVC的 _MM_TRANSPOSE4_PS 宏将为你做转置。


1
2017-11-13 09:29



您仍然需要在商店之前将旧值(C_0n)添加到每个点积。他们都是独立的,所以它可能不会太慢​​,但它不是更漂亮:) - celion


答案:


对于这样的代码,我喜欢存储A和B的“转置”,以便{A_0m.x,A_1m.x,A_2m.x,A_3m.x}存储在一个向量中等等。然后你可以做只使用乘法和加法的点积,当你完成后,你在一个矢量中有所有4个点积,没有任何改组。

这在光线追踪中经常使用,以一次测试4条光线对着平面(例如,当穿过kd树时)。但是,如果您无法控制输入数据,那么进行转置的开销可能不值得。该代码也将在SSE4之前的机器上运行,尽管这可能不是问题。


关于现有代码的小效率说明:而不是这个

tmp0 = _mm_add_ps(tmp0, tmp1);
tmp0 = _mm_add_ps(tmp0, tmp2);
tmp0 = _mm_add_ps(tmp0, tmp3);
tmp0 = _mm_add_ps(tmp0, C_0n);

这样做可能稍微好一些:

tmp0 = _mm_add_ps(tmp0, tmp1);  // 0 + 1 -> 0
tmp2 = _mm_add_ps(tmp2, tmp3);  // 2 + 3 -> 2
tmp0 = _mm_add_ps(tmp0, tmp2);  // 0 + 2 -> 0
tmp0 = _mm_add_ps(tmp0, C_0n);

作为前两个 mm_add_ps现在完全独立了。另外,我不知道添加与改组的相对时间,但这可能会稍快一些。


希望有所帮助。


6
2017-11-13 09:12





也可以使用SSE3 hadd。在一些简单的测试中,它比使用_dot_ps更快。 这将返回可添加的4个点产品。

static inline __m128 dot_p(const __m128 x, const __m128 y[4])
{
   __m128 z[4];

   z[0] = x * y[0];
   z[1] = x * y[1];
   z[2] = x * y[2];
   z[3] = x * y[3];
   z[0] = _mm_hadd_ps(z[0], z[1]);
   z[2] = _mm_hadd_ps(z[2], z[3]);
   z[0] = _mm_hadd_ps(z[0], z[2]);

   return z[0];
}

3
2017-12-17 11:03





您可以尝试将点积结果保留为低位字,并使用标量存储操作 _mm_store_ss 将每个m128寄存器中的一个浮点数保存到数组的相应位置。 Nehalem的存储缓冲区应该在同一行上累积连续写入并批量刷新到L1。

专业方法是celion的转置方法。 MSVC的 _MM_TRANSPOSE4_PS 宏将为你做转置。


1
2017-11-13 09:29



您仍然需要在商店之前将旧值(C_0n)添加到每个点积。他们都是独立的,所以它可能不会太慢​​,但它不是更漂亮:) - celion


我意识到这个问题很老,但为什么要用 _mm_add_ps 在所有?替换为:

tmp0 = _mm_or_ps(tmp0, tmp1);
tmp2 = _mm_or_ps(tmp2, tmp3);
tmp0 = _mm_or_ps(tmp0, tmp2);

你可以隐藏一些 _mm_dp_ps 潜伏。首先 _mm_or_ps 不等待最后的2个点产品,它是一个(快速)逐位操作。最后:

_mm_storeu_ps(C_2, _mm_add_ps(tmp0, C_0));

1
2017-12-22 00:41