看来您正在对输出数组的每个元素进行水平求和。 (也许作为 matmul 的一部分?)这通常是次优的;尝试对第二个内循环进行矢量化,以便可以生成result[i + 0..3]
在向量中,根本不需要水平总和。
对于大于一个向量的数组的点积,垂直求和 (进入多个累加器 https://stackoverflow.com/questions/45113527/why-does-mulss-take-only-3-cycles-on-haswell-different-from-agners-instruction),最后只总结一次。
对于一般的水平缩减,请参见进行水平 SSE 向量和(或其他简化)的最快方法 https://stackoverflow.com/q/6996764- 提取高半部分并添加到低半部分。重复此操作,直到只剩下 1 个元素。
如果您在内循环中使用它,您绝对不想使用hadd(same,same)
。这会花费 2 个 shuffle uop,而不是 1 个,除非你的编译器将你从自己的手中拯救出来。 (而 gcc/clang 则不然。)hadd
对于代码大小来说很有好处,但当你只有 1 个向量时几乎没有其他作用。它可以是有用且高效的,有两个不同的 inputs.
对于 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
}
如果您希望将结果广播到 a 的每个元素__m256d
,你会用vshufpd
and vperm2f128
交换高/低半部分(如果针对英特尔进行调整)。并全程使用256位FP相加。如果你关心早期的 Ryzen,你可能会减少到 128,使用_mm_shuffle_pd
交换,然后vinsertf128
获得 256 位向量。或者使用 AVX2,vbroadcastsd
关于这件事的最终结果。但这对于英特尔来说会比始终保持 256 位慢,同时仍避免vhaddpd
.
编译为gcc7.3 -O3 -march=haswell
在 Godbolt 编译器资源管理器上 https://gcc.godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,lang:c%2B%2B,source:%27%23include+%3Cimmintrin.h%3E%0A%0Adouble+hsum_double_avx(__m256d+v)+%7B%0A++++__m128d+vlow++%3D+_mm256_castpd256_pd128(v)%3B%0A++++__m128d+vhigh+%3D+_mm256_extractf128_pd(v,+1)%3B+//+high+128%0A+++++++++++vlow++%3D+_mm_add_pd(vlow,+vhigh)%3B+++++//+add+the+low+128%0A%0A++++__m128d+high64+%3D+_mm_unpackhi_pd(vlow,+vlow)%3B%0A++++return++_mm_cvtsd_f64(_mm_add_sd(vlow,+high64))%3B%0A%7D%0A%0A//+not+better+even+on+Intel,+but+horrible+on+AMD%0Adouble+hsum_double_avx256_paul(__m256d+acc)%0A%7B%0A++++acc+%3D+_mm256_hadd_pd(acc,+acc)%3B++++//+horizontal+add+top+lane+and+bottom+lane%0A++++acc+%3D+_mm256_add_pd(acc,+_mm256_permute2f128_pd(acc,+acc,+0x31))%3B++//+add+lanes%0A++++return++_mm256_cvtsd_f64(acc)%3B+//+extract+double%0A%7D%0A%27),l:%275%27,n:%270%27,o:%27C%2B%2B+source+%231%27,t:%270%27)),k:46.84947491248541,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((g:!((h:compiler,i:(compiler:g73,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,trim:%271%27),lang:c%2B%2B,libs:!(),options:%27-xc+-std%3Dgnu99+-Wall+-O3+-march%3Dhaswell%27,source:1),l:%275%27,n:%270%27,o:%27x86-64+gcc+7.3+(Editor+%231,+Compiler+%231)+C%2B%2B%27,t:%270%27)),k:50,l:%274%27,m:75.80477673935619,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((h:output,i:(compiler:1,editor:1,wrap:%271%27),l:%275%27,n:%270%27,o:%27%231+with+x86-64+gcc+7.3%27,t:%270%27)),header:(),l:%274%27,m:24.195223260643818,n:%270%27,o:%27%27,s:0,t:%270%27)),k:53.15052508751459,l:%273%27,n:%270%27,o:%27%27,t:%270%27)),l:%272%27,n:%270%27,o:%27%27,t:%270%27)),version:4
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
到不同的寄存器而不是销毁保存着的 xmm0_mm256_castpd256_pd128
result.
在第一代 Ryzen (Zen 1 / 1+) 上,根据Agner Fog 的说明书 http://agner.org/optimize/, vextractf128
为 1 uop,延迟为 1c,吞吐量为 0.33c。
不幸的是,@PaulR 的版本在 Zen 2 之前的 AMD 上很糟糕;它就像您可能在英特尔库或编译器输出中找到的“cripple AMD”函数一样。 (我不认为 Paul 是故意这样做的,我只是指出忽略 AMD CPU 会如何导致代码在它们上运行速度变慢。)
在禅宗 1 上,vperm2f128
是 8 uop、3c 延迟和每 3c 吞吐量 1 个。vhaddpd ymm
是 8 uops(相对于您可能期望的 6 uops),7c 延迟,每 3c 吞吐量 1 个。阿格纳说这是一个“混合域”指令。 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 版本可能至少在 Intel 以及muchAMD 更好。它还应该产生稍微更少的热量,从而允许稍微更高的涡轮增压/使用更少的电池电量。 (但希望这个 hsum 是整个循环的一小部分,可以忽略不计!)
延迟也不差,所以没有理由使用低效的hadd
/ vpermf128
版本。
Zen 2 及更高版本具有 256 位宽向量寄存器和执行单元(包括 shuffle)。他们不必将穿越车道的洗牌分成许多微指令,但相反vextractf128
不再像vmovdqa xmm
。 Zen 2 更接近 Intel 256 位向量的成本模型。