【问题标题】:Split a number into several numbers, each with only one significant bit将一个数字拆分为多个数字,每个数字只有一个有效位
【发布时间】:2020-02-09 21:03:21
【问题描述】:

是否有任何有效的算法(或处理器指令)可以帮助将数字(32 位和 64 位)分成多个数字,其中只有一个 1 位。

我想隔离数字中的每个设置位。例如,

输入:
01100100

输出:

01000000 
00100000
00000100

只想到number & mask。 汇编或С++。

【问题讨论】:

  • 你想要位值还是掩码?
  • 我需要按数字获取所有位掩码
  • 你希望输出如何,在一个数组中,还是什么?效率不高但大致有效的 MSVE 将有助于了解您想要的输出方式,甚至是没有实现的函数签名。

标签: c++ c++11 assembly binary bit-manipulation


【解决方案1】:

是的,与Brian Kernighan's algorithm to count set bits 类似,除了不计算我们提取的位并在每个中间结果中使用最低设置位:

while (number) {
    // extract lowest set bit in number
    uint64_t m = number & -number;
    /// use m
    ...
    // remove lowest set bit from number
    number &= number - 1;
}

在现代 x64 汇编中,number & -number 可以编译为blsinumber &= number - 1 可以编译为blsr,两者都很快,因此只需要几个有效的指令即可实现。

由于m 可用,可以使用number ^= m 重置最低设置位,但这可能会使编译器更难看到它可以使用blsr,这是一个更好的选择,因为它仅取决于直接在number上,所以它缩短了循环携带的依赖链。

【讨论】:

  • 只是为了好玩,我编写了一个 AVX512 版本,它使用两个 vpcompressd 存储在内存中生成一组掩码。可能没用;我希望正常的用例是遍历掩码并对每个掩码做一些事情。
【解决方案2】:

标准方式是

while (num) {
    unsigned mask = num ^ (num & (num-1)); // This will have just one bit set
    ...
    num ^= mask;
}

例如以num = 2019 开头,您将按顺序排列

1
2
32
64
128
256
512
1024

【讨论】:

  • 为什么不num & -num
  • @harold:带无符号的一元减号对我来说感觉很奇怪(例如在 MISRA 中是禁止的)
  • 哦,好吧,这很奇怪,无符号的一元减号是安全的,但有符号整数是不安全的
  • @harold:我知道......我仍然认为在进行位摆弄时应该使用 unsigned 并且一元减号在这种情况下是一个奇怪的操作。请注意x & -xx ^ (x & (x - 1)) 生成(使用g++)完全相同的机器代码。
  • num & -num 是我听说的隔离最低设置位的标准方法。然后您可以单独清除最低位。如果编译方式与编写方式相似,则您的方式具有更少的指令级并行性:循环携带的依赖链长 4 个操作:-1&^,然后是另一个 ^。正常的方式只有2个,每一步隔离最低设置位是一个独立的链。
【解决方案3】:

如果您一次迭代一个位隔离掩码,则一次生成一个掩码是有效的;请参阅@harold 的回答。


但如果你真的只想要所有的掩码,x86 和 AVX512F 可以有效地并行化这个。(至少可能有用,具体取决于周围的代码。更有可能这只是应用 AVX512 和对大多数用例没有用处)。

关键的构建块是AVX512F vpcompressd:给定一个掩码(例如来自 SIMD 比较),它会将选定的 dword 元素打乱为向量底部的连续元素。

一个 AVX512 ZMM / __m512i 向量保存 16 个 32 位整数,因此我们只需要 2 个向量来保存每个可能的单位掩码。 我们的输入数字一个掩码,它选择哪些元素应该成为输出的一部分。(无需将其广播到向量和vptestmd 或类似的东西中; 我们可以把kmov 放到一个掩码寄存器中直接使用。)

另见我在AVX2 what is the most efficient way to pack left based on a mask? 上的 AVX512 答案

#include <stdint.h>
#include <immintrin.h>

