你有一个三(或四)个浮点数的向量。总结它们的最快方法是什么?
SSE(movaps,shuffle,add,movd)总是比x87快吗? SSE4.2中的水平加法说明值得吗?移动到FPU的成本是多少,然后是faddp,faddp?什么是最快的特定指令序列?
“尝试安排事情,这样你一次可以总结四个向量”将不被接受为答案。 : - )
答案 0 :(得分:64)
以下是根据Agner Fog's microarch guide微型指南和说明表调整的一些版本。另请参阅x86标记wiki。它们应该在任何CPU上都很有效,没有主要的瓶颈。 (例如,我避免了一些可以帮助一些人,但在另一个uarch上缓慢的事情)。代码大小也被最小化。
常见的2x hadd
成语仅适用于代码大小,而不适用于任何现有CPU。它有用例(见下文),但这不是其中之一。
我还包括AVX版本。使用AVX / AVX2进行任何类型的水平缩小都应以vextractf128
和"垂直"开头。减少一个XMM(__m128
)向量的操作。
查看所有代码on the Godbolt Compiler Explorer的asm输出。另请参阅我对Agner Fog's C++ Vector Class Library horizontal_add
函数的改进。 (message board thread和github上的代码)。我使用CPP宏为SSE2,SSE4和AVX的代码大小选择最佳shuffle,并在AVX不可用时避免movdqa
。
需要考虑权衡:
haddps
,因此这里非常重要。横向添加不常见:
没有uop-cache 的CPU 可能会支持2x haddps
:它运行时会变慢,但这种情况并不常见。只有2个指令可以最大限度地减少对周围代码的影响(I $ size)。
带有uop-cache 的CPU 可能会支持更少uops的东西,即使它有更多指令/ x86代码大小。使用的总uops缓存行是我们想要最小化的,这不是最小化总uop(采用分支和32B边界总是启动新的uop缓存行)。
无论如何,说到这里,横向总和出现了很多,所以我试图精心制作一些编译得很好的版本。没有在任何真实硬件上进行基准测试,甚至没有仔细测试。在随机常量或其他东西中可能存在错误。
如果您正在制作代码的后备/基线版本,请记住只有旧CPU会运行它;较新的CPU将运行您的AVX版本,或SSE4.1或其他任何。
像K8和Core2(merom)这样的旧CPU只有64位shuffle单位。 Core2有大多数指令的128位执行单元,但不适用于shuffle。 (Pentium M和K8将所有128b向量指令作为两个64位半部处理)。
像movhlps
那样以64位块移动数据(不会在64位半场内移动)的随机播放也很快。
在缓慢洗牌的旧CPU上:
movhlps
(Merom:1uop)明显快于shufps
(Merom:3uops)。在Pentium-M上,比movaps
便宜。此外,它在Core2上的FP域中运行,避免了其他shuffle的旁路延迟。unpcklpd
比unpcklps
快。pshufd
速度很慢,pshuflw
/ pshufhw
速度很快(因为它们只会将64位一半洗牌)pshufb mm0
(MMX)很快,pshufb xmm0
很慢。haddps
非常慢(Merom和Pentium M上6uops)movshdup
(Merom:1uop)很有意思:它是唯一一个在64b元素中随机播放的1uop insn。 shufps
(包括Penryn)将数据带入整数域,导致绕过延迟,使其返回到addps
的FP执行单元,但movhlps
完全在FP域。 shufpd
也在浮动域中运行。
movshdup
在整数域中运行,但只有一个uop。
shufps
旁路延迟,movhlps
)
如果没有AVX,请避免浪费movaps
/ movdqa
指令,需要仔细选择随机播放。只有少数shuffle可以作为复制和随机播放,而不是修改目的地。组合来自两个输入(例如unpck*
或movhlps
)的数据的随机播放可以与不再需要的tmp变量一起使用,而不是_mm_movehl_ps(same,same)
。
其中一些可以更快(保存MOVAPS)但更丑/更少"清洁"通过使用虚拟arg作为初始shuffle的目的地。例如:
// Use dummy = a recently-dead variable that vec depends on,
// so it doesn't introduce a false dependency,
// and the compiler probably still has it in a register
__m128d highhalf_pd(__m128d dummy, __m128d vec) {
#ifdef __AVX__
// With 3-operand AVX instructions, don't create an extra dependency on something we don't need anymore.
(void)dummy;
return _mm_unpackhi_pd(vec, vec);
#else
// Without AVX, we can save a MOVAPS with MOVHLPS into a dead register
__m128 tmp = _mm_castpd_ps(dummy);
__m128d high = _mm_castps_pd(_mm_movehl_ps(tmp, _mm_castpd_ps(vec)));
return high;
#endif
}
float hsum_ps_sse1(__m128 v) { // v = [ D C | B A ]
__m128 shuf = _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 3, 0, 1)); // [ C D | A B ]
__m128 sums = _mm_add_ps(v, shuf); // sums = [ D+C C+D | B+A A+B ]
shuf = _mm_movehl_ps(shuf, sums); // [ C D | D+C C+D ] // let the compiler avoid a mov by reusing shuf
sums = _mm_add_ss(sums, shuf);
return _mm_cvtss_f32(sums);
}
# gcc 5.3 -O3: looks optimal
movaps xmm1, xmm0 # I think one movaps is unavoidable, unless we have a 2nd register with known-safe floats in the upper 2 elements
shufps xmm1, xmm0, 177
addps xmm0, xmm1
movhlps xmm1, xmm0 # note the reuse of shuf, avoiding a movaps
addss xmm0, xmm1
# clang 3.7.1 -O3:
movaps xmm1, xmm0
shufps xmm1, xmm1, 177
addps xmm1, xmm0
movaps xmm0, xmm1
shufpd xmm0, xmm0, 1
addss xmm0, xmm1
我报了clang bug about pessimizing the shuffles。它有自己的内部表示,用于改组,然后将其转换为随机播放。 gcc更经常使用与您使用的内在函数直接匹配的指令。
在指令选择未经手动调整的代码中,clang通常比gcc更好,或者即使内在函数对于非常数情况是最优的,常量传播也可以简化事物。总的来说,编译器像内在函数的正确编译器一样工作,而不仅仅是汇编程序,这是一件好事。编译器通常可以从标量C生成良好的asm,它甚至不会尝试按照好的方式工作。最终,编译器会将内在函数视为另一个C运算符,作为优化器的输入。
float hsum_ps_sse3(__m128 v) {
__m128 shuf = _mm_movehdup_ps(v); // broadcast elements 3,1 to 2,0
__m128 sums = _mm_add_ps(v, shuf);
shuf = _mm_movehl_ps(shuf, sums); // high half -> low half
sums = _mm_add_ss(sums, shuf);
return _mm_cvtss_f32(sums);
}
# gcc 5.3 -O3: perfectly optimal code
movshdup xmm1, xmm0
addps xmm0, xmm1
movhlps xmm1, xmm0
addss xmm0, xmm1
这有几个好处:
不需要任何movaps
份副本来解决破坏性混洗(没有AVX):movshdup xmm1, xmm2
目的地是只写的,所以它会创建{{ 1}}出于我们的死登记册。这也是我使用tmp
代替movehl_ps(tmp, sums)
。
小代码。混洗指令很小:movehl_ps(sums, sums)
是3个字节,movhlps
是4个字节(与movshdup
相同)。不需要立即字节,因此对于AVX,shufps
为5个字节,vshufps
和vmovhlps
均为4。
我可以使用vmovshdup
而不是addps
保存另一个字节。由于不能在内部环路内使用,因此切换额外晶体管的额外能量可能可以忽略不计。来自上3个元素的FP异常不存在风险,因为所有元素都包含有效的FP数据。然而,clang / LLVM实际上"理解"矢量shuffle,如果它知道只有低元素很重要,则发出更好的代码。
与SSE1版本一样,将奇数元素添加到自身可能会导致FP异常(如溢出),否则不会发生,但这不应该是一个问题。非正规数很慢,但IIRC产生+ Inf结果并不适用于大多数搜索。
如果代码大小是您最关心的问题,那么两个addss
(haddps
)指令就可以解决问题(Paul R的回答)。这也是最容易打字和记忆的。但是,不快。甚至英特尔Skylake仍然会将每个_mm_hadd_ps
解码为3 uop,具有6个周期延迟。因此,即使它节省了机器代码字节(L1 I-cache),它也会在更有价值的uop-cache中占用更多空间。 haddps
的真实用例:a transpose-and-sum problem,或在中间步in this SSE atoi()
implementation进行一些缩放。
此版本保存代码字节与Marat's answer to the AVX question。
haddps
#ifdef __AVX__
float hsum256_ps_avx(__m256 v) {
__m128 vlow = _mm256_castps256_ps128(v);
__m128 vhigh = _mm256_extractf128_ps(v, 1); // high 128
vlow = _mm_add_ps(vlow, vhigh); // add the low 128
return hsum_ps_sse3(vlow); // and inline the sse3 version, which is optimal for AVX
// (no wasted instructions, and all of them are the 4B minimum)
}
#endif
vmovaps xmm1,xmm0 # huh, what the heck gcc? Just extract to xmm1
vextractf128 xmm0,ymm0,0x1
vaddps xmm0,xmm1,xmm0
vmovshdup xmm1,xmm0
vaddps xmm0,xmm1,xmm0
vmovhlps xmm1,xmm1,xmm0
vaddss xmm0,xmm0,xmm1
vzeroupper
ret
存储到内存并返回可避免ALU uop。如果洗牌端口压力或一般的ALU uops是一个瓶颈,这是好事。 (请注意,它不需要double hsum_pd_sse2(__m128d vd) { // v = [ B | A ]
__m128 undef = _mm_undefined_ps(); // don't worry, we only use addSD, never touching the garbage bits with an FP add
__m128 shuftmp= _mm_movehl_ps(undef, _mm_castpd_ps(vd)); // there is no movhlpd
__m128d shuf = _mm_castps_pd(shuftmp);
return _mm_cvtsd_f64(_mm_add_sd(vd, shuf));
}
# gcc 5.3.0 -O3
pxor xmm1, xmm1 # hopefully when inlined, gcc could pick a register it knew wouldn't cause a false dep problem, and avoid the zeroing
movhlps xmm1, xmm0
addsd xmm0, xmm1
# clang 3.7.1 -O3 again doesn't use movhlps:
xorpd xmm2, xmm2 # with #define _mm_undefined_ps _mm_setzero_ps
movapd xmm1, xmm0
unpckhpd xmm1, xmm2
addsd xmm1, xmm0
movapd xmm0, xmm1 # another clang bug: wrong choice of operand order
// This doesn't compile the way it's written
double hsum_pd_scalar_sse2(__m128d vd) {
double tmp;
_mm_storeh_pd(&tmp, vd); // store the high half
double lo = _mm_cvtsd_f64(vd); // cast the low half
return lo+tmp;
}
# gcc 5.3 -O3
haddpd xmm0, xmm0 # Lower latency but less throughput than storing to memory
# ICC13
movhpd QWORD PTR [-8+rsp], xmm0 # only needs the store port, not the shuffle unit
addsd xmm0, QWORD PTR [-8+rsp]
或其他任何东西,因为x86-64 SysV ABI提供了一个红色区域,信号处理程序不会踩到它。)
有些人存储到一个数组并总结所有元素,但编译器通常不会意识到数组的低元素仍然存在于商店之前的寄存器中。
sub rsp, 8
是一种方便的复制和随机播放。遗憾的是,位和字节移位就位,pshufd
将目标的高半部分置于结果的低半部分,与punpckhqdq
将高半部分提取到不同寄存器的方式相反。
在某些CPU上使用movhlps
可能会很好,但前提是我们有一个临时注册表。 movhlps
是一个安全的选择,并且在Merom之后的所有事情上都很快。
pshufd
在某些CPU上,对整数数据使用FP shuffle是安全的。我没有这样做,因为在现代CPU上最多可以节省1或2个代码字节,没有速度增益(除了代码大小/对齐效果)。
答案 1 :(得分:18)
const __m128 t = _mm_add_ps(v, _mm_movehl_ps(v, v));
const __m128 sum = _mm_add_ss(t, _mm_shuffle_ps(t, t, 1));
const __m128 t1 = _mm_movehl_ps(v, v);
const __m128 t2 = _mm_add_ps(v, t1);
const __m128 sum = _mm_add_ss(t1, _mm_shuffle_ps(t2, t2, 1));
我发现这些速度与双倍HADDPS
大致相同(但我没有太仔细地测量过)。
答案 2 :(得分:10)
您可以在SSE3中的两个HADDPS
说明中执行此操作:
v = _mm_hadd_ps(v, v);
v = _mm_hadd_ps(v, v);
这将总和放在所有元素中。
答案 3 :(得分:3)
我肯定会试试SSE 4.2。若你多次这样做(我假设你是性能问题),你可以用(1,1,1,1)预先加载一个寄存器,然后做几个dot4(my_vec(s),one_vec)在上面。是的,它确实是多余的,但这些日子相当便宜,而且这种操作可能会受到水平依赖性的支配,这可能在新的SSE点积函数中更加优化。您应该测试它是否优于Paul R发布的双重水平添加。
我还建议将它与直标量(或标量SSE)代码进行比较 - 奇怪的是它通常更快(通常是因为内部序列化但使用寄存器旁路紧密流水线,其中特殊的水平指令可能无法快速完成(尚未) )除非你运行类似SIMT的代码,听起来你不是(否则你会做四个点产品)。