有没有办法获得存储在__m256d变量中的值的总和?我有这个代码。
acc = _mm256_add_pd(acc, _mm256_mul_pd(row, vec));
//acc in this point contains {2.0, 8.0, 18.0, 32.0}
acc = _mm256_hadd_pd(acc, acc);
result[i] = ((double*)&acc)[0] + ((double*)&acc)[2];
此代码有效,但我想用SSE / AVX指令替换它。
答案 0 :(得分:7)
您似乎正在为输出数组的每个元素执行水平求和。 (也许作为matmul的一部分?)这通常是次优的;尝试在第二个内部循环上进行矢量化,这样您就可以在向量中生成result[i + 0..3]
,而根本不需要水平求和。
对于一般的水平缩减,请参阅Fastest way to do horizontal float vector sum on x86:提取高半部分并添加到低半部分。重复,直到你归结为1个元素。
如果您在内循环中使用此功能,那么您绝对不想使用hadd(same,same)
。除非您的编译器将您从自己身上拯救出来,否则这需要花费2个shuffle uops而不是1。 (并且gcc / clang不要。)hadd
对代码大小有好处,但几乎没有别的,除非你可以将它用于两个不同的输入。
对于AVX,这意味着我们需要的唯一256位操作是提取,这在AMD和Intel上都很快。其余的都是128位:
#include <immintrin.h>
inline
double hsum_double_avx(__m256d v) {
__m128d vlow = _mm256_castpd256_pd128(v);
__m128d vhigh = _mm256_extractf128_pd(v, 1); // high 128
vlow = _mm_add_pd(vlow, vhigh); // reduce down to 128
__m128d high64 = _mm_unpackhi_pd(vlow, vlow);
return _mm_cvtsd_f64(_mm_add_sd(vlow, high64)); // reduce to scalar
}
如果您希望将结果广播到__m256
的每个元素,您可以使用vshufpd
和vperm2f128
来交换高/低一半 (如果调整为英特尔)。并使用256位FP添加整个时间。如果您完全关心Ryzen,可以减少到128,使用_mm_shuffle_pd
进行交换,然后使用vinsertf128
获得256位向量。或者使用AVX2,vbroadcastsd
得出最终结果。但是,对于英特尔而言,这一点要比在整个时间内保持256位的速度慢,同时仍然避免使用vhaddpd
。
使用gcc7.3 -O3 -march=haswell
on the Godbolt compiler explorer
vmovapd xmm1, xmm0 # silly compiler, vextract to xmm1 instead
vextractf128 xmm0, ymm0, 0x1
vaddpd xmm0, xmm1, xmm0
vunpckhpd xmm1, xmm0, xmm0 # no wasted code bytes on an immediate for vpermilpd or vshufpd or anything
vaddsd xmm0, xmm0, xmm1 # scalar means we never raise FP exceptions for results we don't use
vzeroupper
ret
内联后(您肯定希望它),vzeroupper
会沉到整个函数的底部,希望vmovapd
优化,vextractf128
改为不同的寄存器破坏保存_mm256_castpd256_pd128
结果的xmm0。
根据Agner Fog's instruction tables,在Ryzen上,vextractf128
是1 uop,延迟为1c,吞吐量为0.33c。
在Ryzen上,vperm2f128
是8 uops,3c延迟,每3c吞吐量一个。 vhaddpd ymm
是8 uops(相对于你可能预期的6),7c延迟,每3c吞吐量一个。阿格纳说,它是一个混合领域&#34;指令。 256位操作总是至少需要2次。
# Paul's version # Ryzen # Skylake
vhaddpd ymm0, ymm0, ymm0 # 8 uops # 3 uops
vperm2f128 ymm1, ymm0, ymm0, 49 # 8 uops # 1 uop
vaddpd ymm0, ymm0, ymm1 # 2 uops # 1 uop
# total uops: # 18 # 5
VS
# my version with vmovapd optimized out: extract to a different reg
vextractf128 xmm1, ymm0, 0x1 # 1 uop # 1 uop
vaddpd xmm0, xmm1, xmm0 # 1 uop # 1 uop
vunpckhpd xmm1, xmm0, xmm0 # 1 uop # 1 uop
vaddsd xmm0, xmm0, xmm1 # 1 uop # 1 uop
# total uops: # 4 # 4
总的uop吞吐量通常是加载,存储和ALU混合的代码的瓶颈,所以我希望4-uop版本在英特尔上可能至少要好一些,以及多更好的AMD。它也应该稍微减少热量,因此允许稍高的涡轮/使用更少的电池电量。 (但希望这个hsum是你的总循环中的一小部分,这可以忽略不计!)
延迟也不会更糟,因此我们没有理由使用效率低下的hadd
/ vpermf128
版本。
答案 1 :(得分:3)
你可以这样做:
acc = _mm256_hadd_pd(acc, acc); // horizontal add top lane and bottom lane
acc = _mm256_add_pd(acc, _mm256_permute2f128_pd(acc, acc, 0x31)); // add lanes
result[i] = _mm256_cvtsd_f64(acc); // extract double
注意:如果这是代码的“热”(即性能关键)部分(特别是如果在AMD CPU上运行),那么您可能希望查看有关更高效实现的Peter Cordes's answer。