我正在尝试优化一种算法,该算法将处理大量数据集,这些数据集可能会受益于AVX SIMD指令。不幸的是,输入存储器布局对于所需的计算并不是最佳的。必须重新排序信息,方法是将恰好4个字节的单个字节中的__m256i
值组合起来:
开始编辑
我的目标CPUS不支持AVX2指令,所以像@Elalfer和@PeterCordes指出的那样,我无法使用__m256i值,代码必须转换为使用__m128i值代替)
结束编辑
内存中的DataSet布局
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
值,而其余位则被忽略。
我通过重新排列数据来执行转置,执行左移以将所需的位列作为每个子字节中的最高有效位,最后提取并将这些位组合成单个uint32
通过_mm256_movemask_epi8()
内在函数_t值。
答案 0 :(得分:4)
其中一种方法是 - 用_mm256_shuffle_epi8
打包字节,混合所有_mm256_blend_epi32
结果向量(你需要做4个这样的加载+随机播放),并做一个32位permute _mm256_permutevar8x32_epi32
。
这是一个伪代码(我希望你能想出一下shuffle mask):
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
更新:忘记我说的原因&#34;将其他字节清零&#34; - 这样您就可以将blend
替换为or
更新:通过在下面的彼得评论中重新排列or
次操作,减少了一个周期延迟。
<强> PS 即可。我还建议您在进行位操作时查看BMI指令集。
答案 1 :(得分:2)
你可以尝试展开那个循环,这应该至少摆脱循环体中的一个比较(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];
答案 2 :(得分:2)
我只是注意到编辑,它有一个特殊情况的答案。
如果您需要在同一数据上执行许多不同的位位置,那么您当前的计划是好的。
如果您只需要128B内存中的一位位置(尤其是最高位位置),则可以使用_mm256_movemask_ps
从每个32b元素中获取高位。然后在GP寄存器中组合四个8位掩码。
一个好的编译器应该优化它:
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的想法类似,但使用shuffle单位代替pack
pshufb
指令。此外,所有AND都是独立的,因此它们可以并行执行。 Intel CPU可以同时执行3个AND,但只能进行一次shuffle。 (或者在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向量,并在掩码数据时获得16b。最后你不需要最后的选择。
使用整数指令合并蒙版:(m2<<16) | m1
。如果需要,通过组合两个32b掩模,甚至可以达到64b的掩模数据。
性能:这可以避免使用AVX单独加载指令,因为vpand
可以micro-fuse a memory operand if used with a one-register addressing mode。
vpand
指令。 (或者只有2,如果我们在地址上等待,因为只有2个加载端口。)vpand
,一个pack
(L1,L2)pack
(L3,L4)pack
延迟= 8(SnB及更高版本)
吞吐量:3次shuffle(p5),4次逻辑(p015),1次移位(p0),1 pmovmsk(p0)。 4加载uops。
变量中的移位计数通过编译器内联/展开无法解析为编译时常量:latency = 9.并且移位会为p1 / p5生成另一个uop。
对于Haswell及更高版本的AVX2,vpermd
还有3个额外延迟。