Fee*_*ure 41 floating-point optimization x86 assembly sse
你有一个三(或四)个浮点数的向量.总结它们的最快方法是什么?
SSE(movaps,shuffle,add,movd)总是比x87快吗?SSE4.2中的水平加法说明值得吗?移动到FPU的成本是多少,然后是faddp,faddp?什么是最快的特定指令序列?
"尝试安排事情,这样你可以一次总结四个向量"将不被接受作为答案.:-)
Pet*_*des 69
以下是根据Agner Fog的微指南指南的微指南和指令表进行调整的一些版本.另请参阅x86标记wiki.它们应该在任何CPU上都很有效,没有主要的瓶颈.(例如,我避免了一些可以帮助一些人,但在另一个uarch上缓慢的事情).代码大小也被最小化.
常见的2x hadd
习惯用法仅适用于代码大小,而不适用于任何现有CPU.它有用例(见下文),但这不是其中之一.
我还包括一个AVX版本.使用AVX/AVX2进行任何类型的水平缩小应以a vextractf128
和"垂直"操作开始,以减少一个XMM(__m128
)向量.
请参阅Godbolt Compiler Explorer上所有这些代码的asm输出. 另请参阅我对Agner Fog的C++ Vector类库 horizontal_add
函数的改进.(留言板线程和github上的代码).我使用CPP宏为SSE2,SSE4和AVX的代码大小选择最佳shuffle,并避免movdqa
AVX不可用时.
需要考虑以下权衡:
haddps
,因此这在这里非常重要.当水平添加不频繁时:
没有uop-cache的 CPU 可能会支持2x haddps
:它运行时会很慢,但这种情况并不常见.只有2个指令可以最大限度地减少对周围代码的影响(I $ size).
具有uop-cache的 CPU 可能会偏向于需要更少uops的东西,即使它是更多指令/更多x86代码大小.使用的总uops缓存行是我们想要最小化的,这并不像最小化总uop那样简单(采用的分支和32B边界总是启动一个新的uop缓存行).
无论如何,说到这一点,横向总和出现了很多,所以这是我努力精心制作一些编译得很好的版本.没有在任何真实硬件上进行基准测试,甚至没有仔细测试.shuffle常量或其他东西可能存在错误.
如果您正在制作代码的后备/基线版本,请记住只有旧CPU会运行它 ; 较新的CPU将运行您的AVX版本,或SSE4.1或其他.
像K8和Core2(merom)这样的旧CPU只有64位的shuffle单元.Core2有大多数指令的128位执行单元,但不适用于shuffle.(Pentium M和K8将所有128b向量指令处理为两个64位半部分).
这样的随机播放movhlps
以64位块移动数据(在64位半部内没有改组)也很快.
在具有慢速shuffle的旧CPU上:
movhlps
(Merom:1uop)明显快于shufps
(Merom:3uops).在奔腾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
在Core2(包括Penryn)上将数据带入整数域,导致旁路延迟将其返回到FP执行单元addps
,但movhlps
完全在FP域中. shufpd
也在float域中运行.
movshdup
在整数域中运行,但只有一个uop.
AMD K10,Intel Core2(Penryn/Wolfdale)以及所有后来的CPU都将xmm shuffle作为单个uop运行.(但请注意shufps
Penryn 的旁路延迟,避免使用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
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
我报告了一个关于对洗牌感到悲观的吵闹声.它有自己的内部表示,用于改组,然后将其转换为随机播放.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
Run Code Online (Sandbox Code Playgroud)
这有几个好处:
不需要任何movaps
副本来破坏性混洗(没有AVX):movshdup xmm1, xmm2
目的地是只写的,所以它tmp
为我们创建了一个死寄存器.这也是我用来movehl_ps(tmp, sums)
代替的原因movehl_ps(sums, sums)
.
小代码.混洗指令很小: movhlps
是3个字节,movshdup
是4个字节(相同shufps
).没有直接的字节是必需的,因此与AVX,vshufps
是5个字节,但vmovhlps
并vmovshdup
都是4.
我可以用addps
而不是保存另一个字节addss
.由于这不会在内部环路中使用,因此切换额外晶体管的额外能量可能可以忽略不计.来自上3个元素的FP异常不存在风险,因为所有元素都包含有效的FP数据.然而,clang/LLVM实际上"理解"向量shuffle,并且如果它知道只有低元素很重要,则发出更好的代码.
与SSE1版本一样,将奇数元素添加到自身可能会导致FP异常(如溢出),否则不会发生,但这应该不是问题.非正规数很慢,但IIRC产生+ Inf结果并不是大多数的研究.
如果代码大小是你主要关注的问题,那么两个haddps
(_mm_hadd_ps
)指令就可以解决问题(Paul R的回答).这也是最容易打字和记忆的.但这并不快.甚至英特尔Skylake仍然将每个解码haddps
为3 uop,具有6个周期延迟.因此,即使它节省了机器代码字节(L1 I-cache),它也会在更有价值的uop-cache中占用更多空间.实际用例haddps
:转置和转换问题,或在此SSE atoi()
实现的中间步骤中进行一些扩展.
此版本保存代码字节与Marat对AVX问题的答案.
#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
Run Code Online (Sandbox Code Playgroud)
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]
Run Code Online (Sandbox Code Playgroud)
存储到内存和后退避免了ALU uop.如果洗牌端口压力或一般的ALU uops是一个瓶颈,这是好的.(请注意,它不需要sub rsp, 8
或任何东西,因为x86-64 SysV ABI提供了一个红色区域,信号处理程序不会踩到它.)
有些人存储到一个数组并总结所有元素,但编译器通常没有意识到数组的低元素仍然存在于存储之前的寄存器中.
pshufd
是一个方便的复制和随机播放.遗憾的是,位和字节移位就位,punpckhqdq
并将目标的高半部分置于结果的低半部分,与此方式相反movhlps
可将高半部分提取到不同的寄存器中.
使用movhlps
的第一个步骤可能是对某些CPU好,但前提是我们有一个从无到有REG. pshufd
是一个安全的选择,在Merom之后的所有事情上都很快.
int hsum_epi32_sse2(__m128i x) {
#ifdef __AVX__
__m128i hi64 = _mm_unpackhi_epi64(x, x); // 3-operand non-destructive AVX lets us save a byte without needing a mov
#else
__m128i hi64 = _mm_shuffle_epi32(x, _MM_SHUFFLE(1, 0, 3, 2));
#endif
__m128i sum64 = _mm_add_epi32(hi64, x);
__m128i hi32 = _mm_shufflelo_epi16(sum64, _MM_SHUFFLE(1, 0, 3, 2)); // Swap the low two elements
__m128i sum32 = _mm_add_epi32(sum64, hi32);
return _mm_cvtsi128_si32(sum32); // SSE2 movd
//return _mm_extract_epi32(hl, 0); // SSE4, even though it compiles to movd instead of a literal pextrd r32,xmm,0
}
# gcc 5.3 -O3
pshufd xmm1,xmm0,0x4e
paddd xmm0,xmm1
pshuflw xmm1,xmm0,0x4e
paddd xmm0,xmm1
movd eax,xmm0
int hsum_epi32_ssse3_slow_smallcode(__m128i x){
x = _mm_hadd_epi32(x, x);
x = _mm_hadd_epi32(x, x);
return _mm_cvtsi128_si32(x);
}
Run Code Online (Sandbox Code Playgroud)
在某些CPU上,对整数数据使用FP shuffle是安全的.我没有这样做,因为在现代CPU上最多可以节省1或2个代码字节,没有速度增益(除了代码大小/对齐效果).
Kor*_*nel 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));
Run Code Online (Sandbox Code Playgroud)
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));
Run Code Online (Sandbox Code Playgroud)
我发现这些速度与双倍速度大致相同HADDPS
(但我没有太仔细地测量).
Pau*_*l R 10
您可以HADDPS
在SSE3 中的两个说明中执行此操作:
v = _mm_hadd_ps(v, v);
v = _mm_hadd_ps(v, v);
Run Code Online (Sandbox Code Playgroud)
这将总和放在所有元素中.
我肯定会尝试 SSE 4.2。如果您多次执行此操作(如果性能是一个问题,我假设您是这样做的),您可以使用 (1,1,1,1) 预加载寄存器,然后执行几次 dot4(my_vec(s), one_vec)在上面。是的,它做了多余的乘法,但现在这些乘法相当便宜,而且这样的操作很可能由水平依赖关系主导,这可能在新的 SSE 点积函数中得到更优化。您应该测试一下它是否优于 Paul R 发布的双水平添加。
我还建议将它与直接标量(或标量 SSE)代码进行比较 - 奇怪的是,它通常更快(通常是因为在内部它是序列化的,但使用寄存器旁路紧密流水线化,其中特殊的水平指令可能无法快速路径(尚未)),除非您正在运行类似 SIMT 的代码,听起来你不是这样的(否则你会做四点积)。
归档时间: |
|
查看次数: |
24754 次 |
最近记录: |