关于c ++:在某个位置或更低位置计算设置位的有效方法是什么?

What is the efficient way to count set bits at a position or lower?

任何给定的std::bitset<64> bits数集和一位位的位置X(0~63)

什么是最有效的方法来计数的比特位置或在X下或返回0,如果位在X是一个集合

注:如果我是一位集返回的至少1

在蛮力的方式是非常慢。

1
2
3
4
5
6
7
8
9
10
int countupto(std::bitset<64> bits, int X)
{
  if (!bits[X]) return 0;
  int total=1;
  for (int i=0; i < X; ++i)
  {
    total+=bits[i];
  }
  return total;
}

方法和count()学院bitset想给你一切popcount比特流,但不支持bitset

注:这不是一个DUP(如何计数的比特数集A的32位整数?这是一位不为所有潜在的范围0到X


这个C++得到G++来发射非常好的x86 ASM(GoogBort编译器资源管理器)。我希望它也能在其他64位体系结构上有效地编译(如果std::bitset::count有一个hw popcount可供使用,否则这将是最慢的部分):好的。

1
2
3
4
5
6
7
8
9
10
#include <bitset>

int popcount_subset(std::bitset<64> A, int pos) {
  int high_bits_to_eliminate = 63 - pos;
  A <<= (high_bits_to_eliminate & 63);  // puts A[pos] at A[63].

  return (A[63]? ~0ULL : 0) & A.count();  // most efficient way: great code with gcc and clang
  // see the godbolt link for some #ifdefs with other ways to do the check, like
    // return A[BSET_SIZE-1] ? A.count() : 0;
}

这在32位体系结构上可能不是最佳的,因此如果需要进行32位构建,请比较其他替代方案。好的。

只要你对硬编码的63S做些什么,并把移位计数的& 63屏蔽改为更一般的范围检查,这对其它大小的位集就有效。为了在具有奇怪大小的位集的情况下获得最佳性能,请为目标机器的size <= register width生成一个具有专门化的模板函数。在这种情况下,将位集提取到适当宽度的unsigned类型,并移动到寄存器的顶部而不是位集的顶部。好的。

您可能希望它也能为bitset<32>生成理想的代码,但它并不是很理想。gcc/clang仍然在x86-64上使用64位寄存器。好的。

对于大的位集,移动整个内容的速度将比只弹出包含pos的下面的单词并在该单词上使用这个速度慢。(如果您可以假定SSSE3而不是popcntinsn硬件支持,或者32位目标,那么矢量化的popcount确实会在x86上大放异彩。avx2 256bit pshufb是进行批量popccount的最快方法,但如果没有avx2,我认为64位popcnt非常接近128位pshufb的实现。更多讨论请参见评论。)好的。

如果您有一个64位元素的数组,并且想要分别计算每个元素中某个位置下的位,那么您肯定应该使用simd。该算法的移位部分向量化,而不仅仅是popcnt部分。在基于pshufb的popcnt之后,对全零寄存器使用psadbw,将64位块中的字节水平和,该popcnt分别为每个字节中的位产生计数。SSE/AVX没有64位算术右移,但是您可以使用不同的技术来混合每个元素的高位。好的。我是怎么想到这个的:

要让编译器输出的ASM指令将:好的。

  • 从64位值中删除不需要的位
  • 测试所需位的最高值。
  • 加油吧。
  • 根据测试结果返回0或PopCount。(无分支或分支实现都有优势。如果分支是可预测的,则无分支实现的速度往往较慢。)
  • 最明显的方法是生成一个蒙版((1<<(pos+1)) -1&它。一种更有效的方法是使用63-pos左移位,将要打包的位留在寄存器的顶部。好的。

    这也有一个有趣的副作用,即将要测试的位作为寄存器中的顶位。测试符号位,而不是任何其他任意位,只需要很少的指令。算术右移可以将符号位广播到寄存器的其余部分,从而比通常的无分支代码更有效。好的。

    做popcount是一个备受争议的问题,但实际上是难题中更棘手的部分。在x86上,它有非常有效的硬件支持,但仅在最近足够的硬件上。在Intel CPU上,popcnt指令仅在Nehalem和更新版本上可用。我忘记了AMD何时增加了支持。好的。

    因此,为了安全地使用它,您需要使用不使用popcnt的回退来进行CPU调度。或者,生成独立的二进制文件,这些二进制文件依赖于/不依赖于某些CPU功能。好的。

    不使用popcnt指令的popcount可以通过几种方式完成。其中一个使用ssse3 pshufb来实现4位LUT。但是,当在整个阵列上使用时,这是最有效的,而不是一次使用单个64B。标量比特黑客在这里可能是最好的,并且不需要SSSE3(因此可以与具有64位但不具有pshufb的古老AMD CPU兼容)。好的。比特广播:

    (A[63]? ~0ULL : 0)要求编译器将高位广播到所有其他位的位置,允许它用作popcount结果的和屏蔽,使其归零(或不归零)。注意,即使对于大的位集大小,它仍然只是掩盖EDOCX1的输出(0),而不是位集本身,所以~0ULL是好的,我使用ull来确保不会要求编译器只将位广播到寄存器的低32b(例如,在Windows上使用UL)。好的。

    这个广播可以通过算术右移63来完成,它在高位的副本中移动。好的。

    Clang从原始版本生成了此代码。在Glenn介绍了4的不同实现之后,我意识到我可以通过编写更像ASM的源代码来引导GCC走向Clang的最佳解决方案。显然,直接请求算术右移的((int64_t)something) >> 63并不具有严格的可移植性,因为有符号右移被定义为算术或逻辑。本标准不提供任何便携式算术右移运算符。(不过,这并不是未定义的行为。)无论如何,幸运的是,编译器足够聪明:只要您给出足够的提示,GCC就会看到最好的方法。好的。

    这个源代码在x86-64和带有gcc和clang的arm64上生成了很好的代码。两者都只需在popcnt的输入上使用算术右移位(这样移位可以与popcnt并行运行)。它在32位x86和gcc上也编译得很好,因为屏蔽只发生在32位变量上(在添加了多个popcnt结果之后)。它是32位(当位集大于寄存器时)上讨厌的函数的其余部分。好的。

    三元算子与原版本的gcc

    用gcc编译5.3.0(GCC -O3 -march=nehalem -mtune=haswell人之间,这样,还仍然散发):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ; the original ternary-operator version.  See below for the optimal version we can coax gcc into emitting.
    popcount_subset(std::bitset<64ul>, int):
        ; input bitset in rdi, input count in esi (SysV ABI)
        mov     ecx, esi    ; x86 variable-count shift requires the count in cl
        xor     edx, edx    ; edx=0
        xor     eax, eax    ; gcc's workaround for popcnt's false dependency on the old value of dest, on Intel
        not     ecx         ; two's complement bithack for 63-pos (in the low bits of the register)
        sal     rdi, cl     ; rdi << ((63-pos) & 63);  same insn as shl (arithmetic == logical left shift)
        popcnt  rdx, rdi
        test    rdi, rdi    ; sets SF if the high bit is set.
        cmovs   rax, rdx    ; conditional-move on the sign flag
        ret

    国有企业如何证明一个C开头的语句,x和x + 1,~,~(x - 1)的收益率相同的结果吗?是使用GCC的背景上的两个补体-x == ~x + 1的身份。(2),它可以使用整数运算和零位不在高,低投入,如果只有一部分是想要的结果吗?这是tangentially mentions shl面具移位计数,所以我们只需要低ecx握住63 - pos6位)。这是我写的,因为它的链接和任何人阅读本段,目前仍可能发现它有趣。

    一些这些指令将离你而去时,内联。(例如,GCC会产生在ECX伯爵在第一广场)。

    (三)相乘而得到的概念(GCC),并启用了USE_mul)

    1
    2
        shr     rdi, 63
        imul    eax, edi

    而在端testcmovsxor/ /。

    性能分析microarch哈斯韦尔,以日期(乘雾从理学版):

    • mov r,r融合域:1 0 UOP,潜伏期,在执行单元
    • xor-归零法:1融合域的UOP公司,在执行单元
    • notP0 P1是:UOP/1//P5 P6,潜伏期1 1c中,每0.25c吞吐量
    • shl(又名sal)与计数是在cluops P0 P6:3 / 1 2C 2C型:潜伏期,人均产量。(这是理学的ivybridge雾只需要2日表示,这是uops,奇怪)。
    • 1 popcnt:UOP是P1潜伏期1每1C,3C,吞吐量
    • shr r,imm:UOP/1 1C是P0 P6,潜伏期。每1次的吞吐量。
    • imul r,r:3C是1uop P1潜伏期。
    • ret不计数

    totals:

    • 9 uops融合域的问题,可以在2周期(在理论的影响;采用缓存地图通常瓶颈点的前端)。
    • (4 uops位移/ P0 P6)。2 uops为P1。1 ALU的UOP公司的任何端口。一个可以运行在每2C(saturating变速操作系统的前端的港口),最糟糕的是一个瓶颈。

    潜伏期:从关键路径当位准备的结果是:当shl→(2)→(3)popcntimul(3)。所有8个周期。当准备从pos或9C条,因为一个额外的not1C是它的潜伏期。

    最佳bitbroadcast版replaces shrsar(相同的性能,与andimul)及(1C 3C而潜伏期,任何端口上运行)。唯一的变化是一个系统的性能的关键路径到6环的潜伏期。bottlenecked吞吐量仍然是在前端。能够运行在任何人的and差分端口是不是做这个,除非你是混合与代码(而不是在线port1瓶颈的吞吐量就看这代码是运行在紧环)。

    (三)病毒融合域(11)版本:(一uops前端每2.75c)。执行单位:bottlenecked仍然在转移端口(P0(P6)在每7c一2C型。潜伏期:从位到结果,从结果(POS)。(2 cmov潜伏期是2 uops P0,P1是有/ / / P5 P6)。

    clang有一些不同的技巧:它不是testcmovs,而是通过使用算术右移将符号位广播到寄存器的所有位置来生成一个包含所有1或所有0的掩码。我喜欢:用and代替cmov,在英特尔上效率更高。尽管如此,它仍然具有数据依赖性,并且为分支的两边(这通常是CMOV的主要缺点)工作。更新:使用正确的源代码,gcc也将使用这个方法。好的。

    clang 3.7 -O3 -Wall -march=nehalem -mtune=haswell。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    popcount_subset(std::bitset<64ul>, int):
        mov     ecx, 63
        sub     ecx, esi      ; larger code size, but faster on CPUs without mov-elimination
        shl     rdi, cl       ; rdi << ((63-pos) & 63)
        popcnt  rax, rdi      ; doesn't start a fresh dep chain before this, like gcc does
        sar     rdi, 63       ; broadcast the sign bit
        and     eax, edi      ; eax = 0 or its previous value
        ret

    sar / and取代了xor / test / cmovcmov是英特尔CPU上的2-uop指令,所以这真的很好。(对于三元运算符版本)。好的。

    Clang在使用多源版本或"比特广播"源版本时仍然使用sar / and技巧,而不是实际的imul。所以这些帮助GCC而不伤害Clang。(sar/and肯定比shr/imul好:关键路径上的延迟小2c。)pow_of_two_sub版本确实伤害了clang(见第一个godbolt链接:从这个答案中省略,以避免混乱的想法没有出现)。好的。

    mov ecx, 63/sub ecx, esi在CPU上实际上更快,而没有消除寄存器、寄存器移动的MOV(零延迟,无执行端口,由寄存器重命名处理)。这包括Intel Pre-Ivybridge,但不是最新的Intel和AMD CPU。好的。

    Clang的mov imm/sub方法只将pos的一个延迟周期放在关键路径上(超过位集->结果延迟),而不是在mov r,r有1c延迟的CPU上,将mov ecx, esi/not ecx的两个延迟周期放在关键路径上。好的。

    使用bmi2(haswell和更高版本),最佳ASM版本可以将mov保存到ecx。其他所有操作都是一样的,因为shlx将其移位计数输入寄存器屏蔽为操作数大小,就像shl一样。好的。

    x86移位指令具有疯狂的cisc语义,如果移位计数为零,则标志不会受到影响。因此变量计数移位指令(可能)依赖于标记的旧值。"正常的"x86 shl r, cl"在haswell上解码为3个Uops,但bmi2 shlx r, r, r仅为1。因此,很糟糕的是,GCC仍然使用-march=haswell而不是使用shlx(在某些其他情况下确实使用)来发射sal。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // hand-tuned BMI2 version using the NOT trick and the bitbroadcast
    popcount_subset(std::bitset<64ul>, int):
        not     esi           ; The low 6 bits hold 63-pos.  gcc's two-s complement trick
        xor     eax, eax      ; break false dependency on Intel.  maybe not needed when inlined.
        shlx    rdi, rdi, rsi ; rdi << ((63-pos) & 63)
        popcnt  rax, rdi
        sar     rdi, 63       ; broadcast the sign bit: rdi=0 or -1
        and     eax, edi      ; eax = 0 or its previous value
        ret

    英特尔哈斯韦尔性能分析:6个融合域UOP(前端:每1.5C一个)。执行单位:2 p0/p6移位Uops。1 P1UOP。2任何端口UOP:(从总执行端口限制来看,每1.25C一个)。关键路径延迟:shlx(1)->popcnt(3)->and(1)=5C位集->结果。(或来自pos的6c->结果)。好的。

    注意,当内联时,人工(或智能编译器)可以避免对xor eax, eax的需要。它的存在只是因为popcnt对输出寄存器(在Intel上)的错误依赖,我们需要eax中的输出(调用方最近可能使用了长DEP链)。对于-mtune=bdver2或其他东西,gcc不会将用于popcnt输出的寄存器归零。好的。

    在内联时,我们可以使用一个输出寄存器,该寄存器必须至少早于popcnt的源寄存器准备好,以避免出现问题。如果以后不需要源代码,编译器将在适当的位置执行popcnt rdi,rdi,但这里不是这样。相反,我们可以选择另一个寄存器,它必须在源之前准备好。popcnt的输入依赖于63-pos的输入,我们可以消除它,所以popcnt rsi,rdi对rsi的依赖不能延迟它。或者,如果我们在登记簿上登记了63,我们可以登记popcnt rsi,rdi/sarx rax, rsi, reg_63/and eax, esi。或者,bmi2 3操作数移位指令也可以让我们在以后需要的情况下不去修改输入。好的。

    这是如此轻,以至于循环开销和设置输入操作数/存储结果将是主要因素。(并且63-pos可以使用编译时常数进行优化,或者在任何变量计数来自的地方进行优化。)好的。

    英特尔编译器有趣地将自己的脚射到了脚下,并没有利用一个[63]是符号位的事实。shl/bt rdi, 63/jc。它甚至以一种非常愚蠢的方式设置树枝。它可以使eax归零,然后根据shl设置的符号标志跳过popcnt或不跳过popcnt。好的。

    -O3 -march=corei7on godbolt的ICC13输出开始的最佳分支实现:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
       // hand-tuned, not compiler output
            mov       ecx, esi    ; ICC uses neg/add/mov :/
            not       ecx
            xor       eax, eax    ; breaks the false dep, or is the return value in the taken-branch case
            shl       rdi, cl
            jns    .bit_not_set
            popcnt    rax, rdi
    .bit_not_set:
            ret

    这是非常理想的:A[pos] == true案例有一个没有分支。不过,它不会在无分支方法上节省太多。好的。

    如果A[pos] == false情况更常见:跳过ret指令,跳转到popcntret指令。(或者在内联之后:跳到执行popcnt的末尾的一个块,然后跳回来)。好的。好啊。


    我的直接反应是测试指定的位,并立即返回0,它是明确的。

    如果您超过了这一点,那么创建一个设置了该位(和不太重要的位)的位掩码,并使用原始输入创建一个and。然后使用count()成员函数获取结果中设置的位数。

    至于创建遮罩:可以左移1个N位,然后减去1。


    下面的位很容易在位和掩码之间转换,因此类似这样的操作应该有效:

    1
    2
    3
    4
    5
    6
    int popcnt(bitset<64> bs, int x) {
        // Early out when bit not set
        if (!bs[x]) return 0;
        // Otherwise, make mask from `x`, mask and count bits
        return (bs & bitset<64>((1UL << x) - 1)).count() + 1;
    }

    这里假设bitset::count是有效实现的(使用popcnt内部函数或有效的回退);这并不能保证,但STL人员倾向于优化这类事情。


    假设一个unsigned longunsigned long long足够大,可以容纳64位,您可以调用bits.to_unlong()bits.to_ullong()将位集数据作为一个整数,屏蔽x((1 << X) - 1以上的位,然后按照您所链接问题的答案计算这些位。


    我已经编辑了一个我以前见过的问题,它将检查一个数字中是否设置了奇数或偶数位。这是C的,但是把它推入C++不应该太难。解决方案的关键是while循环中的内容。在纸上尝试了解它是如何挑选LSB的,然后将其从X中删除的。其余的代码是直接向前的。代码在O(n)中运行,其中n是x中的设定位数。这比线性时间要好得多,我也认为只有在第一次看到这个问题时才有可能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    #include <stdio.h>

    int
    count(long x, int pos)
    {
        /* if bit at location pos is not set, return 0 */
        if (!((x >> pos) & 1))
        {
            return 0;
        }

        /* prepare x by removing set bits after position pos */
        long tmp = x;
        tmp = tmp >> (pos + 1);
        tmp = tmp << (pos + 1);
        x ^= tmp;

        /* increment count every time the first set bit of x is removed (from the right) */
        int y;
        int count = 0;
        while (x != 0)
        {
            y = x & ~(x - 1);
            x ^= y;
            count++;
        }
        return count;
    }

    int
    main(void)
    {
        /* run tests */
        long num = 0b1010111;
        printf("%d
    "
    , count(num, 0)); /* prints: 1 */
        printf("%d
    "
    , count(num, 1)); /* prints: 2 */
        printf("%d
    "
    , count(num, 2)); /* prints: 3 */
        printf("%d
    "
    , count(num, 3)); /* prints: 0 */
        printf("%d
    "
    , count(num, 4)); /* prints: 4 */
        printf("%d
    "
    , count(num, 5)); /* prints: 0 */
        printf("%d
    "
    , count(num, 6)); /* prints: 5 */
    }