一般来说,对于任何类型的向量水平缩减,提取/洗牌高半部分以与低部分对齐,然后垂直相加(或最小/最大/或/和/异或/乘/其他);重复直到 a 只有一个元素(向量的其余部分有大量垃圾)。
如果您从宽度超过 128 位的向量开始,将其缩小一半,直到达到 128 位(然后您可以在该向量上使用此答案中的函数之一)。但如果您需要将结果广播到最后的所有元素,那么您可以考虑一路进行全角洗牌。
更广泛的向量和整数的相关问答,以及FP
-
__m128
and __m128d
这个答案(见下文)
-
__m256d
对 Ryzen 1 与 Intel 进行性能分析(说明原因vextractf128
比vperm2f128
) 使用 SSE/AVX 获取 __m256d 中存储的值的总和 https://stackoverflow.com/questions/49941645/get-sum-of-values-stored-in-m256d-with-sse-avx
-
__m256
如何水平求和__m256? https://stackoverflow.com/questions/13219146/how-to-sum-m256-horizontally
-
Intel AVX:双精度浮点变量点积的 256 位版本 https://stackoverflow.com/questions/10454150/intel-avx-256-bits-version-of-dot-product-for-double-precision-floating-point-v/47445367#47445367的单个向量。
-
的点积arrays(不仅仅是 3 或 4 个元素的单个向量):进行垂直 mul/add 或 FMA 到多个累加器 https://stackoverflow.com/questions/45113527/why-does-mulss-take-only-3-cycles-on-haswell-different-from-agners-instruction,最后还有 hsum。完整的 AVX+FMA 数组点积示例 https://stackoverflow.com/questions/59494745/avx2-computing-dot-product-of-512-float-arrays,包括一个高效的 hsumafter循环。 (对于数组的简单求和或其他缩减,请使用该模式但不使用乘法部分,例如使用 add 而不是 fma)。做not为每个 SIMD 向量分别进行水平工作;最后做一次。
如何使用 SIMD 计算字符出现次数 https://stackoverflow.com/questions/54541129/how-to-count-character-occurrences-using-simd作为一个整数计数的例子_mm256_cmpeq_epi8
再次匹配整个数组,仅在最后进行求和。 (值得特别提及的是,先进行一些 8 位累加,然后扩大 8 -> 64 位以避免溢出,而此时无需执行完整的 hsum。)
Integer
-
__m128i
32 位元素:这个答案(见下文)。 64 位元素应该是显而易见的:只有一个 pshufd/paddq 步骤。
-
__m128i
8 位无符号uint8_t
没有换行/溢出的元素:psadbw https://www.felixcloutier.com/x86/psadbw反对_mm_setzero_si128()
,然后对两个 qword 半部进行 hsum(对于更宽的向量,则为 4 或 8)。水平求和 SSE 无符号字节向量的最快方法 https://stackoverflow.com/questions/36998538/fastest-way-to-horizontally-sum-sse-unsigned-byte-vector显示带有 SSE2 的 128 位。使用 AVX 内在函数对 __m512i 中的 8 位整数求和 https://stackoverflow.com/questions/55296777/summing-8-bit-integers-in-m512i-with-avx-intrinsics有一个 AVX512 示例。如何使用 SIMD 计算字符出现次数 https://stackoverflow.com/q/54541129有一个AVX2__m256i
例子。
(For int8_t
有符号字节您可以异或 set1_epi8(0x80) 在 SAD 之前翻转为无符号,然后从最终的 hsum 中减去偏差;看详细信息在这里 https://stackoverflow.com/questions/70370454/how-to-horizontally-sum-signed-bytes-in-xmm,还显示了仅从内存中执行 9 个字节而不是 16 个字节的优化)。
-
16 位无符号:_mm_madd_epi16
set1_epi16(1) 是单微指令加宽水平加法:SIMD:累积相邻对 https://stackoverflow.com/questions/55057933/simd-accumulate-adjacent-pairs。然后继续处理 32 位 hsum。
-
__m256i
and __m512i
具有 32 位元素。使用 AVX512 或 AVX2 计算所有打包 32 位整数之和的最快方法 https://stackoverflow.com/questions/60108658/fastest-method-to-calculate-sum-of-all-packed-32-bit-integers-using-avx512-or-av。对于 AVX512,英特尔添加了一堆“减少”内联函数(不是硬件指令)来为您执行此操作,例如_mm512_reduce_add_ps
(以及 pd、epi32 和 epi64)。还减少_min/max/mul/和/或。手动执行会得到基本相同的 asm。
-
水平最大值(而不是相加):使用 SSE 获取 __m128i 向量中的最大值? https://stackoverflow.com/questions/9877700/getting-max-value-in-a-m128i-vector-with-sse/9878321#9878321
主要回答this问题:大部分是浮动的__m128
以下是一些基于调整的版本Agner Fog 的微架构指南 http://agner.org/optimize/的微架构指南和说明表。另请参阅x86 /questions/tagged/x86标签维基。它们在任何 CPU 上都应该高效,没有重大瓶颈。 (例如,我避免了那些对一个 uarch 有一点帮助但对另一个 uarch 来说很慢的事情)。代码大小也被最小化。
通用SSE3/SSSE3 2xhadd
惯用语仅适用于代码大小,而不适用于任何现有 CPU 的速度。它有一些用例(例如转置和添加,见下文),但单个向量不是其中之一。
我还包含了 AVX 版本。任何类型的 AVX / AVX2 水平缩减都应该从vextractf128
以及“垂直”操作以减少到 1 个 XMM (__m128
)向量。一般来说,对于宽向量,最好的选择是反复缩小一半,直到缩小到 128 位向量,无论元素类型如何。 (除8位整数外,则vpsadbw
如果您想对 hsum 进行求和而不溢出到更宽的元素,请作为第一步。)
查看所有这些代码的 asm 输出关于 Godbolt 编译器资源管理器 http://gcc.godbolt.org/#compilers:!((compiler:g6,options:%27-xc+-O3+-Wall+-fverbose-asm+-march%3Dhaswell+-mno-avx%27,sourcez:MQSwdgxgNgrgJgUwAQB4QFt3gC4CdwB0AFgHwBQoksiqAztnDseWQPStLZEi1I9JQQAa2QBZAIa5x2AOS0ANEgBGMbJwDuCcSLicA9kgh70ABxBRk2A0oTZsCXIb2ICbDtHFgA5khhgAZnq42H7SCFAAnkiIECCIvFa%2BtMjoegBu4ia0rLREMP4muuD0Wrp6/kipaURQWWT%2BUHrSSES0MOgA%2BlkdtMkAjAAUHR3ofQBMABxIaQCUSADeSEvLK6tr62vs00gAvEgA2kgAIkgAwkgAPkgAQkgAgkgAumQrw6OTSLn5y3sjnV/%2BBoILq0AZpRTgpAdUSiDoAZQAEgBVABiKIAMgBRAZjRQAZkUAAZFH0ZjMANxLLaHc4nK63B7PV4jcZTNroXhLX6YDriOBwEFgxQAimbDjs3h7Q5HADU51OMrpNxlDzuMtuTOWAI2XKhPKqCBqgoBwvatFFqy2B2W5yWStl8sVTypHAsai4yCMpnMDiQ4jSejifsq6WUUVwCBgtHAPgBLy1Zp1uz1nT5At6Awlwry/gp8aWEZCuDArx5EDS2F6HX8eLGmbNeYAvmR6o1mq12iCesk8UMWR9Zgt81D%2B2yc8m/iN0oa4DATILZpT1lslLgmnAIOJ6Ehwgh0AgwJWkAS%2BvokLjCcO3qzPonuan%2BQvs/k8yttet71O0oaoMac6aOVFLZuC8IgWnEKAKgAWhIAQ9HUcDIOHCUNk/NNu3rDln1zclh0LGBiw2Sdy0rWhq1rTDzVw5sWy2Wh0AgixHCMGhowALwQRQVDUKMYEYqJaEaTRHD8CxeicRAoPY5B%2BHEJATDXJQLHQVsmjUDtOm6Ig0z7d4pkHeYr1HW8OQnHltMfLIhWmV8E1Mj8Uw6CyBSsrMJVsgtbAIkt1mIisqxrOt3OoltQH8RAKmGO4ADUAA1hlU9t2TGABWAA2Lt/QAD101K0psodmT06YhN1P48o6Td6CyCqslZMEPJHYrqhAUCzPQCqECyvBxAgbB/FZJ8kFJSlgNasDWSM5qb3a3lLNBNIhIhECiAtZYtjTTgiGQUrJpWfDCI0rtegQXs0lZNakw2sAijAQQwEsbbPh7aYHGjPQwEUdRuAgMD%2BD0ExsAwCCkECRwYti4ctgGMADHULd7Fu%2BhcBgPqQA%2BhQ/Ruv0oCgJByi2vc/QjQmkAAFluLAwAwdoZjIZtgAPRh/BbNx2HZrY4D0GAlIQMguZ5iwWnZLp02SOtr0mXQ0jgOZFiTBWtjSZNDluK5GSmm8/AisqeW1hB/HABAXNBS6FY2TmPpkNR1CCXAIi%2B5APsiJJkDTOEjkUB7v0cKxUe4bxSa8SQlHELxkCUEAj3UKOwM8JAUQABT9flNY%2BAFsFMT8DSNKz9f8RRiIRwoF1li0tg9En%2BFhkNqlqOA04mXR30/KrsG6QpMxzTOTEag6fL8ysBX8NKyaGHl0NoOAwTgbCyRC2iOY5vhD1rMgcGFzsEDMWtuwQCXRxAJAsrligQHCg2RwhhKitZI/uFH3XOj8ExeqEbgOm3kBR4GLLFBPpcOoth4iggDBwnhdCwzAFBeIeBUZA2/PcOKAhbC8CjJ8f0bswz2CQDHLg3M1APWNjGYMVQKDhGSI3e%2B38yZPx6DmIEn8d51j/lCGE8JkRoixAMPoRJ8SKDGPPchN1z5UJMo/NCj4v4UQfmTf%2BjVJYTGobWOhAIgSNCYSAPoaVMKj0LuwxEqIMTYl4UgYkx4BHzxWFsOE8MTCk1KtgW2O5lIHkrGI9kKjJECmkUFdoeiWggFrH3LyhFB7RkGtGCinjBGAOsRwOEcJMRjFrg3eJ/dHJdR6n1TRMioBEjNjYpJcidzfhLPgmAbUo5OG9GJM8VQkb2D5PjCoclBD2CkHjEwWTcC6FwLWeQWVMDyEvDRDeR1fHdh7D0ISPQGK4xYggXSd9j4zEMisLKs1nK5JYfI3CGytnoV8b/PZeFQklnCVoyYPQgksKbKzReKdMiI1BmudASAJBSFkLwCGLQ7AmAAFzsHoG/acuAGjwQIF6VgABHGACB6DozANkPotY%2BgAE4%2BhkzSqwIg8EoJWCku0KCHV0pQTxfgNiH1sD8VYKisYDKyZ9EJMAeljLmWJXUiLboDEvnLI%2BCfQqywjDIrUIokyABFPiug9hZX2cKjGYrjKNCOHxPGex2RSr5PKl0gTVUgz2AMJAUFFAmuPniJAMpj4AHZ/4pKtVlfKdMVgiu3OK7g%2Br1WORzr%2BVy7QtWz0ldKxqdF2ieuTEas1ZqsqngdRah1KVLU2v/oSJNWV7XH1oQ6p1w5XVKuKuycN3jBQqrVYoD1aqQ2ugMIa41pr63H1TQ6jNWUs3HxzS6xVTUbyNGTIWtVOqkBjQjXW0d0bY3mrTYmh11qkDOoVaK7tHxuCzTURYY0Yay0mU9USGNVaTIjqjQ2rKTbj4TvTWm%2BNmap1pvyjOuduau3ivZLNSeoJGjlpAOXDg6EsjRANlob5hgoCeC8HIT4DChYAyBlgDiuAzlFguWWfyZFAqYXuUAA%3D%3D)),filterAsm:(binary:!t,commentOnly:!t,directives:!t,intel:!t,labels:!t),version:3.另请参阅我的改进Agner Fog 的 C++ 矢量类库 http://www.agner.org/optimize/#vectorclass horizontal_add
功能。 (留言板主题 http://www.agner.org/optimize/vectorclass/read.php?i=124,并编写代码github https://github.com/pcordes/vectorclass/commits/master)。我使用 CPP 宏为 SSE2、SSE4 和 AVX 的代码大小选择最佳洗牌,并避免movdqa
当 AVX 不可用时。
有一些权衡需要考虑:
- 代码大小:出于 L1 I-cache 的原因以及从磁盘获取代码(较小的二进制文件)的原因,越小越好。二进制总大小对于整个程序中重复做出的编译器决策至关重要。如果您费心使用内在函数手动编写某些内容,那么如果可以提高速度,那么花费一些代码字节是值得的对于整个程序(要小心使展开看起来不错的微基准)。
- uop-cache 大小:通常比 L1 I$ 更珍贵。 4 个单微操作指令占用的空间少于 2 个
haddps
,所以这在这里非常相关。
- 延迟:有时相关
- 吞吐量(后端端口):通常不相关,水平总和不应该是最内循环中的唯一内容。端口压力仅作为包含该压力的整个回路的一部分才重要。
- 吞吐量(总前端融合域 uops):如果周围的代码在 hsum 使用的同一端口上没有出现瓶颈,则这是 hsum 对整个吞吐量的影响的代理。
当水平添加不频繁时:
CPUs 没有 uop 缓存可能会喜欢 2xhaddps
如果很少使用:运行时速度很慢,但并不常见。只有 2 条指令可以最大限度地减少对周围代码(I$ 大小)的影响。
CPUs 带有 uop 缓存可能会喜欢需要更少微指令的东西,即使它有更多的指令/更多的 x86 代码大小。使用的 uop 缓存行总数是我们想要最小化的,这并不像最小化 uop 总数那么简单(采用的分支和 32B 边界总是启动一个新的 uop 缓存行)。
无论如何,话虽如此,横向总和得出lot,所以这是我精心制作一些编译良好的版本的尝试。没有在任何真实硬件上进行基准测试,甚至没有经过仔细测试。洗牌常量或其他内容可能存在错误。
如果您正在制作代码的后备/基线版本,请记住只有旧的 CPU 才能运行它;较新的 CPU 将运行您的 AVX 版本或 SSE4.1 或其他版本。
K8、Core2(merom) 及更早版本等旧版 CPU 仅具有 64 位随机单元。 Core2 具有适用于大多数指令的 128 位执行单元,但不适用于洗牌。 (Pentium M 和 K8 将所有 128b 向量指令作为两个 64 位一半处理)。
随机播放像movhlps
以 64 位块的形式移动数据(64 位半块内不进行混洗)的速度也很快。
相关:新 CPU 上的 shuffle,以及避免 Haswell 及更高版本上 1/clock shuffle 吞吐量瓶颈的技巧:AVX512 中的 128 位跨通道运算是否能提供更好的性能? https://stackoverflow.com/questions/47646238/do-128bit-cross-lane-operations-in-avx512-give-better-performance
在洗牌速度较慢的旧 CPU 上:
-
movhlps
(Merom:1uop)明显快于shufps
(梅罗姆:3uop)。在 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
Core2(包括 Penryn)将数据带入整数域,导致绕过延迟将其返回 FP 执行单元addps
, but movhlps
完全属于 FP 域。shufpd
也运行在浮点域中。
movshdup
在整数域中运行,但只有一个微指令。
AMD K10、Intel Core2(Penryn/Wolfdale) 以及所有更高版本的 CPU 将所有 xmm shuffle 作为单个 uop 运行。 (但请注意旁路延迟shufps
在 Penryn 上,避免使用movhlps
)
无需AVX,避免浪费movaps
/movdqa
说明需要仔细选择洗牌。只有少数随机播放起到复制和随机播放的作用,而不是修改目标。组合来自两个输入的数据的随机播放(例如unpck*
or movhlps
) 可以与不再需要的 tmp 变量一起使用,而不是_mm_movehl_ps(same,same)
.
通过采用虚拟参数作为初始洗牌的目的地,其中一些可以变得更快(保存 MOVAPS),但更丑陋/不太“干净”。例如:
// 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
}
SSE1(又名 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 https://llvm.org/bugs/show_bug.cgi?id=26491。它有自己的洗牌内部表示,并将其变回洗牌。 gcc 更经常使用与您使用的内在函数直接匹配的指令。
通常,在指令选择未手动调整的代码中,clang 比 gcc 做得更好,或者即使内在函数对于非常量情况而言是最佳的,常量传播也可以简化事情。总的来说,编译器像一个适合内在函数的编译器一样工作,而不仅仅是一个汇编器,这是一件好事。编译器通常可以从标量 C 生成良好的 asm,但它甚至不会尝试按照良好的 asm 的方式工作。最终编译器会将内在函数视为另一个 C 运算符作为优化器的输入。
SSE3
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
的目的地是只写的,所以它创建tmp
为我们提供了一个死寄存器。这也是我使用的原因movehl_ps(tmp, sums)
代替movehl_ps(sums, sums)
.
-
代码尺寸小。洗牌指令很小:movhlps
是3个字节,movshdup
是 4 个字节(与shufps
)。不需要立即字节,因此使用 AVX,vshufps
是 5 个字节但是vmovhlps
and vmovshdup
都是4。
我可以节省另一个字节addps
代替addss
。由于这不会在内部循环中使用,因此切换额外晶体管的额外能量可能可以忽略不计。上面 3 个元素的 FP 异常不存在风险,因为所有元素都保存有效的 FP 数据。然而,clang/LLVM 实际上“理解”向量洗牌,并且如果它知道只有低元素重要,就会发出更好的代码。
与 SSE1 版本一样,向其自身添加奇数元素可能会导致 FP 异常(如溢出),否则不会发生这种情况,但这应该不是问题。非正规化很慢,但 IIRC 产生 +Inf 结果并不在大多数 uarches 上。
SSE3 优化代码大小
如果代码大小是您主要关心的问题,那么两个haddps
(_mm_hadd_ps
)指令就可以解决问题(Paul R 的回答)。这也是最容易输入和记住的。这是not fast, 尽管。甚至 Intel Skylake 仍然可以解码每个haddps
至 3 uop,具有 6 个周期延迟。因此,尽管它节省了机器代码字节(L1 I-cache),但它在更有价值的 uop-cache 中占用了更多空间。真实用例haddps
: 转置求和问题 https://stackoverflow.com/questions/51274287/computing-8-horizontal-sums-of-eight-avx-single-precision-floating-point-vectors,或者在中间步骤进行一些缩放在本次上交所atoi()执行 https://stackoverflow.com/a/35132718/224132.
AVX:
与其他版本相比,此版本节省了一个代码字节。Marat 对 AVX 问题的回答 https://stackoverflow.com/questions/13219146/how-to-sum-m256-horizontally/13222410#13222410.
#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]
存储到内存并返回避免了 ALU uop。如果洗牌端口压力或一般的 ALU uops 是瓶颈,那么这很好。 (注意,不需要sub rsp, 8
或任何东西,因为 x86-64 SysV ABI 提供了信号处理程序不会踩踏的红色区域。)
有些人存储到数组并对所有元素求和,但编译器通常没有意识到数组的低位元素仍然存在于存储之前的寄存器中。
Integer:
pshufd
是一种方便的复制和随机播放方式。不幸的是,位和字节移位是就位的,并且punpckhqdq
将目标的高半部分放入结果的低半部分,与方式相反movhlps
可以将高半部分提取到不同的寄存器中。
Using movhlps
第一步在某些 CPU 上可能会很好,但前提是我们有一个暂存寄存器。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);
}
在某些 CPU 上,对整数数据使用 FP shuffle 是安全的。我没有这样做,因为在现代 CPU 上最多可以节省 1 或 2 个代码字节,并且没有速度增益(除了代码大小/对齐效果)。