// suggest 64-byte alignment for out_array
// returns count of set bits = length stored
unsigned bit_isolate_avx512(uint32_t out_array[32], uint32_t x)
{
    const __m512i bitmasks_lo = _mm512_set_epi32(
           1UL << 15,  1UL << 14,  1UL << 13,  1UL << 12,
           1UL << 11,  1UL << 10,  1UL << 9,   1UL << 8,
           1UL << 7,   1UL << 6,   1UL << 5,   1UL << 4,
           1UL << 3,   1UL << 2,   1UL << 1,   1UL << 0
     );
     const __m512i bitmasks_hi = _mm512_slli_epi32(bitmasks_lo, 16);    // compilers actually do constprop and load another 64-byte constant, but this is more readable in the source.

    __mmask16 set_lo = x;
    __mmask16 set_hi = x>>16;

    int count_lo = _mm_popcnt_u32(set_lo);  // doesn't actually cost a kmov, __mask16 is really just uint16_t
    _mm512_mask_compressstoreu_epi32(out_array, set_lo, bitmasks_lo);
    _mm512_mask_compressstoreu_epi32(out_array+count_lo, set_hi, bitmasks_hi);

    return _mm_popcnt_u32(x);
}

使用 clang on Godbolt 和 gcc 可以很好地编译,而不是使用 mov、movzx 和 popcnt 的几个次优选择,并且无缘无故地制作一个帧指针。 (它也可以-march=knl编译;它不依赖于AVX512BW或DQ。)

# clang9.0 -O3 -march=skylake-avx512
bit_isolate_avx512(unsigned int*, unsigned int):
        movzx   ecx, si
        popcnt  eax, esi
        shr     esi, 16
        popcnt  edx, ecx
        kmovd   k1, ecx
        vmovdqa64       zmm0, zmmword ptr [rip + .LCPI0_0] # zmm0 = [1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768]
        vpcompressd     zmmword ptr [rdi] {k1}, zmm0
        kmovd   k1, esi
        vmovdqa64       zmm0, zmmword ptr [rip + .LCPI0_1] # zmm0 = [65536,131072,262144,524288,1048576,2097152,4194304,8388608,16777216,33554432,67108864,134217728,268435456,536870912,1073741824,2147483648]
        vpcompressd     zmmword ptr [rdi + 4*rdx] {k1}, zmm0
        vzeroupper
        ret

