// 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
}
字符串
__m128 float with SSE 1(aka SSE):
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。它有自己的内部表示来进行shuffling,并将其转换回shuffles。gcc更经常使用与您使用的intrinsic直接匹配的指令。 通常clang比gcc做得更好,在代码中指令选择不是手工调优的,或者常量传播可以简化事情,即使内部函数对于非常量情况是最佳的。总的来说,编译器像内部函数的正确编译器一样工作是一件好事,不只是一个汇编器。编译器通常可以从标量C生成好的asm,甚至不尝试以好的asm的方式工作。最终编译器将把intrinsic当作另一个C操作符作为优化器的输入。
如果代码大小是您主要关心的问题,那么两个haddps(_mm_hadd_ps)指令将完成此操作(Paul R的回答)。这也是最容易输入和记住的。但是它不快。即使是Intel Skylake仍然将每个haddps解码为3个uop,有6个周期的延迟。所以即使它节省了机器码字节(L1 I-cache),它在更有价值的uop-cache中占用更多的空间。haddps的真实的用例:a transpose-and-sum problem,或者在中间步骤in this SSE atoi() implementation进行一些扩展。
__m256使用AVX浮动:
这个版本比马拉特对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
型
__m128d double双精度:
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]
5条答案
按热度按时间au9on6nz1#
一般来说,对于任何类型的向量水平缩减,提取/ Shuffle 高半部分与低部分对齐,然后垂直添加(或min/max/或/和/xor/multiply/whatever);重复直到只有一个元素(在向量的其余部分中具有高垃圾)。
如果你从宽度大于128位的向量开始,缩小一半,直到128位(然后你可以在该向量上使用本答案中的函数之一)。但是如果你需要在最后将结果广播到所有元素,那么你可以考虑一直进行全宽 Shuffle 。
更宽向量、整数和FP的相关Q& A
__m128
和__m128d
这个答案(见下文)__m256d
与Ryzen 1对比英特尔的性能分析(显示为什么vextractf128
远远优于vperm2f128
)Get sum of values stored in __m256d with SSE/AVX__m256
How to sum __m256 horizontally?How to count character occurrences using SIMD作为计数
_mm256_cmpeq_epi8
的整数示例,同样在整个数组中匹配,仅在末尾进行hsum。(值得特别提及的是,进行一些8位累加,然后扩展8 -> 64位以避免溢出,而无需在该点进行完整的hsum。)我知道了
__m128i
32位元素:这个答案(见下文)。64位元素应该是显而易见的:只有一个pshufd/paddq步骤。__m128i
**8位无符号uint8_t
**元素,无 Package /溢出:psadbw
对_mm_setzero_si128()
,然后hsum两个qword半(或4或8用于更宽的向量)。Fastest way to horizontally sum SSE unsigned byte vector显示128位SSE 2。Summing 8-bit integers in __m512i with AVX intrinsics有AVX 512示例。How to count character occurrences using SIMD有AVX 2__m256i
示例。(For**
int8_t
有符号字节**您可以XOR set1_epi8(0x 80)在SAD之前翻转为无符号,然后从最终的hsum中减去偏差;请参阅details here,还显示了仅从内存中执行9个字节而不是16个字节的优化)。_mm_madd_epi16
和set1_epi16(1)是一个单微操作加宽水平加法:SIMD: Accumulate Adjacent Pairs。然后继续进行32位hsum。__m256i
和__m512i
带有32位元素。Fastest method to calculate sum of all packed 32-bit integers using AVX512 or AVX2。对于AVX 512,英特尔添加了一堆“reduce”内联函数(不是硬件指令)来为您执行此操作,如_mm512_reduce_add_ps
(以及pd、epi 32和epi 64)。还有reduce_min/max/穆尔/和/或。手动执行此操作会导致基本相同的asm。this 问题的主要答案:主要是float和
__m128
下面是一些基于Agner Fog's microarch guide的微架构指南和指令表进行了优化的版本。另请参阅x86标签wiki。它们在任何CPU上都应该是高效的,没有主要的瓶颈。(例如,我避免了那些对一个uarch有点帮助但对另一个uarch很慢的东西)。代码大小也被最小化。
常见的SSE 3/SSSE 3 2x
hadd
习惯用法仅适用于代码大小,而不是任何现有CPU的速度。它有一些用例(如转置和添加,见下文),但单个向量不是其中之一。我还包含了一个AVX版本,任何AVX /AVX 2的水平缩减都应该从一个
vextractf128
开始,然后通过一个“垂直”操作缩减到一个XMM(__m128
)vector。一般来说,对于宽向量,最好的办法是重复缩小一半,直到减少到128位向量,而不管元素类型如何。(除了8位整数,如果你想在不溢出的情况下对更宽的元素求和,那么vpsadbw
作为第一步。**请参阅Godbolt浏览器上所有这些代码的asm输出。**还请参阅我对Agner Fog的C++ Vector Class Library
horizontal_add
函数的改进。(message board thread,以及github上的代码)。我使用CPP宏为SSE 2,SSE 4和AVX选择最佳的shuffle代码大小,并在AVX不可用时避免movdqa
。需要考虑以下权衡:
代码大小:对于L1 I缓存和从磁盘获取代码(较小的二进制文件)来说,较小的更好。总的二进制文件大小对于在整个程序中反复进行的编译器决策至关重要。如果你费心用intrinsic手工编写代码,如果它能为整个程序提供任何加速 *,那么花几个代码字节是值得的(要小心使展开看起来很好的微基准测试)。
uop-cache size:通常比L1 I$更珍贵。4单uop指令占用的空间比2
haddps
少,因此这在这里是高度相关的。延迟:有时相关
吞吐量(后端端口):通常无关紧要,水平总和不应该是最内层循环中的唯一内容。端口压力仅作为包含此内容的整个循环的一部分。
throughput(total front-end fused-domain uops):如果周围代码没有在hsum使用的同一个端口上形成瓶颈,则这是hsum对整个吞吐量影响的代理。
水平加法不常见时:
没有uop-cache的CPU在很少使用的情况下可能更喜欢2x
haddps
:当它运行时会很慢,但这并不常见。只有2条指令可以最大限度地减少对周围代码的影响(I$ size)。带uop-cache的CPU可能更喜欢使用较少uop的东西,即使它是更多的指令/更多的x86代码大小。使用的uop缓存行总数是我们想要最小化的,这并不像最小化uop总数那么简单(采取的分支和32 B边界总是开始一个新的uop缓存行)。
无论如何,水平求和出现了很多次,所以这里是我精心制作的一些版本的尝试,这些版本编译得很好。没有在任何真实的硬件上进行基准测试,甚至没有仔细测试。在shuffle常量或其他东西中可能有错误。
如果您正在制作代码的回退/基线版本,请记住,只有旧的CPU才能运行它;新的CPU将运行您的AVX版本,或SSE 4.1或其他版本。
像K8和Core 2(merom)以及更早的老CPU只有64位shuffle单元。Core 2对大多数指令都有128位执行单元,但不包括shuffle。(Pentium M和K8将所有128 b向量指令作为两个64位半处理)。
像
movhlps
这样以64位块移动数据的 Shuffle (64位半块内没有 Shuffle )也很快。相关:新CPU上的shuffles,以及避免Haswell和更高版本上1/时钟shuffle吞吐量瓶颈的技巧:Do 128bit cross lane operations in AVX512 give better performance?
在慢速 Shuffle 的旧CPU上:
movhlps
(Merom:1uop)比shufps
(Merom:3uops)快得多。在Pentium-M上,比movaps
便宜。此外,它在Core 2上的FP域中运行,避免了其他shuffle的旁路延迟。unpcklpd
比unpcklps
快。pshufd
慢,pshuflw
/pshufhw
快(因为它们只混洗64位的一半)pshufb mm0
(MMX)速度快,pshufb xmm0
速度慢。haddps
非常慢(Merom和Pentium M上为6 uops)*
movshdup
(Merom:1uop)很有趣:它是唯一一个在64 b元素内混洗的1uop insn。Core 2(包括Penryn)上的
shufps
将数据带入整数域,导致旁路延迟将其返回到addps
的FP执行单元,但movhlps
完全在FP域中。movshdup
在整数域中运行,但只有一个uop。AMD K10、Intel Core 2(Penryn/Wolfdale)和所有后续CPU都将所有xmm shuffle作为单个uop运行。(但请注意Penryn上的
shufps
的旁路延迟,movhlps
避免了旁路延迟)在没有AVX的情况下,避免浪费
movaps
/movdqa
指令需要仔细选择shuffle。只有少数shuffle作为复制和shuffle工作,而不是修改目标。将来自两个输入的数据(如unpck*
或movhlps
)合并数据合并的shuffle可以与不再需要的tmp变量一起使用,而不是_mm_movehl_ps(same,same)
。**其中一些可以更快(保存MOVAPS),但通过将虚拟arg用作初始 Shuffle 的目的地,会更丑/更不“干净”。**例如:
字符串
__m128 float with SSE 1(aka SSE):
型
我报告了一个clang bug about pessimizing the shuffles。它有自己的内部表示来进行shuffling,并将其转换回shuffles。gcc更经常使用与您使用的intrinsic直接匹配的指令。
通常clang比gcc做得更好,在代码中指令选择不是手工调优的,或者常量传播可以简化事情,即使内部函数对于非常量情况是最佳的。总的来说,编译器像内部函数的正确编译器一样工作是一件好事,不只是一个汇编器。编译器通常可以从标量C生成好的asm,甚至不尝试以好的asm的方式工作。最终编译器将把intrinsic当作另一个C操作符作为优化器的输入。
__m128使用SSE 3浮动
型
这有几个优点:
movaps
副本来处理破坏性的shuffle(没有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 ,如果它知道只有低位元素才重要,它就会发出更好的代码。与SSE 1版本一样,向自身添加奇数元素可能会导致FP异常(如溢出),否则不会发生,但这应该不是问题。反正规化很慢,但IIRC产生+Inf结果在大多数uarch上都不会发生。
SSE 3优化代码大小
如果代码大小是您主要关心的问题,那么两个
haddps
(_mm_hadd_ps
)指令将完成此操作(Paul R的回答)。这也是最容易输入和记住的。但是它不快。即使是Intel Skylake仍然将每个haddps
解码为3个uop,有6个周期的延迟。所以即使它节省了机器码字节(L1 I-cache),它在更有价值的uop-cache中占用更多的空间。haddps
的真实的用例:a transpose-and-sum problem,或者在中间步骤in this SSEatoi()
implementation进行一些扩展。__m256使用AVX浮动:
这个版本比马拉特对AVX问题的回答节省了一个代码字节。
型
__m128d double双精度:
型
存储到内存和返回避免了ALU uop。如果shuffle端口压力或ALU uop是瓶颈,这很好。(注意,它不需要
sub rsp, 8
或任何东西,因为x86-64 SysV ABI提供了一个信号处理程序不会踩到的红色区域。)有些人存储到数组中并对所有元素求和,但编译器通常不会意识到数组的低元素仍然存在于存储之前的寄存器中。
__m128i int32_t解析:
pshufd
是一个方便的复制和混洗。不幸的是,位和字节移位是到位的,punpckhqdq
将目标的高半部分放在结果的低半部分,与movhlps
将高半部分提取到不同寄存器的方式相反。在某些CPU上使用
movhlps
作为第一步可能是好的,但前提是我们有一个临时注册表。pshufd
是一个安全的选择,并且在Merom之后的任何事情上都很快。型
在某些CPU上,对整数数据使用FP shuffle是安全的,我没有这样做,因为在现代CPU上,最多只能节省保存1或2个代码字节,没有速度增益(除了代码大小/对齐效果)。
k10s72fa2#
SSE 2
四个:
字符串
r1+r2+r3:
型
我发现它们的速度和double
HADDPS
差不多(但我没有太仔细地测量过)。gkl3eglg3#
您可以在SSE3中的两个
HADDPS
指令中执行此操作:字符串
这将所有元素的和。
hujrc8aj4#
我肯定会给给予SSE 4.2一个尝试。如果你是这样做多次(如果性能是一个问题,我假设你是),你可以用(1,1,1,1)预加载一个寄存器,然后执行几个dot 4(my_vec(s),one_vec)。是的,它做了一个多余的乘法,但这些都是相当便宜的,这样的操作很可能是由水平依赖关系主导的,这可能是更优化的新的SSE点积函数。你应该测试,看看它是否优于双水平加保罗R张贴。
我还建议将其与纯标量(或标量SSE)代码进行比较-奇怪的是,它通常更快(通常是因为它在内部是串行化的,但使用寄存器旁路进行了紧密的流水线处理,其中特殊的水平指令可能还没有快速路径),除非您正在运行类似SIMT的代码,这听起来好像不是(否则您会做四个点积)。
jm81lzqq5#
通常,“最快可能的方法”的问题预先假设了一个需要在时间关键循环中多次完成的任务。
那么最快的方法可能是成对工作的迭代方法,它可以分摊迭代之间的一些工作。
字符串
所需的和将从累加器的第二个元素(索引1)中找到(在1次迭代之后),而第一个元素将包含迄今为止所有元素的总减少。
型
我有疑问,如果这将被证明是更快的矢量长度为3或4比先生提出的Cordes,然而,对于16或8位数据,这种方法应该证明是值得的。然后当然需要执行3或4轮分别才能获得结果。
如果水平操作恰好是sum --那么每次迭代实际上可以只使用一个
hadd
。