Assembly 进行水平SSE矢量求和(或其他缩减)的最快方法
给定三(或四)个浮点数的向量。什么是求和的最快方法 SSE(movaps、shuffle、add、movd)是否总是比x87快?SSE3中的水平添加指令值得吗 迁移到FPU,然后是faddp,faddp的成本是多少?最快的特定指令序列是什么Assembly 进行水平SSE矢量求和(或其他缩减)的最快方法,assembly,optimization,floating-point,sse,simd,x86,Assembly,Optimization,Floating Point,Sse,Simd,X86,给定三(或四)个浮点数的向量。什么是求和的最快方法 SSE(movaps、shuffle、add、movd)是否总是比x87快?SSE3中的水平添加指令值得吗 迁移到FPU,然后是faddp,faddp的成本是多少?最快的特定指令序列是什么 “尽量安排事情,以便一次可以对四个向量求和”将不会被接受为答案。:-)e、 g.对于一个数组求和,您可以使用多个向量累加器进行垂直求和(以隐藏addps延迟),并在循环后减少到一个,但随后您需要对最后一个向量进行水平求和。您可以在SSE3中的两个HADDP指
“尽量安排事情,以便一次可以对四个向量求和”将不会被接受为答案。:-)e、 g.对于一个数组求和,您可以使用多个向量累加器进行垂直求和(以隐藏addps延迟),并在循环后减少到一个,但随后您需要对最后一个向量进行水平求和。您可以在SSE3中的两个
HADDP
指令中执行此操作:
v = _mm_hadd_ps(v, v);
v = _mm_hadd_ps(v, v);
这会将总和放入所有元素。您可以在SSE3中的两个
HADDP
指令中进行此操作:
v = _mm_hadd_ps(v, v);
v = _mm_hadd_ps(v, v);
这将把总和放在所有元素中。我肯定会尝试SSE 4.2。如果您多次这样做(如果性能有问题,我假设您会这样做),您可以使用(1,1,1,1)预加载一个寄存器,然后对其执行多个dot4(my_vec,one_vec)。是的,它做了一个多余的乘法运算,但这些运算现在相当便宜,这样的运算很可能被水平依赖性所支配,在新的SSE点积函数中,水平依赖性可能会更优化。您应该进行测试,看看它是否优于双水平加法
我还建议将其与纯标量(或标量SSE)代码进行比较——奇怪的是,它通常更快(通常是因为它在内部是序列化的,但使用寄存器旁路进行了严格的流水线处理,其中特殊的水平指令可能还没有快速路径),除非您运行的是类似SIMT的代码,听起来您并没有(否则,您将使用四个点积)。我肯定会尝试SSE 4.2。如果您多次这样做(如果性能有问题,我假设您会这样做),您可以使用(1,1,1,1)预加载一个寄存器,然后执行多个点积(my_vec,one_vec)在它上面。是的,它做了一个多余的乘法运算,但这些运算现在相当便宜,这样的运算很可能被水平相关性所支配,这可能在新的SSE点积函数中更为优化。你应该测试一下它是否优于双水平加法 我还建议将其与纯标量(或标量SSE)代码进行比较——奇怪的是,它通常更快(通常是因为它在内部是序列化的,但使用寄存器旁路进行了严格的流水线处理,其中特殊的水平指令可能还没有快速路径),除非您运行的是类似SIMT的代码,听起来您并没有(否则您将使用四点积)。SSE2 所有四个: r1+r2+r3: 我发现它们的速度与double
HADDPS
差不多(但我没有仔细测量)。SSE2
所有四个:
r1+r2+r3:
我发现它们的速度与double
HADDPS
差不多(但我没有测量得太近)。一般来说,对于任何类型的向量水平归约,提取/洗牌高半到低,然后垂直相加(或最小/最大/或/和/异或/乘法/无论什么)如果你从大于128位的向量开始,缩小一半,直到你得到128(然后你可以使用这个向量中的一个函数)。除非你需要对结果的所有元素广播,否则你可以考虑做全宽度拖曳。
更宽向量、整数和FP的相关Q&A
和\uuum128
此答案(见下文)\uuuum128d
对Ryzen 1与Intel进行性能分析(说明为什么\uuuu m256d
比vextractf128
好得多)vperm2f128
\uuum256
- 单个向量的
- 数组的点积(不仅仅是3个或4个元素的单个向量):在循环结束后进行垂直mul/add或FMA和hsum,包括有效的hsum。(对于数组的简单求和或其他缩减,使用该模式,但不使用乘法部分,例如add而不是FMA).不要对每个SIMD向量单独执行水平工作;在结束时执行一次
作为计数
匹配的整数示例,再次在整个数组上进行匹配,最后只进行求和(值得特别提及的是,进行一些8位累加,然后加宽8->64位以避免溢出,而不在该点进行完整的求和)\u mm256\u cmpeq\u epi8
32位元素:此答案(见下文)。64位元素应该是显而易见的:只有一个pshufd/paddq步骤\uuu m128i
8位无符号元素:针对\uuum128i
,然后对两个qword半部分进行求和(对于较宽的向量,为4或8)。使用SSE2显示128位。 有一个AVX512示例。有一个AVX2\umm\u setzero\u si128()
示例 (对于有符号字节,可以使用XOR set1(0x80)将SAD之前的值翻转为unsigned,然后从最终的hsum中减去偏差)\uuu m256i
将set1(1)作为单个uop加宽水平添加构建块,用于窄整数:\u mm\u madd\u epi16
- 带有32位元素的
和\uuuum256i
。 。对于AVX512,Intel添加了一系列为您执行此操作的“reduce”内联函数(而不是硬件指令),如\uuuum512i
(以及pd、epi32和epi64)。此外,还添加了reduce\u min/max/mul/和/或。手动执行此操作将导致基本相同的asm\u mm512\u reduce\u add\u ps
- 水平最大值(而不是添加):
这个问题的主要答案:主要是浮动和
\uuuu m128
以下是一些基于的Microach指南和指令表进行优化的版本。另请参阅tag wiki。它们在任何CPU上都应该是高效的,没有主要瓶颈
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));
// 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
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
#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
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]
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);
}