在 Skylake-AVX512 上,vpcompressd zmm{k1}, zmm 是端口 5 的 2 微秒。输入向量 -> 输出的延迟为 3 个周期,但输入掩码 -> 输出的延迟为 6 个周期。 (https://www.uops.info/table.html/https://www.uops.info/html-instr/VPCOMPRESSD_ZMM_K_ZMM.html)。内存目标版本is 4 uops: 2p5 + 通常的存储地址和存储数据微指令,它们在较大指令的一部分时不能微熔。

最好压缩成 ZMM reg 然后存储,至少在第一次压缩时,以节省总 uops。第二个可能仍应利用 vpcompressd [mem]{k1} 的屏蔽存储功能,因此输出数组不需要填充即可踩到。 IDK 如果这有助于缓存行拆分,即掩码是否可以避免在第二个缓存行中为具有全零掩码的部分重放存储 uop。

在 KNL 上,vpcompressd zmm{k1} 只是一个微指令。 Agner Fog 没有使用内存目标 (https://agner.org/optimize/) 对其进行测试。


这是 Skylake-X 前端的 14 个融合域微指令,用于实际工作(例如,在内联到多个 x 值的循环后,因此我们可以将 vmovdqa64 负载提升出循环. 否则那是另外 2 微秒)。所以前端瓶颈 = 14 / 4 = 3.5 个周期。

后端端口压力:端口 5 6 uop(2x kmov(1) + 2x vpcompressd(2)):每 6 个周期 1 次迭代。 (即使在 IceLake (instlatx64) 上,vpcompressd 仍然是 2c 吞吐量,不幸的是,显然 ICL 的额外 shuffle 端口不能处理这些微指令。kmovw k, r32 仍然是 1/clock,所以大概仍然是端口 5也是。)

(其他端口都很好:popcnt 在端口 1 上运行,当 512 位微指令在运行时,该端口的向量 ALU 被关闭。但它的标量 ALU 不是,它是唯一一个处理 3 周期延迟整数指令的。 movzx dword, word 无法消除,只有 movzx dword, byte 可以做到,但它可以在任何端口上运行。)

延迟:整数结果只是一个popcnt(3 个周期)。记忆结果的第一部分在掩码准备好后大约 7 个周期存储。 (kmov -> vpcompressd)。 vpcompressd 的向量源是一个常量,因此 OoO exec 可以尽早准备好它,除非它在缓存中丢失。


通过移位构建 1&lt;&lt;0..15 常量是可能的,但可能不值得。例如使用vpmovzxbd 加载16 字节_mm_setr_epi8(0..15),然后在set1(1) 的向量上使用vpsllvd(您可以从广播中获取或使用vpternlogd+shift 即时生成)。但这可能不值得,即使你用 asm 手工编写(所以它是你的选择,而不是编译器),因为这已经使用了很多 shuffle,并且常量生成至少需要 3 或 4 条指令(每个至少有 6 个字节长;仅 EVEX 前缀每个就是 4 个字节)。

不过,我会生成 hi 部分,并从 lo 转移,而不是单独加载它。除非周围的代码在端口 0 上遇到严重瓶颈,否则 ALU uop 并不比加载 uop 差。一个 64 字节的常量填满了整个缓存行。

您可以使用vpmovzxwd 负载压缩 lo 常量:每个元素适合 16 位。值得考虑是否可以将其提升到循环之外,这样每次操作就不会花费额外的随机播放。


如果您希望将结果存储在 SIMD 向量中而不是存储到内存中,您可以将 2x vpcompressd 放入寄存器中,并且可以使用 count_lo 来查找 vpermt2d 的随机播放控制向量。可能来自数组上的滑动窗口而不是 16x 64 字节向量?但结果不能保证适合一个向量,除非您知道您的输入设置了 16 位或更少的位。


64 位整数的情况更糟 8x 64 位元素意味着我们需要 8 个向量。因此,与标量相比,它可能不值得,除非您的输入设置了很多位。

不过,您可以在循环中执行此操作,使用 vpslld by 8 来移动向量元素中的位。你会认为kshiftrq 会很好,但是有 4 个周期的延迟,这是一个很长的循环承载的 dep 链。无论如何,您都需要每个 8 位块的标量 popcnt 来调整指针。所以你的循环应该使用shr/kmovmovzx/popcnt。 (使用计数器 += 8 和 bzhi 来喂 popcnt 会花费更多的微指令)。

循环携带的依赖项都很短(并且循环只运行 8 次迭代以覆盖 64 位掩码),因此乱序 exec 应该能够很好地重叠工作以进行多次迭代。特别是如果我们展开 2,那么向量和掩码依赖项可以在指针更新之前。

  • 向量:vpslld 立即数,从向量常数开始
  • 掩码:shr r64, 8x 开头。 (在移出所有位后,当它变为 0 时可能会停止循环。这个 1 周期的 dep 链足够短,OoO exec 可以在它发生时快速穿过它并隐藏大部分的错误预测惩罚。)
  • 指针:lea rdi, [rdi + rax*4],其中 RAX 保存 popcnt 结果。

其余的工作在迭代中都是独立的。根据周围的代码,我们可能会在端口 5 上出现瓶颈,vpcompressd shuffles 和 kmov

【讨论】:

    猜你喜欢
    • 2021-08-26
    • 1970-01-01
    • 2011-05-07
    • 1970-01-01
    • 1970-01-01
    • 2016-11-07
    • 1970-01-01
    • 2019-10-05
    • 2017-10-07
    相关资源
    最近更新 更多