【发布时间】:2022-01-28 19:13:30
【问题描述】:
给定 2 个位掩码,应交替访问 (0,1,0,1...)。我试图获得一个运行时高效的解决方案,但没有找到比以下示例更好的方法。
uint32_t mask[2] { ... };
uint8_t mask_index = 0;
uint32_t f = _tzcnt_u32(mask[mask_index]);
while (f < 32) {
// element adding to result vector removed, since not relevant for question itself
mask[0] >>= f + 1;
mask[1] >>= f + 1;
mask_index ^= 1;
f = _tzcnt_u32(mask[mask_index]);
}
ASM 输出(MSVC,x64)似乎被炸毁了。
inc r9
add r9,rcx
mov eax,esi
mov qword ptr [rdi+rax*8],r9
inc esi
lea rax,[rcx+1]
shrx r11d,r11d,eax
mov dword ptr [rbp],r11d
shrx r8d,r8d,eax
mov dword ptr [rbp+4],r8d
xor r10b,1
movsx rax,r10b
tzcnt ecx,dword ptr [rbp+rax*4]
mov ecx,ecx
cmp rcx,20h
jb main+240h (07FF632862FD0h)
cmp r9,20h
jb main+230h (07FF632862FC0h)
有人给点建议吗?
(这是Solve loop data dependency with SIMD - finding transitions between -1 and +1 in an int8_t array of sgn values 使用 SIMD 创建位掩码的后续)
更新
我想知道一个潜在的解决方案是否可以通过将两个比特流的块加载到寄存器(在我的例子中为 AVX2)中来使用 SIMD,如下所示:
|m0[0]|m1[0]|m0[1]|m1[1]|m0[2]|m1[2]|m0[n+1]|m1[n+1]|
或
每个流有 1 个注册块
|m0[0]|m0[1]|m0[2]|m0[n+1]|
|m1[0]|m1[1]|m1[2]|m1[n+1]|
或将流分成相同大小的块,并一次处理尽可能多的 lanes 放入寄存器。假设我们有 256*10 个元素,可能会在 10 次迭代中结束,如下所示: |m0[0]|m0[256]|m0[512]|...| |m1[0]|m1[256]|m1[512]|...| 并单独处理join
不确定这是否是实现每个周期更多迭代并限制水平位扫描、移位/清除操作和避免分支的方法。
【问题讨论】:
-
尽可能删除 while 循环,或尝试展开循环。也许使用 int64 而不是 2x int32 并进行位移以获得正确的 int。基准。 tzcnt 有多快?如果您可以在不跳转的情况下按顺序处理值,您可能会获得最佳性能。
-
您是否尝试过使用
std::swap(mask[0], mask[1])编写它,而不是欺骗编译器将掩码实际保存在内存中并对其使用变量索引?或者像杰罗姆说的,展开。 (您还每次都在欺骗 MSVC 将uint8_t mask_index零扩展为指针宽度。由于某种原因,它选择了符号扩展,其中 mov-elimination 甚至在 Intel CPU 上也不起作用......无论如何,不要使用完全没有索引,但如果你这样做了,至少要设为unsigned,而不是 8 位,因为它会存在于寄存器中。) -
我尽量避免在热点中使用 std 进行计算,以便完全控制(和学习)。正如@PeterCordes 指出的那样,这是处理孤立问题的另一个问题的后续行动。对我来说,Jérôme Richard 得出的答案完美地解释和解决了这个问题。我的结论是通过使用 1 个掩码而不是 2 个掩码来避免在早期阶段进行隐含分支。
-
std::swap是微不足道的,但可以肯定的是,我也可以在这里手动使用一个临时的。不管你如何交换,只要你这样做,编译器就会为你解决这个问题,最坏的情况是xchg,最好是展开。我的主要观点是,有更多对编译器友好的方式来表达事物。 -
嗯,如果你确实有一个面具而不是两个?然后你在
tzcnt和班次之间使用not?或blsi/add清除连续运行的运行?
标签: c++ performance optimization bit-manipulation x86-64