在保持 YMM 部分不变的情况下对 XMM 寄存器执行 PSRLDQ 的正确内在序列是什么?
What is the correct intrinsic sequence to do PSRLDQ to an XMM register while keeping the YMM part unchanged?
假设 xmm0
是第一个参数,这就是我想要生成的代码类型。
psrldq xmm0, 1
vpermq ymm0, ymm0, 4eh
ret
我用内在函数写的。
__m256i f_alias(__m256i p) {
*(__m128i *)&p = _mm_bsrli_si128(*(__m128i *)&p, 1);
return _mm256_permute4x64_epi64(p, 0x4e);
}
这是clang
的结果,没问题。
f_alias: #clang
vpsrldq xmm1, xmm0, 1
vperm2i128 ymm0, ymm0, ymm1, 33
ret
但是 gcc
产生错误代码。
f_alias: #gcc
push rbp
vpsrldq xmm2, xmm0, 1
mov rbp, rsp
and rsp, -32
vmovdqa YMMWORD PTR [rsp-32], ymm0
vmovdqa XMMWORD PTR [rsp-32], xmm2
vpermq ymm0, YMMWORD PTR [rsp-32], 78
leave
ret
我尝试了不同的版本。
__m256i f_insert(__m256i p) {
__m128i xp = _mm256_castsi256_si128(p);
xp = _mm_bsrli_si128(xp, 1);
p = _mm256_inserti128_si256(p, xp, 0);
return _mm256_permute4x64_epi64(p, 0x4e);
}
clang
生成相同的代码。
f_insert: #clang
vpsrldq xmm1, xmm0, 1
vperm2i128 ymm0, ymm0, ymm1, 33
ret
但是gcc
在翻译内在函数时过于直白。
f_insert: #gcc
vpsrldq xmm1, xmm0, 1
vinserti128 ymm0, ymm0, xmm1, 0x0
vpermq ymm0, ymm0, 78
ret
用内部函数编写此操作的好方法是什么?如果可能的话,我想让 gcc
产生像 clang
这样的好代码。
一些附带问题。
- 将
PSRLDQ
与 AVX 代码混合使用是否不好?像 clang
那样使用 VPSRLDQ
更好吗?如果使用 PSRLDQ
没有任何问题,这似乎是一种更简单的方法,因为它不会像 VEX
版本那样将 YMM
部分归零。
- 同时使用
F
和 I
指令的目的是什么,它们似乎无论如何都做同样的工作,例如,VINSERTI128
/VINSERTF128
或 VPERMI128
/VPERMF128
?
我是个傻瓜。 clang
给了我一个答案,为什么我没有注意到?
vpsrldq xmm1, xmm0, 1
vperm2i128 ymm0, ymm0, ymm1, 33
ret
这个序列很简单,
__m256i f_____(__m256i p) {
__m128i xp = _mm256_castsi256_si128(p);
xp = _mm_bsrli_si128(xp, 1);
__m256i _p = _mm256_castsi128_si256(xp);
return _mm256_permute2x128_si256(p, _p, 0x21);
}
而且确实 gcc
也能产生高效的代码..
f_____:
vpsrldq xmm1, xmm0, 1
vperm2i128 ymm0, ymm0, ymm1, 33
ret
Skylake 上的最佳 asm 将使用旧版 SSE psrldq xmm0, 1
,其效果是将向量的其余部分作为数据依赖项处理而保持不变。 (在寄存器上,指令无论如何都会读取,因为这不是 movdqa
或其他东西)。但这将是 disastrous on Haswell, or on Ice Lake,当 legacy-SSE 指令在任何 YMM 具有“脏”上半部分时写入 XMM 寄存器时,两者都会向“已保存的上半部分”状态进行代价高昂的转换。我不确定 Zen1 或 Zen2/3/4... 如何处理它。
在 Skylake 上几乎一样好,在其他任何地方都是最佳的,是 copy-and-shift 然后 vpblendd
复制原始的高半部分,因为你不需要在 128- 之间移动任何数据位通道。 (您版本中的 _mm256_permute4x64_epi64(p, 0x4e);
与您在标题中询问的操作分开 lane-swap 。如果您还想要其他内容,请继续使用 vperm2i128
合并作为其中的一部分lane-swap。如果不是,就是一个错误。)
vpblendd
比任何 shuffle 更有效,能够 运行 在多个执行端口中的任何一个上,在 Intel CPU 上有 1 个周期延迟。 (Lane-crossing 像 vperm2i128
这样的洗牌在主流 Intel 上是 1 uop / 3 周期延迟,在 AMD 和 Alder Lake 的 E-cores 上明显更差。https://uops.info/)相比之下, 可变 带有矢量控制的混合通常更昂贵,但即时混合非常好。
是的,在某些 CPU 上使用 XMM (__m128i
) 移位比移位两半然后与原始混合更有效。这将减少使用 cast intrinsics 的输入,但如果编译器没有优化它,你会在 Zen1 和 Alder Lake E-cores 上浪费 uops,其中 vpsrldq ymm
的每一半都需要一个单独的 uop .
__m256i rshift_lowhalf_by_1(__m256i v)
{
__m128i low = _mm256_castsi256_si128(v);
low = _mm_bsrli_si128(low, 1);
return _mm256_blend_epi32(v, _mm256_castsi128_si256(low), 0x0F);
}
gcc/clang 使用 xmm byte-shift 和 YMM vpblendd
编译它(Godbolt)。 (Clang 翻转立即数并使用相反的源寄存器,但相同的区别。)
vpblendd
在 Zen1 上是 2 微指令,因为它必须处理向量的两半。对于特殊情况,解码器不会立即查看,例如保留整个矢量的一半。而且它仍然可以复制到一个单独的目的地,不一定会覆盖任何一个源 in-place。出于类似的原因,不幸的是,vinserti128
也是 2 微指令。 (vextracti128
在 Zen1 上只有 1 uop;我希望 vinserti128
只会是 1,并在检查 uops.info 之前写了以下版本):
// don't use on any CPU *except* Zen1, or an Alder Lake pinned to an E-core.
__m256i rshift_alder_lake_e(__m256i v)
{
__m128i low = _mm256_castsi256_si128(v);
low = _mm_bsrli_si128(low, 1);
return _mm256_inserti128_si256(v, low, 0); // still 2 uops on Zen1 or Alder Lake, same as vpblendd
// clang optimizes this to vpblendd even with -march=znver1. That's good for most uarches, break-even for Zen1, so that's fine.
}
Alder Lake E-cores 可能会有一点好处,其中 vinserti128
延迟被列为 [1;2]
而不是 vpblendd
的平坦 2
.但是由于任何 Alder Lake 系统也会有 P 核心,你实际上并不想要用户 vinserti128
因为它在其他方面都更糟糕。
What is the purpose of having both VINSERTI128/VPERMI128 and VINSERTF128/VPERMF128?
vinserti128
与内存源仅进行 128 位加载,vperm2i128
进行 256 位加载,这可能会跨越高速缓存行或页面边界以获取您甚至不去的数据使用。
在 load/store 执行单元只有 128 位宽数据路径缓存的 AVX CPU 上(如 Sandy/Ivy Bridge),这是一个显着的好处。
在洗牌单元只有 128 位宽的 CPU 上(如本答案中讨论的 Zen1),vperm2i128
的 2 个完整源输入和任意洗牌让它变得更昂贵(除非我猜你有更聪明的解码器,它们发出许多微指令来移动向量的一半,这取决于立即数)。
例如Zen1 的 vperm2i/f128
为 8 微指令,延迟为 2c,吞吐量为 3c!。 (具有 256 位执行单元的 Zen2 将其提高到 1 uop,3c 延迟,1c 吞吐量)。参见 https://uops.info/
What is the purpose of having both F and I instructions which seems to do the same job anyway
与往常一样 (dating back to stuff like SSE1 orps
vs. SSE2 pxor
/ orpd
),让 CPU 在 SIMD-integer 和 SIMD-FP.
上有不同的 bypass-forwarding 域
Shuffle 单元很昂贵,因此通常值得在 FP 和整数之间共享它们(英特尔现在这样做的方式在 vpaddd
指令之间使用 vperm2f128
时不会产生额外的延迟)。
但是例如混合很简单,所以可能有不同的 FP 和整数混合单元,并且 paddd
指令之间的 blendvps
有延迟惩罚。 (参见 https://agner.org/optimize/)
假设 xmm0
是第一个参数,这就是我想要生成的代码类型。
psrldq xmm0, 1
vpermq ymm0, ymm0, 4eh
ret
我用内在函数写的。
__m256i f_alias(__m256i p) {
*(__m128i *)&p = _mm_bsrli_si128(*(__m128i *)&p, 1);
return _mm256_permute4x64_epi64(p, 0x4e);
}
这是clang
的结果,没问题。
f_alias: #clang
vpsrldq xmm1, xmm0, 1
vperm2i128 ymm0, ymm0, ymm1, 33
ret
但是 gcc
产生错误代码。
f_alias: #gcc
push rbp
vpsrldq xmm2, xmm0, 1
mov rbp, rsp
and rsp, -32
vmovdqa YMMWORD PTR [rsp-32], ymm0
vmovdqa XMMWORD PTR [rsp-32], xmm2
vpermq ymm0, YMMWORD PTR [rsp-32], 78
leave
ret
我尝试了不同的版本。
__m256i f_insert(__m256i p) {
__m128i xp = _mm256_castsi256_si128(p);
xp = _mm_bsrli_si128(xp, 1);
p = _mm256_inserti128_si256(p, xp, 0);
return _mm256_permute4x64_epi64(p, 0x4e);
}
clang
生成相同的代码。
f_insert: #clang
vpsrldq xmm1, xmm0, 1
vperm2i128 ymm0, ymm0, ymm1, 33
ret
但是gcc
在翻译内在函数时过于直白。
f_insert: #gcc
vpsrldq xmm1, xmm0, 1
vinserti128 ymm0, ymm0, xmm1, 0x0
vpermq ymm0, ymm0, 78
ret
用内部函数编写此操作的好方法是什么?如果可能的话,我想让 gcc
产生像 clang
这样的好代码。
一些附带问题。
- 将
PSRLDQ
与 AVX 代码混合使用是否不好?像clang
那样使用VPSRLDQ
更好吗?如果使用PSRLDQ
没有任何问题,这似乎是一种更简单的方法,因为它不会像VEX
版本那样将YMM
部分归零。 - 同时使用
F
和I
指令的目的是什么,它们似乎无论如何都做同样的工作,例如,VINSERTI128
/VINSERTF128
或VPERMI128
/VPERMF128
?
我是个傻瓜。 clang
给了我一个答案,为什么我没有注意到?
vpsrldq xmm1, xmm0, 1
vperm2i128 ymm0, ymm0, ymm1, 33
ret
这个序列很简单,
__m256i f_____(__m256i p) {
__m128i xp = _mm256_castsi256_si128(p);
xp = _mm_bsrli_si128(xp, 1);
__m256i _p = _mm256_castsi128_si256(xp);
return _mm256_permute2x128_si256(p, _p, 0x21);
}
而且确实 gcc
也能产生高效的代码..
f_____:
vpsrldq xmm1, xmm0, 1
vperm2i128 ymm0, ymm0, ymm1, 33
ret
Skylake 上的最佳 asm 将使用旧版 SSE psrldq xmm0, 1
,其效果是将向量的其余部分作为数据依赖项处理而保持不变。 (在寄存器上,指令无论如何都会读取,因为这不是 movdqa
或其他东西)。但这将是 disastrous on Haswell, or on Ice Lake,当 legacy-SSE 指令在任何 YMM 具有“脏”上半部分时写入 XMM 寄存器时,两者都会向“已保存的上半部分”状态进行代价高昂的转换。我不确定 Zen1 或 Zen2/3/4... 如何处理它。
在 Skylake 上几乎一样好,在其他任何地方都是最佳的,是 copy-and-shift 然后 vpblendd
复制原始的高半部分,因为你不需要在 128- 之间移动任何数据位通道。 (您版本中的 _mm256_permute4x64_epi64(p, 0x4e);
与您在标题中询问的操作分开 lane-swap 。如果您还想要其他内容,请继续使用 vperm2i128
合并作为其中的一部分lane-swap。如果不是,就是一个错误。)
vpblendd
比任何 shuffle 更有效,能够 运行 在多个执行端口中的任何一个上,在 Intel CPU 上有 1 个周期延迟。 (Lane-crossing 像 vperm2i128
这样的洗牌在主流 Intel 上是 1 uop / 3 周期延迟,在 AMD 和 Alder Lake 的 E-cores 上明显更差。https://uops.info/)相比之下, 可变 带有矢量控制的混合通常更昂贵,但即时混合非常好。
是的,在某些 CPU 上使用 XMM (__m128i
) 移位比移位两半然后与原始混合更有效。这将减少使用 cast intrinsics 的输入,但如果编译器没有优化它,你会在 Zen1 和 Alder Lake E-cores 上浪费 uops,其中 vpsrldq ymm
的每一半都需要一个单独的 uop .
__m256i rshift_lowhalf_by_1(__m256i v)
{
__m128i low = _mm256_castsi256_si128(v);
low = _mm_bsrli_si128(low, 1);
return _mm256_blend_epi32(v, _mm256_castsi128_si256(low), 0x0F);
}
gcc/clang 使用 xmm byte-shift 和 YMM vpblendd
编译它(Godbolt)。 (Clang 翻转立即数并使用相反的源寄存器,但相同的区别。)
vpblendd
在 Zen1 上是 2 微指令,因为它必须处理向量的两半。对于特殊情况,解码器不会立即查看,例如保留整个矢量的一半。而且它仍然可以复制到一个单独的目的地,不一定会覆盖任何一个源 in-place。出于类似的原因,不幸的是,vinserti128
也是 2 微指令。 (vextracti128
在 Zen1 上只有 1 uop;我希望 vinserti128
只会是 1,并在检查 uops.info 之前写了以下版本):
// don't use on any CPU *except* Zen1, or an Alder Lake pinned to an E-core.
__m256i rshift_alder_lake_e(__m256i v)
{
__m128i low = _mm256_castsi256_si128(v);
low = _mm_bsrli_si128(low, 1);
return _mm256_inserti128_si256(v, low, 0); // still 2 uops on Zen1 or Alder Lake, same as vpblendd
// clang optimizes this to vpblendd even with -march=znver1. That's good for most uarches, break-even for Zen1, so that's fine.
}
Alder Lake E-cores 可能会有一点好处,其中 vinserti128
延迟被列为 [1;2]
而不是 vpblendd
的平坦 2
.但是由于任何 Alder Lake 系统也会有 P 核心,你实际上并不想要用户 vinserti128
因为它在其他方面都更糟糕。
What is the purpose of having both VINSERTI128/VPERMI128 and VINSERTF128/VPERMF128?
vinserti128
与内存源仅进行 128 位加载,vperm2i128
进行 256 位加载,这可能会跨越高速缓存行或页面边界以获取您甚至不去的数据使用。
在 load/store 执行单元只有 128 位宽数据路径缓存的 AVX CPU 上(如 Sandy/Ivy Bridge),这是一个显着的好处。
在洗牌单元只有 128 位宽的 CPU 上(如本答案中讨论的 Zen1),vperm2i128
的 2 个完整源输入和任意洗牌让它变得更昂贵(除非我猜你有更聪明的解码器,它们发出许多微指令来移动向量的一半,这取决于立即数)。
例如Zen1 的 vperm2i/f128
为 8 微指令,延迟为 2c,吞吐量为 3c!。 (具有 256 位执行单元的 Zen2 将其提高到 1 uop,3c 延迟,1c 吞吐量)。参见 https://uops.info/
What is the purpose of having both F and I instructions which seems to do the same job anyway
与往常一样 (dating back to stuff like SSE1 orps
vs. SSE2 pxor
/ orpd
),让 CPU 在 SIMD-integer 和 SIMD-FP.
Shuffle 单元很昂贵,因此通常值得在 FP 和整数之间共享它们(英特尔现在这样做的方式在 vpaddd
指令之间使用 vperm2f128
时不会产生额外的延迟)。
但是例如混合很简单,所以可能有不同的 FP 和整数混合单元,并且 paddd
指令之间的 blendvps
有延迟惩罚。 (参见 https://agner.org/optimize/)