在 AVX(及更高版本)中打包非连续矢量元素
Packing non-contiguous vector elements in AVX (and higher)
具有这种性质的代码:
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
不打包任何数据(4vmovsd
加载数据,1个存储),执行3vfmaddXXXsd
。尽管如此,我将其矢量化的动机是它可以只使用一个 vfmadd231pd
来完成。我使用 AVX2 的内在函数编写此代码的“最干净”尝试是:
void foo_intrin(double *restrict A, double *restrict x,
double *restrict y) {
__m256d __vop0, __vop1,__vop2;
__m128d __lo256, __hi256;
// THE ISSUE
__vop0 = _mm256_maskload_pd(&A[4], _mm256_set_epi64x(0,-1,-1,-1));
__vop1 = _mm256_mask_i64gather_pd(_mm256_setzero_pd(), &x[5],
_mm256_set_epi64x(0,3368, 1447, 0),
_mm256_set_pd(0,-1,-1,-1), 8);
// 1 vs 3 FMADD, "the gain"
__vop2 = _mm256_fmadd_pd(__vop0, __vop1, __vop2);
// reducing 4 double elements:
// Peter Cordes' answer
__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*, 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, xmm2
vfmadd132pd ymm0, ymm2, ymm1
vmovapd xmm1, xmm0
vextractf128 xmm0, ymm0, 0x1
vaddpd xmm0, xmm0, xmm1
vunpckhpd xmm1, xmm0, xmm0
vaddpd xmm0, xmm0, xmm1
vaddsd xmm0, 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 does not use
maskload
in AVX2 since "masked instructions are very slow in instruction sets prior to AVX512". However, in uops.info 对于 Skylake(“常规”,无 AVX-512)而言:
- VMOVAPD(YMM,M256),例如
_mm256_load_pd
具有延迟 [≤5;≤8] 和 0.5 的吞吐量。
- VMASKMOVPD(YMM、YMM、M256),例如
_mm256_maskload_pd
具有延迟 [1;≤9] 和 0.5 的吞吐量,但以两个微指令而不是一个微指令解码。这个差距有这么大吗?用不同的方式打包更好吗?
- 关于
mask_gather
-fashion 说明,据我对上述所有文档的了解,无论是否使用遮罩,它都提供相同的性能,是否正确? uops.info 和 Intel Intrinsics Guide 报告相同的性能和 ASM 表格;我可能遗漏了什么。
- 是否在所有情况下
gather
都比“简单的”set
更好?用内在术语说话。我知道 set
根据数据类型生成 vmov
类型的指令(例如,如果数据是常量,它可能只加载一个地址,如 .LC0
、.LC1
和 .LC2
).
- 根据 Intel Intrinsics,
_mm256_shuffle_pd
和 _mm256_unpackhi_pd
具有相同的延迟和吞吐量;第一个生成 vpermildp
,第二个生成 vunpckhpd
,uops.info 也报告相同的值。有区别吗?
最后但同样重要的是,这种特别的矢量化是否值得?我只是指的不是我的内在代码,而是像这样矢量化代码的概念。我怀疑有太多的数据移动无法执行,比较干净的代码编译器,一般来说,产生,所以我关心的是改进打包非连续数据的方式。
vfmaddXXXsd
和 pd
指令是“便宜的”(单 uop,2/时钟吞吐量),甚至比洗牌(Intel CPU 上的 1/时钟吞吐量)或收集负载更便宜。 https://uops.info/。加载操作也是 2/clock,所以很多标量加载(尤其是来自同一缓存行)非常便宜,请注意其中 3 个可以折叠到 FMA 的内存源操作数中。
最坏的情况是,打包 4 (x2) 个完全不连续的输入然后手动分散输出与仅使用标量负载和标量 FMA 相比绝对不值得(尤其是当它允许 FMA 的内存源操作数时) .
你的情况远非最坏的情况;您有 1 个输入的 3 个连续元素。如果你知道你可以安全地加载 4 个元素而不会有接触未映射页面的风险,那么就可以处理该输入。 (而且您始终可以使用 maskload)。但是另一个向量仍然是不连续的,可能是加速的障碍。
如果通过洗牌比普通标量需要更多的总指令(实际上是 uops),通常是不值得的。 And/or 如果洗牌吞吐量是比标量版本中的任何瓶颈都严重。
(vgatherdpd
为此计算了尽可能多的指令,是多 uop,每次加载执行 1 次缓存访问。此外,您还必须加载索引的常量向量,而不是将偏移量硬编码到寻址模式中.
此外,在 AMD CPU 上收集速度非常慢,即使是 Zen2。在 AVX512 之前我们根本没有散射,即使在 Ice Lake 上也很慢。不过,您的案例不需要散点,只需要水平总和。这将涉及更多的洗牌和 vaddpd
/ sd
。 因此,即使使用 maskload + gather 作为输入,将 3 个产品放在单独的向量元素中对您来说也不是特别方便。)
一点点 SIMD(不是整个数组,只是一些操作)可能会有帮助,但这看起来不像是重大胜利的案例之一。也许有一些值得做的事情,比如用 load + a shuffle 代替 2 loads。或者可以通过将 前 的 3 个产品加到输出而不是 3 个 FMA 链来求和来缩短 y[5]
的延迟链。在累加器可以容纳大量数字的情况下,这甚至可能在数值上更好;将多个小数字添加到一个大总数中会失去精度。当然,这将花费 1 mul、2 FMA 和 1 add。
具有这种性质的代码:
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
不打包任何数据(4vmovsd
加载数据,1个存储),执行3vfmaddXXXsd
。尽管如此,我将其矢量化的动机是它可以只使用一个 vfmadd231pd
来完成。我使用 AVX2 的内在函数编写此代码的“最干净”尝试是:
void foo_intrin(double *restrict A, double *restrict x,
double *restrict y) {
__m256d __vop0, __vop1,__vop2;
__m128d __lo256, __hi256;
// THE ISSUE
__vop0 = _mm256_maskload_pd(&A[4], _mm256_set_epi64x(0,-1,-1,-1));
__vop1 = _mm256_mask_i64gather_pd(_mm256_setzero_pd(), &x[5],
_mm256_set_epi64x(0,3368, 1447, 0),
_mm256_set_pd(0,-1,-1,-1), 8);
// 1 vs 3 FMADD, "the gain"
__vop2 = _mm256_fmadd_pd(__vop0, __vop1, __vop2);
// reducing 4 double elements:
// Peter Cordes' answer
__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*, 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, xmm2
vfmadd132pd ymm0, ymm2, ymm1
vmovapd xmm1, xmm0
vextractf128 xmm0, ymm0, 0x1
vaddpd xmm0, xmm0, xmm1
vunpckhpd xmm1, xmm0, xmm0
vaddpd xmm0, xmm0, xmm1
vaddsd xmm0, 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 does not use
maskload
in AVX2 since "masked instructions are very slow in instruction sets prior to AVX512". However, in uops.info 对于 Skylake(“常规”,无 AVX-512)而言:- VMOVAPD(YMM,M256),例如
_mm256_load_pd
具有延迟 [≤5;≤8] 和 0.5 的吞吐量。 - VMASKMOVPD(YMM、YMM、M256),例如
_mm256_maskload_pd
具有延迟 [1;≤9] 和 0.5 的吞吐量,但以两个微指令而不是一个微指令解码。这个差距有这么大吗?用不同的方式打包更好吗?
- VMOVAPD(YMM,M256),例如
- 关于
mask_gather
-fashion 说明,据我对上述所有文档的了解,无论是否使用遮罩,它都提供相同的性能,是否正确? uops.info 和 Intel Intrinsics Guide 报告相同的性能和 ASM 表格;我可能遗漏了什么。- 是否在所有情况下
gather
都比“简单的”set
更好?用内在术语说话。我知道set
根据数据类型生成vmov
类型的指令(例如,如果数据是常量,它可能只加载一个地址,如.LC0
、.LC1
和.LC2
).
- 是否在所有情况下
- 根据 Intel Intrinsics,
_mm256_shuffle_pd
和_mm256_unpackhi_pd
具有相同的延迟和吞吐量;第一个生成vpermildp
,第二个生成vunpckhpd
,uops.info 也报告相同的值。有区别吗?
最后但同样重要的是,这种特别的矢量化是否值得?我只是指的不是我的内在代码,而是像这样矢量化代码的概念。我怀疑有太多的数据移动无法执行,比较干净的代码编译器,一般来说,产生,所以我关心的是改进打包非连续数据的方式。
vfmaddXXXsd
和 pd
指令是“便宜的”(单 uop,2/时钟吞吐量),甚至比洗牌(Intel CPU 上的 1/时钟吞吐量)或收集负载更便宜。 https://uops.info/。加载操作也是 2/clock,所以很多标量加载(尤其是来自同一缓存行)非常便宜,请注意其中 3 个可以折叠到 FMA 的内存源操作数中。
最坏的情况是,打包 4 (x2) 个完全不连续的输入然后手动分散输出与仅使用标量负载和标量 FMA 相比绝对不值得(尤其是当它允许 FMA 的内存源操作数时) .
你的情况远非最坏的情况;您有 1 个输入的 3 个连续元素。如果你知道你可以安全地加载 4 个元素而不会有接触未映射页面的风险,那么就可以处理该输入。 (而且您始终可以使用 maskload)。但是另一个向量仍然是不连续的,可能是加速的障碍。
如果通过洗牌比普通标量需要更多的总指令(实际上是 uops),通常是不值得的。 And/or 如果洗牌吞吐量是比标量版本中的任何瓶颈都严重。
(vgatherdpd
为此计算了尽可能多的指令,是多 uop,每次加载执行 1 次缓存访问。此外,您还必须加载索引的常量向量,而不是将偏移量硬编码到寻址模式中.
此外,在 AMD CPU 上收集速度非常慢,即使是 Zen2。在 AVX512 之前我们根本没有散射,即使在 Ice Lake 上也很慢。不过,您的案例不需要散点,只需要水平总和。这将涉及更多的洗牌和 vaddpd
/ sd
。 因此,即使使用 maskload + gather 作为输入,将 3 个产品放在单独的向量元素中对您来说也不是特别方便。)
一点点 SIMD(不是整个数组,只是一些操作)可能会有帮助,但这看起来不像是重大胜利的案例之一。也许有一些值得做的事情,比如用 load + a shuffle 代替 2 loads。或者可以通过将 前 的 3 个产品加到输出而不是 3 个 FMA 链来求和来缩短 y[5]
的延迟链。在累加器可以容纳大量数字的情况下,这甚至可能在数值上更好;将多个小数字添加到一个大总数中会失去精度。当然,这将花费 1 mul、2 FMA 和 1 add。