What is the efficient way to count set bits at a position or lower?
任何给定的
什么是最有效的方法来计数的比特位置或在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; } |
方法和
注:这不是一个DUP(如何计数的比特数集A的32位整数?这是一位不为所有潜在的范围0到X
这个C++得到G++来发射非常好的x86 ASM(GoogBort编译器资源管理器)。我希望它也能在其他64位体系结构上有效地编译(如果
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位构建,请比较其他替代方案。好的。
只要你对硬编码的
您可能希望它也能为
对于大的位集,移动整个内容的速度将比只弹出包含
如果您有一个64位元素的数组,并且想要分别计算每个元素中某个位置下的位,那么您肯定应该使用simd。该算法的移位部分向量化,而不仅仅是popcnt部分。在基于
要让编译器输出的ASM指令将:好的。
最明显的方法是生成一个蒙版(
这也有一个有趣的副作用,即将要测试的位作为寄存器中的顶位。测试符号位,而不是任何其他任意位,只需要很少的指令。算术右移可以将符号位广播到寄存器的其余部分,从而比通常的无分支代码更有效。好的。
做popcount是一个备受争议的问题,但实际上是难题中更棘手的部分。在x86上,它有非常有效的硬件支持,但仅在最近足够的硬件上。在Intel CPU上,
因此,为了安全地使用它,您需要使用不使用
不使用
这个广播可以通过算术右移63来完成,它在高位的副本中移动。好的。
Clang从原始版本生成了此代码。在Glenn介绍了4的不同实现之后,我意识到我可以通过编写更像ASM的源代码来引导GCC走向Clang的最佳解决方案。显然,直接请求算术右移的
这个源代码在x86-64和带有gcc和clang的arm64上生成了很好的代码。两者都只需在popcnt的输入上使用算术右移位(这样移位可以与popcnt并行运行)。它在32位x86和gcc上也编译得很好,因为屏蔽只发生在32位变量上(在添加了多个popcnt结果之后)。它是32位(当位集大于寄存器时)上讨厌的函数的其余部分。好的。
三元算子与原版本的gcc
用gcc编译5.3.0(GCC
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的背景上的两个补体
一些这些指令将离你而去时,内联。(例如,GCC会产生在ECX伯爵在第一广场)。
(三)相乘而得到的概念(GCC),并启用了
1 2 | shr rdi, 63 imul eax, edi |
而在端
性能分析microarch哈斯韦尔,以日期(乘雾从理学版):
mov r,r 融合域:1 0 UOP,潜伏期,在执行单元xor -归零法:1融合域的UOP公司,在执行单元not P0 P1是:UOP/1//P5 P6,潜伏期1 1c中,每0.25c吞吐量shl (又名sal )与计数是在cl uops 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变速操作系统的前端的港口),最糟糕的是一个瓶颈。
潜伏期:从关键路径当位准备的结果是:当
最佳
(三)病毒融合域(11)版本:(一uops前端每2.75c)。执行单位:bottlenecked仍然在转移端口(P0(P6)在每7c一2C型。潜伏期:从位到结果,从结果(POS)。(2
clang有一些不同的技巧:它不是
clang 3.7
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 |
Clang在使用多源版本或"比特广播"源版本时仍然使用
Clang的
使用bmi2(haswell和更高版本),最佳ASM版本可以将
x86移位指令具有疯狂的cisc语义,如果移位计数为零,则标志不会受到影响。因此变量计数移位指令(可能)依赖于标记的旧值。"正常的"x86
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一个)。关键路径延迟:
注意,当内联时,人工(或智能编译器)可以避免对
在内联时,我们可以使用一个输出寄存器,该寄存器必须至少早于
这是如此轻,以至于循环开销和设置输入操作数/存储结果将是主要因素。(并且
英特尔编译器有趣地将自己的脚射到了脚下,并没有利用一个[63]是符号位的事实。
从
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 |
这是非常理想的:
如果
我的直接反应是测试指定的位,并立即返回0,它是明确的。
如果您超过了这一点,那么创建一个设置了该位(和不太重要的位)的位掩码,并使用原始输入创建一个
至于创建遮罩:可以左移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; } |
这里假设
假设一个
我已经编辑了一个我以前见过的问题,它将检查一个数字中是否设置了奇数或偶数位。这是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 */ } |