mar*_*ona 3 c++ bit-manipulation avx avx512
我有__mask64一些 AVX512 操作的结果:
__mmask64 mboth = _kand_mask64(lres, hres);
我想计算其中所有位均已设置的半字节数 ( 0xF)。
简单的解决方案是这样做:
uint64 imask = (uint64)mboth;
while (imask) {
if (imask & 0xf == 0xf)
ret++;
imask = imask >> 4;
}
Run Code Online (Sandbox Code Playgroud)
我想要更好的东西,但我想出的东西并不优雅:
//outside the loop
__m512i b512_1s = _mm512_set1_epi32(0xffffffff);
__m512i b512_0s = _mm512_set1_epi32(0x00000000);
//then...
__m512i vboth = _mm512_mask_set1_epi8(b512_0s, mboth, 0xff);
__mmask16 bits = _mm512_cmpeq_epi32_mask(b512_1s, vboth);
ret += __builtin_popcount((unsigned int)fres);
Run Code Online (Sandbox Code Playgroud)
上面将一个0xff字节放入一个向量中,其中掩码中存在 1 位,然后当现在发现bits放大的半字节为' 时,在掩码中获取一个 1 位。0xf0xffffffff int32
我觉得当原始数据存在于 64 位数字中时,两个 512 位操作太过分了。这个替代方案可能要糟糕得多。它的指令太多,而且仍然在 128 位上运行:
//outside the loop
__m128i b128_1s = _mm_set1_epi32(0xffffffff);
//then...
uint64 maskl = mboth & 0x0f0f0f0f0f0f0f0f;
uint64 maskh = mboth & 0xf0f0f0f0f0f0f0f0;
uint64 mask128[2] = { (maskl << 4) | maskl, (maskh >> 4) | maskh };
__m128i bytes = _mm_cmpeq_epi8(b128_1s, *(__m128i*)mask128);
uint bits = _mm_movemask_epi8(bytes);
ret += __builtin_popcount(bits);
Run Code Online (Sandbox Code Playgroud)
只需一些标量操作,您就可以做到这一点:
imask &= imask << 2;
imask &= imask << 1;
ret += std::popcount(imask & 0x8888888888888888);
Run Code Online (Sandbox Code Playgroud)
对于每个半字节,前两个步骤将该半字节的位的水平与放在该半字节的最高有效位中。半字节的其他部分变成了我们不想要的东西,所以我们只是将它们屏蔽掉。然后对结果进行popcount。
轮班可以向右(如本答案的早期版本所示),也可以轮换,以效果最好的为准。
Clang 从这个版本中实现了高效的 asm,除了之前的异或清零之外,popcnt没有任何浪费的指令,内联应该会消失,因为它popcnt same,same甚至可以在不提前计划的情况下将结果存入 EAX 中以用于调用约定。
GCC 做得不错,但对最后一个重新排序&= mask,因此它是关键路径延迟的一部分,而不是与移位并行,尽管我们尽了最大努力使源代码看起来像单个汇编操作,以尝试将其掌握为更好的汇编。
MSVC 对此很奇怪,它把它变成了右移,并且&= mask像 GCC 一样做最后一个。
// Clang compiles this optimally, hopefully also when inlining
// GCC still does the & mask last, on the critical path
// MSVC mangles this, with two right shifts despite the source going left, and deoptimizes latency like GCC
int count_nibbles(uint64_t imask)
{
uint64_t mask = 0x2222222222222222; // movabs, or hoisted out of a loop
uint64_t shifted = imask << 1; // LEA dst, [src+src] into a new reg
shifted &= imask; // AND
shifted >>= 2; // SHR
imask &= mask; // AND into original reg, in parallel with the shift/AND chain
shifted &= imask; // AND
return std::popcount(shifted); // POPCNT
}
Run Code Online (Sandbox Code Playgroud)
此版本还可以防止 clang 对移位或旋转进行去优化,其中lea reg, [0 + reg*4]8 字节长且在 Alder Lake / Sapphire Rapids 上有 2 个周期的延迟。(https://uops.info/)。
Godbolt用于此版本和其他几个版本(包括 chtz 的 ADD/ADC 技巧的便携式版本)。在函数中的某个点使用asm("" : "+r"(imask))可以强制 GCC 不要取消优化操作顺序,但这可能会阻止它作为更大循环的一部分对其进行优化。
在同一源代码行上编写多个操作不会对 Clang 造成任何伤害,而且这样做仍然不能阻止 GCC 把它搞砸,但这确实说明了最佳的 asm 应该是什么样子。您可能更愿意将其压缩为更少的 C 语句。
如果可能的话,GCC 将移位和与组合在一起的重新排序通常对于 AArch64 很有用and x1, x0, x0, lsr 2。但即便如此,指令级并行仍然是可能的,同时仍然只使用 3 个 AND 指令,其中两个指令具有移位的操作数。 GCC/Clang/MSVC 错过了该优化。按位指令的 AArch64 重复模式立即数确实允许0x2222222222222222或0x8888888888888888,因此不需要单独的常量设置。