有效地收集单个字节,以 4 的字节步长分隔
Efficiently gather individual bytes, separated by a byte-stride of 4
我正在尝试优化一种算法,该算法将处理大量数据集,这些数据集可能会从 AVX SIMD 指令中受益匪浅。不幸的是,输入内存布局对于所需的计算来说并不是最优的。必须重新排序信息,方法是从恰好相隔 4 个字节的各个字节组装 __m256i
个值:
开始编辑
我的目标 CPU 不支持 AVX2 指令,所以就像@Elalfer 和@PeterCordes 指出的那样,我不能使用 __m256i 值,代码必须转换为使用 __m128i 值相反)
编辑结束
内存中的数据集布局
Byte 0 | Byte 1 | Byte 2 | Byte 3
Byte 4 | Byte 5 | Byte 6 | Byte 7
...
Byte 120 | Byte 121 | Byte 122 | Byte 123
Byte 124 | Byte 125 | Byte 126 | Byte 127
__m256i
变量中的所需值:
| Byte 0 | Byte 4 | Byte 8 | ... | Byte 120 | Byte 124 |
除了这个简单的代码之外,还有更有效的方法来收集和重新排列跨步数据吗?
union { __m256i reg; uint8_t bytes[32]; } aux;
...
for( int i = 0; i < 32; i++ )
aux.bytes[i] = data[i * 4];
编辑:
我要优化的步骤是位列转置;换句话说,某一列的位(在我的数据排列中有 32 个可能的位列)应该成为单个 uint32_t
值,而其余位将被忽略。
我通过重新排列数据来执行转置,执行左移以将所需的位列作为每个子字节中的最高有效位,最后提取并 assemble 这些位到通过 _mm256_movemask_epi8()
内在的单个 uint32
_t 值。
您可以尝试展开该循环,这至少应该去掉循环体中的一次比较 (i<32)、一次递增 (i++) 和一次乘法 (i*4)。常量数组偏移量也可能比变量稍微快一些。但请注意,您的编译器无论如何都可能会生成类似(或更好)的代码,并启用适当的编译选项。
union { __m256i reg; uint8_t bytes[32]; } aux;
...
aux.bytes[0] = data[0];
aux.bytes[1] = data[3];
...
aux.bytes[31] = data[124];
其中一种方法是 - 用 _mm256_shuffle_epi8
打包字节,混合所有 _mm256_blend_epi32
结果向量(你需要做 4 次这样的加载+随机播放),然后做一个 32 位排列 _mm256_permutevar8x32_epi32
.
这是一个伪代码(希望你能想出shuffle masks):
L1 = load32byte(buf)
L2 = load32byte(buf+32)
L3 = load32byte(buf+64)
L4 = load32byte(buf+96)
// Pack 4 bytes in the corresponding 32bit DWORD in each lane and zero-out other bytes
L1 = shuffle(L1, mask_for_L1)
L2 = shuffle(L2, mask_for_L2)
L3 = shuffle(L3, mask_for_L3)
L4 = shuffle(L4, mask_for_L4)
// Vec = blend(blend(L1,L2),blend(L3,L4))
Vec = or(or(or(L1,L2),L3),L4)
Vec = permute(Vec) // fix DWORD order in the vector
更新:忘了我说的原因 "zero-out other bytes" - 这样你就可以用 or
替换 blend
更新:根据彼得在下面的评论重新安排 or
操作,减少了一个周期的延迟。
PS。我还建议您在进行位操作时查看 BMI 指令集。
我刚刚注意到编辑,其中有一个特殊情况的答案。
如果你需要对同一个数据做很多不同的位位置,那么你现在的方案很好。
如果您只需要 128B 内存中的一位位置(尤其是最高位位置),则可以使用 _mm256_movemask_ps
从每个 32b 元素中获取高位。然后在 GP 寄存器中组合四个 8bit 掩码。
一个好的编译器应该将其优化为:
vmovdqu ymm0, [buf + 0]
; to select a different bit:
; vpslld ymm0, ymm0, count ; count can be imm8 or the low byte of an xmm register
vmovmskps eax, ymm0
vmovdqu ymm0, [buf + 32]
vmovmskps ebx, ymm0
... ecx and edx
mov ah, bl
mov ch, dl
shl ecx, 16
or eax, ecx
只有在测试高位时这才好(因此您不需要在 vmovmsk
之前移动每个向量)。即便如此,这可能比其他解决方案更多的指令(和代码大小)。
原题答案:
与 Elalfer 的想法类似,但对 pack
指令使用洗牌单元而不是 pshufb
。此外,所有的 AND 都是独立的,因此它们可以并行执行。 Intel CPU 可以一次执行 3 个 AND,但只能执行一次 shuffle。 (或者在 pre-Haswell 上同时洗牌两次。)
// without AVX2: you won't really be able to
// do anything with a __m256i, only __m128i
// just convert everything to regular _mm_..., and leave out the final permute
mask = _mm256_set1_epi32(0x000000ff);
// same mask for all, and the load can fold into the AND
// You can write the load separately if you like, it'll still fold
L1 = and(mask, (buf)) // load and zero the bytes we don't want
L2 = and(mask, (buf+32))
L3 = and(mask, (buf+64))
L4 = and(mask, (buf+96))
// squish dwords from 2 concatenated regs down to words in 1 reg
pack12 = _mm256_packus_epi32(L1, L2);
pack34 = _mm256_packus_epi32(L3, L4);
packed = _mm256_packus_epi16(pack12, pack34); // note the different width: zero-padded-16 -> 8
Vec = permute(packed) // fix DWORD order in the vector (only needed for 256b version)
Vec = shift(Vec, bit_wanted)
bitvec = movemask(Vec)
// shift:
// I guess word or dword granularity is fine, since byte granularity isn't available.
// You only care about the high bit, so it doesn't matter than you're not shifting zeroes into the bottom of each byte.
// _mm_slli_epi32(Vec, imm8): 1 uop, 1c latency if your count is a compile-time constant.
// _mm_sll_epi32 (Vec, _mm_cvtsi32_si128(count)): 2uop 2c latency if it's variable.
// *not* _mm_sllv_epi32(): slower: different shift count for each element.
如果您只使用 AVX(如您所说)执行此操作,那么您将没有可用的 256b 整数指令。只需构建 128b 个向量,并在 mask 数据时得到 16b。最后不需要最终排列。
使用整数指令合并掩码:(m2<<16) | m1
。如果需要,通过组合两个 32b 掩码,甚至可以达到 64b 掩码数据。
性能:这避免了使用 AVX 单独加载指令的需要,因为 vpand
可以 micro-fuse a memory operand if used with a one-register addressing mode。
- 周期 1:3
vpand
条指令。 (或者只有 2 个,如果我们在等待地址,因为只有 2 个加载端口。)
- 周期2:最后一两个
vpand
,一个pack
(L1,L2)
- 周期 3:下一个
pack
(L3、L4)
- 第 4 周期:决赛
pack
- // 256b AVX2:置换
- 周期 5:带 imm8 计数的打包移位:1 uop,1c 延迟。
- 周期 6:移动掩码(3 个周期延迟)
延迟 = 8(SnB 及更高版本)
吞吐量:3 次随机播放 (p5)、4 次逻辑运算 (p015)、1 次移位 (p0)、1 次 pmovmsk (p0)。 4 个负载。
- SnB/IvB:9 个 ALU 微指令 -> 3c。 4 次内存读取:2c.
因此,根据您对掩码的处理方式,需要 3 个累加器来保持执行端口饱和。 (ceil(8/3) = 3.).
变量中的移位计数无法通过编译器内联/展开解析为编译时常量:延迟 = 9。移位为 p1/p5.[=25= 产生另一个 uop ]
对于 Haswell 及更高版本的 AVX2,vpermd
.
还有 3 个额外的延迟
我正在尝试优化一种算法,该算法将处理大量数据集,这些数据集可能会从 AVX SIMD 指令中受益匪浅。不幸的是,输入内存布局对于所需的计算来说并不是最优的。必须重新排序信息,方法是从恰好相隔 4 个字节的各个字节组装 __m256i
个值:
开始编辑
我的目标 CPU 不支持 AVX2 指令,所以就像@Elalfer 和@PeterCordes 指出的那样,我不能使用 __m256i 值,代码必须转换为使用 __m128i 值相反)
编辑结束
内存中的数据集布局
Byte 0 | Byte 1 | Byte 2 | Byte 3
Byte 4 | Byte 5 | Byte 6 | Byte 7
...
Byte 120 | Byte 121 | Byte 122 | Byte 123
Byte 124 | Byte 125 | Byte 126 | Byte 127
__m256i
变量中的所需值:
| Byte 0 | Byte 4 | Byte 8 | ... | Byte 120 | Byte 124 |
除了这个简单的代码之外,还有更有效的方法来收集和重新排列跨步数据吗?
union { __m256i reg; uint8_t bytes[32]; } aux;
...
for( int i = 0; i < 32; i++ )
aux.bytes[i] = data[i * 4];
编辑:
我要优化的步骤是位列转置;换句话说,某一列的位(在我的数据排列中有 32 个可能的位列)应该成为单个 uint32_t
值,而其余位将被忽略。
我通过重新排列数据来执行转置,执行左移以将所需的位列作为每个子字节中的最高有效位,最后提取并 assemble 这些位到通过 _mm256_movemask_epi8()
内在的单个 uint32
_t 值。
您可以尝试展开该循环,这至少应该去掉循环体中的一次比较 (i<32)、一次递增 (i++) 和一次乘法 (i*4)。常量数组偏移量也可能比变量稍微快一些。但请注意,您的编译器无论如何都可能会生成类似(或更好)的代码,并启用适当的编译选项。
union { __m256i reg; uint8_t bytes[32]; } aux;
...
aux.bytes[0] = data[0];
aux.bytes[1] = data[3];
...
aux.bytes[31] = data[124];
其中一种方法是 - 用 _mm256_shuffle_epi8
打包字节,混合所有 _mm256_blend_epi32
结果向量(你需要做 4 次这样的加载+随机播放),然后做一个 32 位排列 _mm256_permutevar8x32_epi32
.
这是一个伪代码(希望你能想出shuffle masks):
L1 = load32byte(buf)
L2 = load32byte(buf+32)
L3 = load32byte(buf+64)
L4 = load32byte(buf+96)
// Pack 4 bytes in the corresponding 32bit DWORD in each lane and zero-out other bytes
L1 = shuffle(L1, mask_for_L1)
L2 = shuffle(L2, mask_for_L2)
L3 = shuffle(L3, mask_for_L3)
L4 = shuffle(L4, mask_for_L4)
// Vec = blend(blend(L1,L2),blend(L3,L4))
Vec = or(or(or(L1,L2),L3),L4)
Vec = permute(Vec) // fix DWORD order in the vector
更新:忘了我说的原因 "zero-out other bytes" - 这样你就可以用 or
blend
更新:根据彼得在下面的评论重新安排 or
操作,减少了一个周期的延迟。
PS。我还建议您在进行位操作时查看 BMI 指令集。
我刚刚注意到编辑,其中有一个特殊情况的答案。
如果你需要对同一个数据做很多不同的位位置,那么你现在的方案很好。
如果您只需要 128B 内存中的一位位置(尤其是最高位位置),则可以使用 _mm256_movemask_ps
从每个 32b 元素中获取高位。然后在 GP 寄存器中组合四个 8bit 掩码。
一个好的编译器应该将其优化为:
vmovdqu ymm0, [buf + 0]
; to select a different bit:
; vpslld ymm0, ymm0, count ; count can be imm8 or the low byte of an xmm register
vmovmskps eax, ymm0
vmovdqu ymm0, [buf + 32]
vmovmskps ebx, ymm0
... ecx and edx
mov ah, bl
mov ch, dl
shl ecx, 16
or eax, ecx
只有在测试高位时这才好(因此您不需要在 vmovmsk
之前移动每个向量)。即便如此,这可能比其他解决方案更多的指令(和代码大小)。
原题答案:
与 Elalfer 的想法类似,但对 pack
指令使用洗牌单元而不是 pshufb
。此外,所有的 AND 都是独立的,因此它们可以并行执行。 Intel CPU 可以一次执行 3 个 AND,但只能执行一次 shuffle。 (或者在 pre-Haswell 上同时洗牌两次。)
// without AVX2: you won't really be able to
// do anything with a __m256i, only __m128i
// just convert everything to regular _mm_..., and leave out the final permute
mask = _mm256_set1_epi32(0x000000ff);
// same mask for all, and the load can fold into the AND
// You can write the load separately if you like, it'll still fold
L1 = and(mask, (buf)) // load and zero the bytes we don't want
L2 = and(mask, (buf+32))
L3 = and(mask, (buf+64))
L4 = and(mask, (buf+96))
// squish dwords from 2 concatenated regs down to words in 1 reg
pack12 = _mm256_packus_epi32(L1, L2);
pack34 = _mm256_packus_epi32(L3, L4);
packed = _mm256_packus_epi16(pack12, pack34); // note the different width: zero-padded-16 -> 8
Vec = permute(packed) // fix DWORD order in the vector (only needed for 256b version)
Vec = shift(Vec, bit_wanted)
bitvec = movemask(Vec)
// shift:
// I guess word or dword granularity is fine, since byte granularity isn't available.
// You only care about the high bit, so it doesn't matter than you're not shifting zeroes into the bottom of each byte.
// _mm_slli_epi32(Vec, imm8): 1 uop, 1c latency if your count is a compile-time constant.
// _mm_sll_epi32 (Vec, _mm_cvtsi32_si128(count)): 2uop 2c latency if it's variable.
// *not* _mm_sllv_epi32(): slower: different shift count for each element.
如果您只使用 AVX(如您所说)执行此操作,那么您将没有可用的 256b 整数指令。只需构建 128b 个向量,并在 mask 数据时得到 16b。最后不需要最终排列。
使用整数指令合并掩码:(m2<<16) | m1
。如果需要,通过组合两个 32b 掩码,甚至可以达到 64b 掩码数据。
性能:这避免了使用 AVX 单独加载指令的需要,因为 vpand
可以 micro-fuse a memory operand if used with a one-register addressing mode。
- 周期 1:3
vpand
条指令。 (或者只有 2 个,如果我们在等待地址,因为只有 2 个加载端口。) - 周期2:最后一两个
vpand
,一个pack
(L1,L2) - 周期 3:下一个
pack
(L3、L4) - 第 4 周期:决赛
pack
- // 256b AVX2:置换
- 周期 5:带 imm8 计数的打包移位:1 uop,1c 延迟。
- 周期 6:移动掩码(3 个周期延迟)
延迟 = 8(SnB 及更高版本)
吞吐量:3 次随机播放 (p5)、4 次逻辑运算 (p015)、1 次移位 (p0)、1 次 pmovmsk (p0)。 4 个负载。
- SnB/IvB:9 个 ALU 微指令 -> 3c。 4 次内存读取:2c.
因此,根据您对掩码的处理方式,需要 3 个累加器来保持执行端口饱和。 (ceil(8/3) = 3.).
变量中的移位计数无法通过编译器内联/展开解析为编译时常量:延迟 = 9。移位为 p1/p5.[=25= 产生另一个 uop ]
对于 Haswell 及更高版本的 AVX2,vpermd
.