C 内在函数、SSE2 点积和 gcc -O3 生成的程序集
Posted
技术标签:
【中文标题】C 内在函数、SSE2 点积和 gcc -O3 生成的程序集【英文标题】:C intrinsics, SSE2 dot product and gcc -O3 generated assembly 【发布时间】:2013-06-08 15:25:29 【问题描述】:我需要使用 SSE2 编写一个点积(没有 _mm_dp_ps 也没有 _mm_hadd_ps):
#include <xmmintrin.h>
inline __m128 sse_dot4(__m128 a, __m128 b)
const __m128 mult = _mm_mul_ps(a, b);
const __m128 shuf1 = _mm_shuffle_ps(mult, mult, _MM_SHUFFLE(0, 3, 2, 1));
const __m128 shuf2 = _mm_shuffle_ps(mult,mult, _MM_SHUFFLE(1, 0, 3, 2));
const __m128 shuf3 = _mm_shuffle_ps(mult,mult, _MM_SHUFFLE(2, 1, 0, 3));
return _mm_add_ss(_mm_add_ss(_mm_add_ss(mult, shuf1), shuf2), shuf3);
但我用 gcc 4.9 (experimental) -O3 查看了生成的汇编程序,我得到了:
mulps %xmm1, %xmm0
movaps %xmm0, %xmm3 //These lines
movaps %xmm0, %xmm2 //have no use
movaps %xmm0, %xmm1 //isn't it ?
shufps $57, %xmm0, %xmm3
shufps $78, %xmm0, %xmm2
shufps $147, %xmm0, %xmm1
addss %xmm3, %xmm0
addss %xmm2, %xmm0
addss %xmm1, %xmm0
ret
我想知道为什么 gcc 在 xmm1、2 和 3 中复制 xmm0... 这是我使用标志获得的代码:-march=native(看起来更好)
vmulps %xmm1, %xmm0, %xmm1
vshufps $78, %xmm1, %xmm1, %xmm2
vshufps $57, %xmm1, %xmm1, %xmm3
vshufps $147, %xmm1, %xmm1, %xmm0
vaddss %xmm3, %xmm1, %xmm1
vaddss %xmm2, %xmm1, %xmm1
vaddss %xmm0, %xmm1, %xmm0
ret
【问题讨论】:
你是在循环中调用这个函数还是真的只是在做一个 4 点乘积?如果您在循环中执行此操作,请查看以下答案:***.com/a/17001365/253056 并将_mm_hadd_ps
替换为标量代码。
我的编译器(不是 gcc)生成了同样的代码,奇怪的巧合。我没有看到任何暗示如果 SHUFPS 使用两个不同的寄存器可能会更快。也许它在旧处理器上。
我的处理器不是那么旧:i5-2450M(所以它有 sse4.2 和 avx,但它是用于“便携式”版本)。我只是在编译我给出的代码:gcc dot.c -O3 -S -o dot.s。所以不涉及循环。
编译器不注意你的处理器。代码还需要在另一台机器上运行。
@HansPassant gcc 如果你像他一样通过-march=native
【参考方案1】:
这是一个仅使用原始 SSE 指令的点积,它还可以在每个元素之间调整结果:
inline __m128 sse_dot4(__m128 v0, __m128 v1)
v0 = _mm_mul_ps(v0, v1);
v1 = _mm_shuffle_ps(v0, v0, _MM_SHUFFLE(2, 3, 0, 1));
v0 = _mm_add_ps(v0, v1);
v1 = _mm_shuffle_ps(v0, v0, _MM_SHUFFLE(0, 1, 2, 3));
v0 = _mm_add_ps(v0, v1);
return v0;
这是 5 个 SIMD 指令(而不是 7 个),但没有真正的机会隐藏延迟。任何元素都将保存结果,例如,float f = _mm_cvtss_f32(sse_dot4(a, b);
haddps
指令的延迟非常糟糕。使用 SSE3:
inline __m128 sse_dot4(__m128 v0, __m128 v1)
v0 = _mm_mul_ps(v0, v1);
v0 = _mm_hadd_ps(v0, v0);
v0 = _mm_hadd_ps(v0, v0);
return v0;
这可能会更慢,尽管它只有 3 个 SIMD 指令。如果您一次可以做多个点积,则可以在第一种情况下交错指令。 Shuffle 在最近的微架构上非常快。
【讨论】:
谢谢,不过在我的情况下,3 个 shuffle 是独立的,并且 adds 必须比 addps 快(需要再进行一个 shuffle 才能在每个元素之间调整结果)。这值得一个基准。 @matovitch "addss must be faster than addps" -addps
不慢于 @ 不是 SSE 的重点吗987654326@?【参考方案2】:
您粘贴的第一个列表仅适用于 SSE 架构。大多数 SSE 指令仅支持两种操作数语法:指令格式为a = a OP b
。
在您的代码中,a
是 mult
。因此,如果没有复制并直接传递mult
(在您的示例中为xmm0
),则其值将被覆盖,然后在剩余的_mm_shuffle_ps
指令中丢失
通过在第二个列表中传递 march=native
,您启用了 AVX 指令。 AVX 使 SSE 指令能够使用三个操作数语法:c = a OP b
。在这种情况下,不必覆盖任何源操作数,因此您不需要额外的副本。
【讨论】:
我不明白。正如你所说,mult
在xmm0
中,但第一次洗牌不会覆盖xmm0
,而是xmm3
,不是吗?
@matovitch 是的,但这只是因为您将 xmm0 复制到 xmm3 才能执行此操作。否则操作将是xmm0 = xmm0 SHUFFLE _MM_SHUFFLE(0, 3, 2, 1)
,然后您的初始值mult
就消失了。所以编译器在操作前做了一个拷贝
我明白了! Shuffle 需要 2 个 xmm 寄存器,因此它必须将 xmm0 复制到它将覆盖的寄存器中。谢谢 ! (等等……是你想说的吗?)
@matovitch 是的,除非您在通过 -march=native
时使用 AVX,就像在第二个示例中一样。
但是由于我计算了第一个坐标上的点积(见补充),我认为,这应该可以在不将 xmm0 加载到 xmm1..3 的情况下工作......也许......但编译器没有理解这一点的方法。我会试试的。【参考方案3】:
让我建议,如果您要使用 SIMD 进行点积,那么您可以尝试找到一种同时对多个向量进行操作的方法。例如,对于 SSE,如果您有四个向量,并且您想用一个固定向量进行点积,那么您可以排列数据,如 (xxxx)、(yyyy)、(zzzz)、(wwww) 并添加每个 SSE 向量并得到一次四个点积的结果。这将使您获得 100%(四倍加速)的效率,并且不仅限于 4 分量向量,它对于 n 分量向量也是 100% 的效率。这是一个仅使用 SSE 的示例。
#include <xmmintrin.h>
#include <stdio.h>
void dot4x4(float *aosoa, float *b, float *out)
__m128 vx = _mm_load_ps(&aosoa[0]);
__m128 vy = _mm_load_ps(&aosoa[4]);
__m128 vz = _mm_load_ps(&aosoa[8]);
__m128 vw = _mm_load_ps(&aosoa[12]);
__m128 brod1 = _mm_set1_ps(b[0]);
__m128 brod2 = _mm_set1_ps(b[1]);
__m128 brod3 = _mm_set1_ps(b[2]);
__m128 brod4 = _mm_set1_ps(b[3]);
__m128 dot4 = _mm_add_ps(
_mm_add_ps(_mm_mul_ps(brod1, vx), _mm_mul_ps(brod2, vy)),
_mm_add_ps(_mm_mul_ps(brod3, vz), _mm_mul_ps(brod4, vw)));
_mm_store_ps(out, dot4);
int main()
float *aosoa = (float*)_mm_malloc(sizeof(float)*16, 16);
/* initialize array to AoSoA vectors v1 =(0,1,2,3, v2 = (4,5,6,7), v3 =(8,9,10,11), v4 =(12,13,14,15) */
float a[] =
0,4,8,12,
1,5,9,13,
2,6,10,14,
3,7,11,15,
;
for (int i=0; i<16; i++) aosoa[i] = a[i];
float *out = (float*)_mm_malloc(sizeof(float)*4, 16);
float b[] = 1,1,1,1;
dot4x4(aosoa, b, out);
printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
_mm_free(aosoa);
_mm_free(out);
【讨论】:
确实,但在我的情况下,我想要一个mm_mul_ps
之后(所以我需要另一个 shuffle 或 _mm_dp_ps(a, b, 0xff)
。不过感谢您提供的出色而详细的示例。【参考方案4】:
(事实上,尽管有所有的赞成票,但在发布此问题时给出的答案 没有达到我的期望。这是我一直在等待的答案。)
SSE 指令
shufps $IMM, xmmA, xmmB
不适合
xmmB = f($IMM, xmmA)
//set xmmB with xmmA's words shuffled according to $IMM
但是作为
xmmB = f($IMM, xmmA, xmmB)
//set xmmB with 2 words of xmmA and 2 words of xmmB according to $IMM
这就是为什么需要将mulps
结果从xmm0
复制到xmm1..3
的原因。
【讨论】:
请注意,gcc 可以使用pshufd
进行复制和随机播放,但在某些 CPU 上会导致多个循环延迟(额外延迟)。在可以在寄存器重命名阶段处理 reg-reg mov
指令的 Intel IvB 及更高版本上,mov
指令具有零延迟,pshufd
将节省 movaps
,但会增加 1 个延迟周期。在 SnB 上,这将是纯粹的收益:mov 有延迟,所以你用一个周期的 mov 延迟换取一个周期的旁路延迟。
在 AMD Bulldozer 上,甚至 FP shuffle 都在 ivec
域中运行,因此对于该 CPU,使用 pshufd
并保存(零延迟)movaps
没有任何缺点。以上是关于C 内在函数、SSE2 点积和 gcc -O3 生成的程序集的主要内容,如果未能解决你的问题,请参考以下文章