表示数字7的8位二进制数如下:
00000111
有三个二进制位被设置为1。
如何确定32位整数中设置位的数量所使用的算法?
表示数字7的8位二进制数如下:
00000111
有三个二进制位被设置为1。
如何确定32位整数中设置位的数量所使用的算法?
popcnt
(几乎肯定是最快的单个整数操作)这样的指令将是最快的。其他一些体系结构可能会使用一个微码循环来实现一个慢速指令,每个周期测试一个位(需要引用-如果存在硬件popcount,通常很快)。如果您的CPU具有大容量缓存并且在紧密循环中执行大量此类操作,预填充的表查找方法可以非常快速。然而,由于“缓存未命中”的开销,它可能会受到影响,其中CPU必须从主存中获取一些表的内容。(单独查找每个字节以保持表的小型化。)如果您想要对连续范围的数字进行popcount计算,只有低字节在256个数字组中发生变化,这样非常好。
如果您知道字节主要是0或1,则针对这些情况有高效的算法,例如在循环中使用位操作清除最低位,直到变为零。
我相信以下是一个非常好的通用算法,被称为“并行”或“可变精度SWAR算法”。我用类似C的伪代码表达了这个算法,您可能需要根据特定语言进行调整(例如,在C++中使用uint32_t,在Java中使用>>>):
GCC10和clang 10.0可以识别这种模式/习语,并在可用时将其编译为硬件popcnt或等效指令,为您提供两全其美的选择。 (Godbolt)int numberOfSetBits(uint32_t i)
{
// Java: use int, and use >>> instead of >>. Or use Integer.bitCount()
// C or C++: use uint32_t
i = i - ((i >> 1) & 0x55555555); // add pairs of bits
i = (i & 0x33333333) + ((i >> 2) & 0x33333333); // quads
i = (i + (i >> 4)) & 0x0F0F0F0F; // groups of 8
i *= 0x01010101; // horizontal sum of bytes
return i >> 24; // return just that top byte (after truncating to 32-bit even when int is wider than uint32_t)
}
|0
将其强制转换为整数:将第一行改为i = (i|0) - ((i >> 1) & 0x55555555);
这是讨论的算法中具有最好的最坏情况行为,因此它能有效地处理任何使用模式或值。在普通的CPU上,它的性能不依赖于数据,包括乘法在内的所有整数操作都是恒定时间。它在"简单"输入上不会变得更快,但仍然相当不错。i = i - ((i >> 1) & 0x55555555);
(i + (i >> 4)) & 0x0F0F0F0F
扩展为4个8位累加器。它在添加之后进行掩码操作,而不是之前,因为任何4位累加器中的最大值是4
,如果相应的输入位的4位都被设置。4+4=8仍然适合4位,因此在i + (i >> 4)
中不可能在半字节元素之间进行进位。vpternlogd
使Harley-Seal非常好。)unsigned int
,以便轻松表明它没有任何符号位的复杂性。此外,使用 uint32_t
是否更安全,即在所有平台上都能得到您所期望的结果? - Craig McQueen>>
的行为是由具体实现定义的。参数需要被更改(或强制转换)为 unsigned
类型,并且由于该代码只针对32位特定情况,应该使用 uint32_t
。 - R.. GitHub STOP HELPING ICEint __builtin_popcount (unsigned int x);
int __builtin_popcountll (unsigned long long x);
popcnt
指令,使用-mpopcnt
(也可以通过-msse4.2
隐含地支持)。请参考GCC x86选项。选择-march=nehalem -mtune=skylake
(或者选择你想要代码假设和优化的CPU)可能是一个不错的选择。在旧的CPU上运行生成的二进制文件将导致非法指令错误。-march=native
(使用gcc、clang或ICC)。
MSVC为x86的popcnt
指令提供了内置函数,但与gcc不同,它实际上是针对硬件指令的内置函数,需要硬件支持。
std::bitset<>::count()
而不是内置的方法std::bitset<>
来暴露这个功能。实际上,在某些情况下,对于某些目标CPU,使用位操作的AND/shift/ADD可能更好。std::bitset
。例如,MSVC无法在编译时启用popcnt
支持,而它的std::bitset<>::count
始终使用表查找,即使使用了/Ox /arch:AVX
(它意味着SSE4.2,进而意味着popcnt功能)。 (更新:请参见下文;这确实使得MSVC的C++20 std::popcount
使用x86 popcnt
,但仍然无法使其bitset<>::count使用。MSVC可以通过更新其标准库头文件以在可用时使用std::popcount来解决这个问题。)#include <bitset>
#include <limits>
#include <type_traits>
template<typename T>
//static inline // static if you want to compile with -mpopcnt in one compilation unit but not others
typename std::enable_if<std::is_integral<T>::value, unsigned >::type
popcount(T x)
{
static_assert(std::numeric_limits<T>::radix == 2, "non-binary type");
// sizeof(x)*CHAR_BIT
constexpr int bitwidth = std::numeric_limits<T>::digits + std::numeric_limits<T>::is_signed;
// std::bitset constructor was only unsigned long before C++11. Beware if porting to C++03
static_assert(bitwidth <= std::numeric_limits<unsigned long long>::digits, "arg too wide for std::bitset() constructor");
typedef typename std::make_unsigned<T>::type UT; // probably not needed, bitset width chops after sign-extension
std::bitset<bitwidth> bs( static_cast<UT>(x) );
return bs.count();
}
See asm from gcc, clang, icc, and MSVC on the Godbolt compiler explorer.
x86-64gcc -O3 -std=gnu++11 -mpopcnt
发出以下内容:unsigned test_short(short a) { return popcount(a); }
movzx eax, di # note zero-extension, not sign-extension
popcnt rax, rax
ret
unsigned test_int(int a) { return popcount(a); }
mov eax, edi
popcnt rax, rax # unnecessary 64-bit operand size
ret
unsigned test_u64(unsigned long long a) { return popcount(a); }
xor eax, eax # gcc avoids false dependencies for Intel CPUs
popcnt rax, rdi
ret
gcc -O3 -std=gnu++11
发出的代码(针对 int
参数版本)如下: rldicl 3,3,0,32 # zero-extend from 32 to 64-bit
popcntd 3,3 # popcount
blr
std::popcount(T)
if(x==0) return 0;
,当使用clang编译为x86时,它无法被优化掉。#include <bit>
int bar(unsigned x) {
return std::popcount(x);
}
-O3 -std=gnu++20 -march=nehalem
现场演示
# clang 11
bar(unsigned int): # @bar(unsigned int)
popcnt eax, edi
cmove eax, edi # redundant: if popcnt result is 0, return the original 0 instead of the popcnt-generated 0...
ret
# gcc 10
xor eax, eax # break false dependency on Intel SnB-family before Ice Lake.
popcnt eax, edi
ret
-arch:AVX
或更高版本(并启用-std:c++latest
),它也能很好地运行。在线演示
int bar(unsigned int) PROC ; bar, COMDAT
popcnt eax, ecx
ret 0
int bar(unsigned int) ENDP ; bar
std::bitset::count
。将其内联后,编译器会将其优化为单个 __builtin_popcount
调用。 - deft_codeunsigned int bitCount (unsigned int value) {
unsigned int count = 0;
while (value > 0) { // until all bits are zero
if ((value & 1) == 1) // check lower bit
count++;
value >>= 1; // shift bits, removing lower bit
}
return count;
}
如果你想要更快的速度(假设你对此进行了良好的记录以帮助后继者),你可以使用表格查找:
// Lookup table for fast calculation of bits set in 8-bit unsigned char.
static unsigned char oneBitsInUChar[] = {
// 0 1 2 3 4 5 6 7 8 9 A B C D E F (<- n)
// =====================================================
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, // 0n
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, // 1n
: : :
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, // Fn
};
// Function for fast calculation of bits set in 16-bit unsigned short.
unsigned char oneBitsInUShort (unsigned short x) {
return oneBitsInUChar [x >> 8]
+ oneBitsInUChar [x & 0xff];
}
// Function for fast calculation of bits set in 32-bit unsigned int.
unsigned char oneBitsInUInt (unsigned int x) {
return oneBitsInUShort (x >> 16)
+ oneBitsInUShort (x & 0xffff);
}
这些依赖于特定的数据类型大小,因此它们并不是那么易携带。但是,由于许多性能优化本身就不可移植,所以这可能不是一个问题。如果您想要可移植性,我建议使用易读的解决方案。
if ((value & 1) == 1) { count++; }
替换为 count += value & 1
,会更加合理易懂。这样不会改变原意。 - Ponkadoodleint pop(unsigned x)
{
x = x - ((x >> 1) & 0x55555555);
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x + (x >> 4)) & 0x0F0F0F0F;
x = x + (x >> 8);
x = x + (x >> 16);
return x & 0x0000003F;
}
在约20个指令(与架构有关)内执行,没有分支。
Hacker's Delight 这本书真是太好了! 强烈推荐。
Integer.bitCount(int)
使用完全相同的实现。 - Marco Bolis0x01010101
常量将所有4个字节加总到高字节中。 (我会先执行x>>16
部分,使缩小工作通过两次缩小有价值的部分来进行。)在没有快速乘法器的CPU上,这是可取的(特别是如果它可以有效地进行大移位,而不是像每个周期1位那样。或者如果它有其他提取高半部分的方法;例如,在16位CPU上,它已经被拆分了)。 - Peter Cordes我认为最快的方法,不使用查找表和popcount,是以下方法。它只需12个操作就可以计算出设置位。
int popcount(int v) {
v = v - ((v >> 1) & 0x55555555); // put count of each 2 bits into those 2 bits
v = (v & 0x33333333) + ((v >> 2) & 0x33333333); // put count of each 4 bits into those 4 bits
return ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
}
之所以可行,是因为你可以将所有位的数量分成两半,计算两个半部分的位数并相加。这种方法也被称为“分而治之”范例。让我们深入了解...
v = v - ((v >> 1) & 0x55555555);
两个比特的位数可以是 0b00
、0b01
或者 0b10
。让我们试着在两个比特上计算一下。
---------------------------------------------
| v | (v >> 1) & 0b0101 | v - x |
---------------------------------------------
0b00 0b00 0b00
0b01 0b00 0b01
0b10 0b01 0b01
0b11 0b01 0b10
这就是所需的内容:最后一列显示了每个两位数对中设置位的计数。如果两位数>= 2 (0b10)
,则and
生成0b01
,否则生成0b00
。
v = (v & 0x33333333) + ((v >> 2) & 0x33333333);
这个语句应该很容易理解。在第一次操作后,我们得到了每两位中集合位的计数,现在我们在每四位中总计这个计数。
v & 0b00110011 //masks out even two bits
(v >> 2) & 0b00110011 // masks out odd two bits
我们将上述结果相加,得到4个比特位中置位的总数。最后一个语句是最棘手的。
c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
让我们进一步分解它...
v + (v >> 4)
这与第二个语句类似;我们按照每4位一组来计算集合中的位数。由于之前的操作,我们知道每个四位二进制数都有其相应的设置位数。让我们看一个例子。假设我们有一个字节0b01000010
。这意味着第一个四位二进制数有4位被设置,第二个四位二进制数有2位被设置。现在我们将这两个四位二进制数加在一起。
v = 0b01000010
(v >> 4) = 0b00000100
v + (v >> 4) = 0b01000010 + 0b00000100
这给出了一个字节中集合位的计数,例如在第二个半字节 0b01000110
中,因此我们屏蔽掉数字中所有字节的前四个字节(将其丢弃)。
0b01000110 & 0x0F = 0b00000110
现在每个字节都有它的一些位被设置。我们需要将它们全部加起来。诀窍是将结果乘以0b10101010
,这具有有趣的性质。如果我们的数字有四个字节,A B C D
,则结果为一个新的数字,其字节为A+B+C+D B+C+D C+D D
。一个4字节的数字最多可以有32个位设置为1,这可以表示为0b00100000
。>> 24
获得。此算法设计用于32位字,但可以轻松修改为64位字。c =
是什么意思?似乎应该被删除。此外,建议添加一组括号A"(((v + (v >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24"以避免一些经典的警告。 - chux - Reinstate Monicapopcount(int v)
和popcount(unsigned v)
。为了提高可移植性,请考虑使用popcount(uint32_t v)
等类型。我真的很喜欢*0x1010101这部分。 - chux - Reinstate Monicareturn (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
这样我们就不需要数字母来看你实际上在做什么(因为你丢弃了第一个 0
,我不小心以为你使用了错误的(翻转的)位模式作为掩码 - 直到我注意到只有7个字母而不是8个)。 - emem如果您正在使用Java,内置方法Integer.bitCount
可以实现此功能。
我感到无聊,于是计时了三种方法的十亿次迭代。编译器为gcc -O3。CPU为第一代Macbook Pro所配备的。
最快的方法是以下代码,用时3.7秒:
static unsigned char wordbits[65536] = { bitcounts of ints between 0 and 65535 };
static int popcount( unsigned int i )
{
return( wordbits[i&0xFFFF] + wordbits[i>>16] );
}
第二名是相同的代码,但查找4个字节而不是2个半字。这需要大约5.5秒。
第三名是位操作的“侧向加法”方法,用时8.6秒。
第四名是GCC的__builtin_popcount(),耗时11秒,令人惭愧。
逐位计算的方法要慢得多,我等得有点无聊了。
因此,如果您最关心性能,请使用第一种方法。如果您关心,但不想花费64Kb的RAM,则使用第二种方法。否则,请使用可读性较差(但速度较慢)的逐位计算方法。
很难想象您会想使用位操作的方法。
编辑:类似的结果在这里。
unsigned int count_bit(unsigned int x)
{
x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);
x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);
x = (x & 0x0000FFFF) + ((x >> 16)& 0x0000FFFF);
return x;
}
让我来解释一下这个算法。
这个算法基于分治算法。假设有一个8位整数213(二进制中为11010101),算法的工作方式如下(每次合并相邻的两个块):
+-------------------------------+
| 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | <- x
| 1 0 | 0 1 | 0 1 | 0 1 | <- first time merge
| 0 0 1 1 | 0 0 1 0 | <- second time merge
| 0 0 0 0 0 1 0 1 | <- third time ( answer = 00000101 = 5)
+-------------------------------+
这是一个需要了解微架构的问题。我刚刚使用C++内联函数来消除函数调用开销,使用rdtsc进行时序测量(时钟周期精确度),在gcc 4.3.3下编译,采用-O3选项,迭代10亿次,并保持所有计数的运行总和以确保编译器不会删除任何重要部分。
inline int pop2(unsigned x, unsigned y) { x = x - ((x >> 1) & 0x55555555); y = y - ((y >> 1) & 0x55555555); x = (x & 0x33333333) + ((x >> 2) & 0x33333333); y = (y & 0x33333333) + ((y >> 2) & 0x33333333); x = (x + (x >> 4)) & 0x0F0F0F0F; y = (y + (y >> 4)) & 0x0F0F0F0F; x = x + (x >> 8); y = y + (y >> 8); x = x + (x >> 16); y = y + (y >> 16); return (x+y) & 0x000000FF; }
Hacker's Delight未修改版本需要12.2千兆周期。我的并行版本(计算两倍的位数)需要13.0千兆周期。在2.4GHz的Core Duo上,两者总共用时10.5秒。25千兆周期=这个时钟频率下略超过10秒,因此我对我的计时非常有信心。
这与指令依赖链有关,对于这个算法非常不利。如果我使用一对64位寄存器,我几乎可以再次提高速度。实际上,如果我聪明地稍早加上x+y,我可以削减一些移位操作。64位版本进行一些小的调整后会达到相同的效果,但是计算的位数会再次翻倍。
有了128位的SIMD寄存器,又增加了一倍的速度,而SSE指令集通常也有巧妙的快捷方式。代码没有必要特别透明。接口简单,算法可以在许多地方在线引用,并且适合全面的单元测试。偶然发现它的程序员甚至可能会学到一些东西。在机器级别上,这些位操作非常自然。
好的,我决定测试修改后的64位版本。对于这个版本,sizeof(unsigned long) == 8。
inline int pop2(unsigned long x, unsigned long y) { x = x - ((x >> 1) & 0x5555555555555555); y = y - ((y >> 1) & 0x5555555555555555); x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333); y = (y & 0x3333333333333333) + ((y >> 2) & 0x3333333333333333); x = (x + (x >> 4)) & 0x0F0F0F0F0F0F0F0F; y = (y + (y >> 4)) & 0x0F0F0F0F0F0F0F0F; x = x + y; x = x + (x >> 8); x = x + (x >> 16); x = x + (x >> 32); return x & 0xFF; }
看起来差不多正确(虽然我没有认真测试)。现在的时间为10.70 gigacycles / 14.1 gigacycles。后面这个数字加起来有1280亿个比特,并对应于此台机器上经过了5.9秒的时间。非并行版本的速度略微提高,因为我正在64位模式下运行,它比32位寄存器稍微更喜欢64位寄存器。
让我们看看这里是否还有更多的OOO流水线技术可用。 这需要更多的涉及,因此我实际上进行了一些测试。 每个项单独加起来总和为64,所有组合总和为256。
内联函数 int pop4(unsigned long x, unsigned long y, unsigned long u, unsigned long v) { 枚举 { m1 = 0x5555555555555555, m2 = 0x3333333333333333, m3 = 0x0F0F0F0F0F0F0F0F, m4 = 0x000000FF000000FF };
x = x - ((x >> 1) & m1); y = y - ((y >> 1) & m1); u = u - ((u >> 1) & m1); v = v - ((v >> 1) & m1); x = (x & m2) + ((x >> 2) & m2); y = (y & m2) + ((y >> 2) & m2); u = (u & m2) + ((u >> 2) & m2); v = (v & m2) + ((v >> 2) & m2); x = x + y; u = u + v; x = (x & m3) + ((x >> 4) & m3); u = (u & m3) + ((u >> 4) & m3); x = x + u; x = x + (x >> 8); x = x + (x >> 16); x = x & m4; x = x + (x >> 32); 返回 x & 0x000001FF; }
一开始我很兴奋,但事实证明gcc即使我在某些测试中没有使用inline关键字,也会使用-O3进行嵌入技巧。当我让gcc玩弄技巧时,十亿次对pop4()的调用需要12.56 gigacycles,但我确定它正在将参数折叠为常数表达式。更现实的数字似乎是19.6gc,可再提高30%。我的测试循环现在看起来像这样,确保每个参数都不同,以防止gcc玩弄技巧。
hitime b4 = rdtsc(); for (unsigned long i = 10L * 1000*1000*1000; i < 11L * 1000*1000*1000; ++i) sum += pop4 (i, i^1, ~i, i|1); hitime e4 = rdtsc();
2560亿位在8.17秒内求和。根据16位表查找中的基准测试,每32 million bit使用1.02秒。不能直接比较,因为另一个基准测试没有给出时钟速度,但看起来我已经击败了64 KB表版,这本来就是对L1缓存的悲惨利用。
更新:决定做显而易见的事情,并通过添加四行重复代码来创建pop6()。总时间为22.8gc,总共3840亿位在9.5秒内求和。现在每32 billion bits使用800ms。
为什么不能反复除以2?
计数器 = 0 当 n > 0 时 如果 (n % 2) 等于 1 计数器加1 n 除以等于2
我同意这不是最快的方法,但是“最好”的定义有些模糊。我认为,“最好”应该具有清晰度的元素。
n
始终返回 0。 - chux - Reinstate Monica