如何在没有 AVX2 的情况下使用字节中的位来设置 ymm 寄存器中的双字? (vmovmskps 的倒数)
How to use bits in a byte to set dwords in ymm register without AVX2? (Inverse of vmovmskps)
我想要实现的是基于字节中的每一位,设置为 ymm 寄存器(或内存位置)中每个双字中的所有内容
例如
al = 0110 0001
ymm0 = 0x00000000 FFFFFFFF FFFFFFFF 00000000 00000000 00000000 00000000 FFFFFFFF
即vmovmskps eax, ymm0
/ _mm256_movemask_ps
的逆运算,将位图转换为矢量蒙版。
我认为有一些 sse/avx 指令可以相对简单地完成此操作,但我一直无法解决。最好与沙桥兼容,所以没有 avx2。
前言:我知道这不符合问题的(全部)要求,所以这个答案是不可接受的。 我只是post 供以后参考。
有一个名为 VPMOVM2B 的新 AVX512(VL|BW) 指令,它可以在 恰好一个 指令中执行您想要的操作:
VPMOVM2B ymm1, k1
Sets each byte in YMM1 to all 1’s or all 0’s based on the value of the corresponding bit in k1.
没法测试,不过应该是你想要的。
如果 AVX2 可用,请参阅 is there an inverse instruction to the movemask instruction in intel avx2? 以获取使用整数 SIMD 的更高效版本。您可以使用该想法并将您的位图拆分为两个 4 位块以与 LUT 一起使用。这可能表现得相当好:vinsertf128
在 Sandybridge 上每个时钟吞吐量为 1,在 Haswell/Skylake.
上每 0.5c 有一个
使用 AVX1 的 SIMD-integer 解决方案可以对 high/low 向量的一半执行相同的工作两次(2x 广播位图,2x 屏蔽它,2x vpcmpeqd xmm
),然后 vinsertf128
,但那有点糟透了。
您可以考虑使用 vpbroadcastd ymm0, mem
/ vpand ymm0, mask
/ vpcmpeqd dst, ymm0, mask
将 AVX2 版本与仅 AVX1 版本分开 ,因为这 非常 高效,尤其是当您从内存中加载位图并且可以读取位图的整个双字时。 (Broadcast-loads 双字或双字不需要 ALU 洗牌,因此值得多读)。 mask
是 set_epi32(1<<7, 1<<6, 1<<5< ..., 1<<0)
,你可以用 vpmovzxbd ymm, qword [constant]
加载它,所以它只需要 8 个字节的数据内存来存储 8 个元素。
Intrinsics 版本,请参阅下面的解释和 asm 版本。 按照我们的预期进行编译 on Godbolt with gcc/clang -march=sandybridge
#include <immintrin.h>
// AVX2 can be significantly more efficient, doing this with integer SIMD
// Especially for the case where the bitmap is in an integer register, not memory
// It's fine if `bitmap` contains high garbage; make sure your C compiler broadcasts from a dword in memory if possible instead of integer load with zero extension.
// e.g. __m256 _mm256_broadcast_ss(float *a); or memcpy to unsigned.
// Store/reload is not a bad strategy vs. movd + 2 shuffles so maybe just do it even if the value might be in a register; it will force some compilers to store/broadcast-load. But it might not be type-punning safe even though it's an intrinsic.
// Low bit -> element 0, etc.
__m256 inverse_movemask_ps_avx1(unsigned bitmap)
{
// if you know DAZ is off: don't OR, just AND/CMPEQ with subnormal bit patterns
// FTZ is irrelevant, we only use bitwise booleans and CMPPS
const __m256 exponent = _mm256_set1_ps(1.0f); // set1_epi32(0x3f800000)
const __m256 bit_select = _mm256_castsi256_ps(
_mm256_set_epi32( // exponent + low significand bits
0x3f800000 + (1<<7), 0x3f800000 + (1<<6),
0x3f800000 + (1<<5), 0x3f800000 + (1<<4),
0x3f800000 + (1<<3), 0x3f800000 + (1<<2),
0x3f800000 + (1<<1), 0x3f800000 + (1<<0)
));
// bitmap |= 0x3f800000; // more efficient to do this scalar, but only if the data was in a register to start with
__m256 bcast = _mm256_castsi256_ps(_mm256_set1_epi32(bitmap));
__m256 ored = _mm256_or_ps(bcast, exponent);
__m256 isolated = _mm256_and_ps(ored, bit_select);
return _mm256_cmp_ps(isolated, bit_select, _CMP_EQ_OQ);
}
如果我们有创意,我们可以使用 AVX1 FP 指令来做同样的事情。 AVX1 有双字广播(vbroadcastss ymm0, mem
)和布尔值(vandps
).这将产生有效的位模式 single-precision floats,因此我们可以使用 vcmpeqps
,但如果我们将位图位留在元素的底部,它们都是非正规的。这在 Sandybridge 上可能实际上没问题:比较 非正规化可能没有惩罚。但是如果你的代码曾经 运行s with DAZ (denormals-are-zero),它会崩溃,所以我们应该避免这种情况。
我们可以 vpor
在屏蔽之前或之后设置指数, 或者我们可以将位图向上移动到 IEEE floating-point 的 8 位指数字段格式。如果您的位图从一个整数寄存器开始,移动它会很好,因为 movd
之前的 shl eax, 23
很便宜。
但如果它在内存中启动,则意味着放弃使用便宜的 vbroadcastss
负载。或者你可以 broadcast-load 到 xmm,vpslld xmm0, xmm0, 23
/ vinsertf128 ymm0, xmm0, 1
。但这仍然比 vbroadcastss
/ vorps
/ vandps
/ vcmpeqps
差
(store/reload 之前的标量 OR 解决了同样的问题。)
所以:
# untested
# pointer to bitmap in rdi
inverse_movemask:
vbroadcastss ymm0, [rdi]
vorps ymm0, ymm0, [set_exponent] ; or hoist this constant out with a broadcast-load
vmovaps ymm7, [bit_select] ; hoist this out of any loop, too
vandps ymm0, ymm0, ymm7
; ymm0 exponent = 2^0, mantissa = 0 or 1<<i where i = element number
vcmpeqps ymm0, ymm0, ymm7
ret
section .rodata
ALIGN 32
; low bit -> low element. _mm_setr order
bit_select: dd 0x3f800000 + (1<<0), 0x3f800000 + (1<<1)
dd 0x3f800000 + (1<<2), 0x3f800000 + (1<<3)
dd 0x3f800000 + (1<<4), 0x3f800000 + (1<<5)
dd 0x3f800000 + (1<<6), 0x3f800000 + (1<<7)
set_exponent: times 8 dd 0x3f800000 ; 1.0f
; broadcast-load this instead of duplicating it in memory if you're hoisting it.
而不是 broadcast-loading set_exponent
,你可以改为随机播放 bit_select
:只要设置了 0x3f800000
位,元素 0 是否无关紧要还设置位 3 或其他内容,而不是位 0。因此 vpermilps
或 vshufps
到 copy-and-shuffle 会起作用。
或者如果位图在整数寄存器中开始,您可以使用标量 OR 并避免使用该向量常量。 (以及更多端口上的标量或 运行s。)
# alternate top of the function for input in an integer reg, not pointer.
or edi, 0x3f800000
mov [rsp-4], edi ; red-zone
vbroadcastss ymm0, [rsp-4]
;; skip the vorps
Store/reload 可能具有与 vmovd
(1c)、vpshufd xmm
(1c)、vinsertf128
(3c) 相似的延迟,从整数寄存器广播总共需要 5c在 Intel SnB-family 上没有 AVX2 或 AVX512。而且它更少 fused-domain 微指令(2 而不是 3),并且不会到达随机端口(SnB-family 上的 p5 为 3 微指令)。您的选择可能取决于周围代码中是否存在 load/store 压力或端口 5 压力。
(SnB/IvB 在 2 个端口上有 integer-shuffle 个单元,只有 FP 洗牌限制为 1。Haswell 删除了 p5 之外的洗牌单元。
但是除非你进行动态调度以避免在 AVX2 CPU 上使用它,否则你可能希望 调整 以适应较新的 CPU,同时仍保持与仅 AVX1 CPU 的兼容性。)
如果你打算用洗牌做一个 ALU 广播(就像 clang 做的那样),你可以借用 clang 做一个 vorps xmm
的技巧来在拆分 256 位操作的 AMD CPU 上保存一个 uop,并且允许更窄的 OR 常数。但这毫无意义:要么你的值在整数寄存器中(你可以在其中使用标量 or
),要么它在你应该使用 vbroadcastss ymm
的内存中。我想如果在 Zen2 之前针对 AMD 进行调整,您可能会考虑广播 XMM 负载、VPOR XMM,然后是 vinsertf128。
https://www.h-schmidt.net/FloatConverter/IEEE754.html 是一个有用的 IEEE754 FP 值 <-> 十六进制位模式转换器,如果您想检查某些 FP 位模式代表什么值。
vcmpeqps
在所有 Intel CPU 上具有与 vaddps
相同的延迟和吞吐量。 (这 不是 巧合;它们 运行 在同一个执行单元上。这意味着在 SnB-Broadwell 上有 3 个周期的延迟,在 Skylake 上有 4 个周期的延迟。但是vpcmpeqd
只有1c延迟。
因此此方法具有良好的吞吐量(仅比 AVX2 整数多 1 uop,其中不需要 vorps
),但延迟较差 3 个周期,或 Skylake 上的 4 个周期。
但是比较浮点数不是危险的或不好的做法吗?
当比较输入之一是计算的舍入结果(例如 vaddps
或 vmulps
的输出)时,完全相等的比较可能会产生意想不到的结果。 Bruce Dawson 关于一般 FP 数学和特别是 x86 的博客系列非常出色,特别是 Comparing Floating Point Numbers,2012 年版
。但在这种情况下,我们正在控制 FP bit-patterns,并且没有舍入。
Non-NaN 具有相同 bit-pattern 的 FP 值将始终比较相等。
具有不同 bit-patterns 的 FP 值将始终与 not-equal 进行比较,除了 -0.0
和 +0.0
(仅符号位不同),以及非规范化值DAZ 模式。后者是我们使用 vpor
的原因;如果您知道 DAZ 已禁用并且您的 FP 硬件不需要辅助比较非正规化,则可以跳过它。 (IIRC,Sandybridge 没有,甚至可以在没有帮助的情况下添加/子非规范化。当英特尔硬件需要微码协助时,通常是在从正常输入产生非规范化结果时,但比较不会产生 FP 结果。)
我想要实现的是基于字节中的每一位,设置为 ymm 寄存器(或内存位置)中每个双字中的所有内容
例如
al = 0110 0001
ymm0 = 0x00000000 FFFFFFFF FFFFFFFF 00000000 00000000 00000000 00000000 FFFFFFFF
即vmovmskps eax, ymm0
/ _mm256_movemask_ps
的逆运算,将位图转换为矢量蒙版。
我认为有一些 sse/avx 指令可以相对简单地完成此操作,但我一直无法解决。最好与沙桥兼容,所以没有 avx2。
前言:我知道这不符合问题的(全部)要求,所以这个答案是不可接受的。 我只是post 供以后参考。
有一个名为 VPMOVM2B 的新 AVX512(VL|BW) 指令,它可以在 恰好一个 指令中执行您想要的操作:
VPMOVM2B ymm1, k1
Sets each byte in YMM1 to all 1’s or all 0’s based on the value of the corresponding bit in k1.
没法测试,不过应该是你想要的。
如果 AVX2 可用,请参阅 is there an inverse instruction to the movemask instruction in intel avx2? 以获取使用整数 SIMD 的更高效版本。您可以使用该想法并将您的位图拆分为两个 4 位块以与 LUT 一起使用。这可能表现得相当好:vinsertf128
在 Sandybridge 上每个时钟吞吐量为 1,在 Haswell/Skylake.
使用 AVX1 的 SIMD-integer 解决方案可以对 high/low 向量的一半执行相同的工作两次(2x 广播位图,2x 屏蔽它,2x vpcmpeqd xmm
),然后 vinsertf128
,但那有点糟透了。
您可以考虑使用 vpbroadcastd ymm0, mem
/ vpand ymm0, mask
/ vpcmpeqd dst, ymm0, mask
将 AVX2 版本与仅 AVX1 版本分开 ,因为这 非常 高效,尤其是当您从内存中加载位图并且可以读取位图的整个双字时。 (Broadcast-loads 双字或双字不需要 ALU 洗牌,因此值得多读)。 mask
是 set_epi32(1<<7, 1<<6, 1<<5< ..., 1<<0)
,你可以用 vpmovzxbd ymm, qword [constant]
加载它,所以它只需要 8 个字节的数据内存来存储 8 个元素。
Intrinsics 版本,请参阅下面的解释和 asm 版本。 按照我们的预期进行编译 on Godbolt with gcc/clang -march=sandybridge
#include <immintrin.h>
// AVX2 can be significantly more efficient, doing this with integer SIMD
// Especially for the case where the bitmap is in an integer register, not memory
// It's fine if `bitmap` contains high garbage; make sure your C compiler broadcasts from a dword in memory if possible instead of integer load with zero extension.
// e.g. __m256 _mm256_broadcast_ss(float *a); or memcpy to unsigned.
// Store/reload is not a bad strategy vs. movd + 2 shuffles so maybe just do it even if the value might be in a register; it will force some compilers to store/broadcast-load. But it might not be type-punning safe even though it's an intrinsic.
// Low bit -> element 0, etc.
__m256 inverse_movemask_ps_avx1(unsigned bitmap)
{
// if you know DAZ is off: don't OR, just AND/CMPEQ with subnormal bit patterns
// FTZ is irrelevant, we only use bitwise booleans and CMPPS
const __m256 exponent = _mm256_set1_ps(1.0f); // set1_epi32(0x3f800000)
const __m256 bit_select = _mm256_castsi256_ps(
_mm256_set_epi32( // exponent + low significand bits
0x3f800000 + (1<<7), 0x3f800000 + (1<<6),
0x3f800000 + (1<<5), 0x3f800000 + (1<<4),
0x3f800000 + (1<<3), 0x3f800000 + (1<<2),
0x3f800000 + (1<<1), 0x3f800000 + (1<<0)
));
// bitmap |= 0x3f800000; // more efficient to do this scalar, but only if the data was in a register to start with
__m256 bcast = _mm256_castsi256_ps(_mm256_set1_epi32(bitmap));
__m256 ored = _mm256_or_ps(bcast, exponent);
__m256 isolated = _mm256_and_ps(ored, bit_select);
return _mm256_cmp_ps(isolated, bit_select, _CMP_EQ_OQ);
}
如果我们有创意,我们可以使用 AVX1 FP 指令来做同样的事情。 AVX1 有双字广播(vbroadcastss ymm0, mem
)和布尔值(vandps
).这将产生有效的位模式 single-precision floats,因此我们可以使用 vcmpeqps
,但如果我们将位图位留在元素的底部,它们都是非正规的。这在 Sandybridge 上可能实际上没问题:比较 非正规化可能没有惩罚。但是如果你的代码曾经 运行s with DAZ (denormals-are-zero),它会崩溃,所以我们应该避免这种情况。
我们可以 vpor
在屏蔽之前或之后设置指数, 或者我们可以将位图向上移动到 IEEE floating-point 的 8 位指数字段格式。如果您的位图从一个整数寄存器开始,移动它会很好,因为 movd
之前的 shl eax, 23
很便宜。
但如果它在内存中启动,则意味着放弃使用便宜的 vbroadcastss
负载。或者你可以 broadcast-load 到 xmm,vpslld xmm0, xmm0, 23
/ vinsertf128 ymm0, xmm0, 1
。但这仍然比 vbroadcastss
/ vorps
/ vandps
/ vcmpeqps
(store/reload 之前的标量 OR 解决了同样的问题。)
所以:
# untested
# pointer to bitmap in rdi
inverse_movemask:
vbroadcastss ymm0, [rdi]
vorps ymm0, ymm0, [set_exponent] ; or hoist this constant out with a broadcast-load
vmovaps ymm7, [bit_select] ; hoist this out of any loop, too
vandps ymm0, ymm0, ymm7
; ymm0 exponent = 2^0, mantissa = 0 or 1<<i where i = element number
vcmpeqps ymm0, ymm0, ymm7
ret
section .rodata
ALIGN 32
; low bit -> low element. _mm_setr order
bit_select: dd 0x3f800000 + (1<<0), 0x3f800000 + (1<<1)
dd 0x3f800000 + (1<<2), 0x3f800000 + (1<<3)
dd 0x3f800000 + (1<<4), 0x3f800000 + (1<<5)
dd 0x3f800000 + (1<<6), 0x3f800000 + (1<<7)
set_exponent: times 8 dd 0x3f800000 ; 1.0f
; broadcast-load this instead of duplicating it in memory if you're hoisting it.
而不是 broadcast-loading set_exponent
,你可以改为随机播放 bit_select
:只要设置了 0x3f800000
位,元素 0 是否无关紧要还设置位 3 或其他内容,而不是位 0。因此 vpermilps
或 vshufps
到 copy-and-shuffle 会起作用。
或者如果位图在整数寄存器中开始,您可以使用标量 OR 并避免使用该向量常量。 (以及更多端口上的标量或 运行s。)
# alternate top of the function for input in an integer reg, not pointer.
or edi, 0x3f800000
mov [rsp-4], edi ; red-zone
vbroadcastss ymm0, [rsp-4]
;; skip the vorps
Store/reload 可能具有与 vmovd
(1c)、vpshufd xmm
(1c)、vinsertf128
(3c) 相似的延迟,从整数寄存器广播总共需要 5c在 Intel SnB-family 上没有 AVX2 或 AVX512。而且它更少 fused-domain 微指令(2 而不是 3),并且不会到达随机端口(SnB-family 上的 p5 为 3 微指令)。您的选择可能取决于周围代码中是否存在 load/store 压力或端口 5 压力。
(SnB/IvB 在 2 个端口上有 integer-shuffle 个单元,只有 FP 洗牌限制为 1。Haswell 删除了 p5 之外的洗牌单元。 但是除非你进行动态调度以避免在 AVX2 CPU 上使用它,否则你可能希望 调整 以适应较新的 CPU,同时仍保持与仅 AVX1 CPU 的兼容性。)
如果你打算用洗牌做一个 ALU 广播(就像 clang 做的那样),你可以借用 clang 做一个 vorps xmm
的技巧来在拆分 256 位操作的 AMD CPU 上保存一个 uop,并且允许更窄的 OR 常数。但这毫无意义:要么你的值在整数寄存器中(你可以在其中使用标量 or
),要么它在你应该使用 vbroadcastss ymm
的内存中。我想如果在 Zen2 之前针对 AMD 进行调整,您可能会考虑广播 XMM 负载、VPOR XMM,然后是 vinsertf128。
https://www.h-schmidt.net/FloatConverter/IEEE754.html 是一个有用的 IEEE754 FP 值 <-> 十六进制位模式转换器,如果您想检查某些 FP 位模式代表什么值。
vcmpeqps
在所有 Intel CPU 上具有与 vaddps
相同的延迟和吞吐量。 (这 不是 巧合;它们 运行 在同一个执行单元上。这意味着在 SnB-Broadwell 上有 3 个周期的延迟,在 Skylake 上有 4 个周期的延迟。但是vpcmpeqd
只有1c延迟。
因此此方法具有良好的吞吐量(仅比 AVX2 整数多 1 uop,其中不需要 vorps
),但延迟较差 3 个周期,或 Skylake 上的 4 个周期。
但是比较浮点数不是危险的或不好的做法吗?
当比较输入之一是计算的舍入结果(例如 vaddps
或 vmulps
的输出)时,完全相等的比较可能会产生意想不到的结果。 Bruce Dawson 关于一般 FP 数学和特别是 x86 的博客系列非常出色,特别是 Comparing Floating Point Numbers,2012 年版
。但在这种情况下,我们正在控制 FP bit-patterns,并且没有舍入。
Non-NaN 具有相同 bit-pattern 的 FP 值将始终比较相等。
具有不同 bit-patterns 的 FP 值将始终与 not-equal 进行比较,除了 -0.0
和 +0.0
(仅符号位不同),以及非规范化值DAZ 模式。后者是我们使用 vpor
的原因;如果您知道 DAZ 已禁用并且您的 FP 硬件不需要辅助比较非正规化,则可以跳过它。 (IIRC,Sandybridge 没有,甚至可以在没有帮助的情况下添加/子非规范化。当英特尔硬件需要微码协助时,通常是在从正常输入产生非规范化结果时,但比较不会产生 FP 结果。)