如何解决在AVX及更高版本中打包非连续矢量元素
具有这种性质的代码:
void foo(double *restrict A,double *restrict x,double *restrict y) {
y[5] += A[4] * x[5];
y[5] += A[5] * x[1452];
y[5] += A[6] * x[3373];
}
使用gcc 10.2
和标志-O3 -mfma -mavx2 -fvect-cost-model=unlimited
(Compiler Explorer)进行编译的结果是:
foo(double*,double*,double*):
vmovsd xmm1,QWORD PTR [rdx+40]
vmovsd xmm0,QWORD PTR [rdi+32]
vfmadd132sd xmm0,xmm1,QWORD PTR [rsi+40]
vmovsd xmm2,QWORD PTR [rdi+40]
vfmadd231sd xmm0,xmm2,QWORD PTR [rsi+11616]
vmovsd xmm3,QWORD PTR [rdi+48]
vfmadd231sd xmm0,xmm3,QWORD PTR [rsi+26984]
vmovsd QWORD PTR [rdx+40],xmm0
ret
它不会将任何数据打包在一起(4个vmovsd
用于装载数据,1个用于存储),执行3 vfmaddXXXsd
。尽管如此,我将其向量化的动机是可以仅使用一个vfmadd231pd
来完成。我使用AVX2的内在函数编写此代码的“最干净的”尝试是:
void foo_intrin(double *restrict A,double *restrict y) {
__m256d __vop0,__vop1,__vop2;
__m128d __lo256,__hi256;
// THE ISSUE
__vop0 = _mm256_maskload_pd(&A[4],_mm256_set_epi64x(0,-1,-1));
__vop1 = _mm256_mask_i64gather_pd(_mm256_setzero_pd(),&x[5],3368,1447,0),_mm256_set_pd(0,-1),8);
// 1 vs 3 FMADD,"the gain"
__vop2 = _mm256_fmadd_pd(__vop0,__vop2);
// reducing 4 double elements:
// Peter Cordes' answer https://stackoverflow.com/a/49943540/2856041
__lo256 = _mm256_castpd256_pd128(__vop2);
__hi256 = _mm256_extractf128_pd(__vop2,0x1);
__lo256 = _mm_add_pd(__lo256,__hi256);
// question:
// could you use here shuffle instead?
// __hi256 = _mm_shuffle_pd(__lo256,__lo256,0x1);
__hi256 = _mm_unpackhi_pd(__lo256,__lo256);
__lo256 = _mm_add_pd(__lo256,__hi256);
y[5] += __lo256[0];
}
哪个会生成以下ASM:
foo_intrin(double*,double*):
vmovdqa ymm2,YMMWORD PTR .LC1[rip]
vmovapd ymm3,YMMWORD PTR .LC2[rip]
vmovdqa ymm0,YMMWORD PTR .LC0[rip]
vmaskmovpd ymm1,ymm0,YMMWORD PTR [rdi+32]
vxorpd xmm0,xmm0,xmm0
vgatherqpd ymm0,QWORD PTR [rsi+40+ymm2*8],ymm3
vxorpd xmm2,xmm2
vfmadd132pd ymm0,ymm2,ymm1
vmovapd xmm1,xmm0
vextractf128 xmm0,0x1
vaddpd xmm0,xmm1
vunpckhpd xmm1,xmm0
vaddpd xmm0,xmm1
vaddsd xmm0,QWORD PTR [rdx+40]
vmovsd QWORD PTR [rdx+40],xmm0
vzeroupper
ret
.LC0:
.quad -1
.quad -1
.quad -1
.quad 0
.LC1:
.quad 0
.quad 1447
.quad 3368
.quad 0
.LC2:
.long 0
.long -1074790400
.long 0
.long -1074790400
.long 0
.long -1074790400
.long 0
.long 0
对不起,如果有人现在有焦虑症发作,我深表歉意。让我们分解一下:
- 我猜那些
vxorpd
用于清理寄存器,但是icc
只会生成一个,而不是两个。 - 根据Agner Fog,VCL在AVX2中不使用
maskload
,因为“被屏蔽的指令在AVX512之前的指令集中非常慢” 。但是,在uops.info中,对于Skylake(“常规”,没有AVX-512)报告说:-
li VMOVAPD(YMM,M256),例如
_mm256_load_pd
的延迟为[≤5;≤8],吞吐量为0.5。 - VMASKMOVPD(YMM,YMM,M256),例如
_mm256_maskload_pd
的延迟为[1;≤9],吞吐量也为0.5,但解码后的分辨率为2而不是1。差异如此巨大吗?用其他方式打包会更好吗?
mask_gather
-时尚说明,据我对以上所有文档的了解,不管是否使用遮罩,它都具有相同的性能,这是正确的吗? uops.info和Intel Intrinsics Guide均报告相同的性能和ASM格式;我很可能错过了一些东西。
- 在所有情况下,
gather
比“简单”set
好吗?用内在术语说话。我知道set
会根据数据类型生成vmov
类型的指令(例如,如果数据是常量,它可能只加载地址,如.LC0
,.LC1
和.LC2
)。
_mm256_shuffle_pd
和_mm256_unpackhi_pd
具有相同的Lantecy和吞吐量。第一个生成vpermildp
,第二个生成vunpckhpd
,而uops.info也报告相同的值。有什么区别吗?最后但并非最不重要的是,这种临时矢量化值得吗?我并不是说我的内在代码,而是这样的向量化代码的概念。我怀疑通常比较干净的代码编译器产生的数据移动太多,因此我关心的是改进打包非连续数据的方式。
解决方法
vfmaddXXXsd
和pd
指令是“便宜的”(单uop,2 /时钟吞吐量),甚至比shuffle(Intel CPU的1时钟吞吐量)或收集加载便宜。 https://uops.info/。加载操作也是2 / clock,因此很多标量加载(尤其是来自同一条缓存行)非常便宜,请注意其中3个可以折叠为FMA的内存源操作数。
最坏的情况是,打包4(x2)个完全不连续的输入,然后手动分散输出,绝对不值得,与仅使用标量负载和标量FMA(尤其是允许FMA的内存源操作数)相比。 / p>
您的情况远没有最坏的情况;您从1个输入中有3个连续元素。如果您知道可以安全地加载4个元素,而没有接触未映射页面的风险,则可以解决该输入问题。 (并且您始终可以使用maskload)。但是另一个向量仍然是不连续的,可能会加速。
如果通过改组比普通标量需要更多的总指令(实际上是uops)来完成操作,通常是不值得的。和/或如果改组吞吐量比任何其他方法都更糟糕的瓶颈,标量版本。
({vgatherdpd
为此目的计算了很多指令,它们是多线程并且每次加载进行1次缓存访问。另外,您还必须加载索引的常数向量,而不是将偏移量硬编码为寻址模式。 / p>
此外,AMD CPU甚至Zen2上的收集速度都非常慢。直到AVX512,我们才完全没有散射,即使在冰湖上,散射也很慢。但是,您的案例不需要分散,只需要水平和即可。这将涉及更多的洗牌和vaddpd
/ sd
。 因此,即使使用maskload +收集输入信息,在单独的矢量元素中具有3个乘积对您来说也不是特别方便。)
一点点SIMD(不是一个完整的数组,只是几个操作)可能会有所帮助,但这看起来并不像是一次重大胜利。也许有些事情值得做,例如用负载+随机播放替换2个负载。或者可以通过将添加到输出中的三个产品 而不是3个FMA的链相加来缩短y[5]
的延迟链。在一个累加器可以容纳大量的情况下,这甚至在数值上可能更好。将多个较小的数字相加成较大的总数会失去精度。当然,这将花费1 mul,2 FMA和1添加。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。