计算字节中设置的位数

14

我很感兴趣,想知道在这种情况下计算字节中设置的比特数的最佳方法是什么

template< unsigned char byte > class BITS_SET
{
public:
    enum {
     B0 = (byte & 0x01) ? 1:0,
     B1 = (byte & 0x02) ? 1:0,
     B2 = (byte & 0x04) ? 1:0,
     B3 = (byte & 0x08) ? 1:0,
     B4 = (byte & 0x10) ? 1:0,
     B5 = (byte & 0x20) ? 1:0,
     B6 = (byte & 0x40) ? 1:0,
     B7 = (byte & 0x80) ? 1:0
    };
public:
 enum{RESULT = B0+B1+B2+B3+B4+B5+B6+B7};
};

如果在运行时知道字节的值,那么使用这种方法是否最优?建议在代码中使用吗?


这被称为人口统计,并且可以比一次测试一个位更有效地完成。在x86上,可以使用单个指令完成。在其他体系结构上,可以用几条指令完成。 - Paul R
1
@PaulR,所提出的模板解决方案将在编译时计算,运行时只需要不到一条指令! - Mark Ransom
啊 - 抱歉 - 没有理解问题的重点! - Paul R
@PaulR,这个问题有点令人困惑-它展示了只能在编译时工作的代码,但却问其是否在运行时最优。我猜测提问者的母语不是英语。 - Mark Ransom
是的,当然。这不是我的母语,我来自格鲁吉亚,但我不明白你为什么提到这个? - user466534
https://en.cppreference.com/w/cpp/numeric/popcount - Jesper Juhl
13个回答

30

对于一个字节的数据,同时考虑速度和内存消耗的最优方式是:

uint8_t count_ones (uint8_t byte)
{
  static const uint8_t NIBBLE_LOOKUP [16] =
  {
    0, 1, 1, 2, 1, 2, 2, 3, 
    1, 2, 2, 3, 2, 3, 3, 4
  };


  return NIBBLE_LOOKUP[byte & 0x0F] + NIBBLE_LOOKUP[byte >> 4];
}

在大多数系统上,从for循环中调用此函数应该会产生相当高效的程序。而且它非常通用。


这比在一个256个条目的表中进行简单查找要花费两倍的时间。 - Ira Baxter
8
@IraBaxter 的确。而且它需要的内存少了16倍。因此,这是嵌入式系统中常见的解决方案之一。 - Lundin
我曾认为 const 默认是 static 的。它们不是吗? - Rui Monteiro
@RuiMonteiro 不一定。本地作用域的“const”理论上也可能会被分配到堆栈中,就像只读内存一样。我想,一些不好的嵌入式系统编译器可能会想到在执行之前将查找表从闪存复制到RAM中。还有哈佛系统方面 - 在哈佛系统上,您甚至可能无法直接从闪存中读取表格。在这种情况下,您可能必须添加一些非标准关键字才能使代码正常工作。 - Lundin

19

对于8位值,只需使用256个元素的查找表。

对于更大尺寸的输入,这就稍微不那么简单了。Sean Eron Anderson在他的Bit Twiddling Hacks页面上提供了几种不同的函数,所有这些函数都具有不同的性能特征。由于它取决于处理器(流水线深度,分支预测器,缓存大小等)和你使用的数据的性质,因此没有一种全能快速的版本。


不要使用256个元素的查找表,这不值得。使用16个元素的表,只需查找每个nibble即可。 - user541686

13

为什么不直接使用标准库?这样最优的实现方式应该由编译器确定,而且很可能比你实际编写的符合标准的代码更好。例如,如果你在 x86 上运行,这将编译为一条指令,但前提是你的目标 CPU 支持

#include <bitset>
#include <iostream>

int main() {
  unsigned char bitfield = 17;
  std::cout << std::bitset<8>(bitfield).count() <<
    std::endl;
}

FYI: 在使用支持SSE4的i5处理器和发布版本的VS2017中,这仍然不能编译成POPCNT,而是使用传统的LUT算法...可悲。 - SonarJetLens
或者直接使用 std::popcount() - Jesper Juhl
std::popcount确实是一个很棒的函数。谢谢你指出来。然而,当我最初在2015年写下这个答案时,它还不是C++标准的一部分。直到C++20才被标准化。 - OmnipotentEntity
std::popcount确实是一个很棒的函数。感谢您指出它的存在。然而,当我最初在2015年写下这个答案时,它还没有成为C++标准的一部分。直到C++20才被标准化。 - OmnipotentEntity

4

对于单个字节值,最快的方法是将答案存储在一个256字节数组中,你可以使用该值进行索引。例如:bits_set[] = {0, 1, 1, 2, ...


3
在当今处理器速度快而内存相对较慢的情况下,我必须看到查找表和一些位操作代码之间的竞争才能宣布查找表是“最快”的。 - Mark Ransom
1
如果你不想创建一个256元素的数组,你可以创建一个16元素的数组,并将第二个一半进行位移。 - Matthew
@MarkRansom:但是一个小的查找表很可能适合于CPU的L1缓存。 - Nathan Osman
@Matthew:一旦你只使用了16个字节,你甚至可以利用一些处理器寄存器来实现这个目的。 - Nathan Osman
@GeorgeEdison,是的,它会适合,但它会推出什么? - Mark Ransom
显示剩余2条评论

3
为什么不进行左移并掩码处理其余部分呢?
int countBits(unsigned char byte){
    int count = 0;
    for(int i = 0; i < 8; i++)
        count += (byte >> i) & 0x01; // Shift bit[i] to the first position, and mask off the remaining bits.
    return count;
}

这个代码可以轻松地适应任何大小的 int 类型,只需简单计算计数值中有多少位,然后在计数循环中使用该值即可。这非常简单。

int countBits(unsigned long long int a){
    int count = 0;
    for(int i = 0; i < sizeof(a)*8; i++)
        count += (a >> i) & 0x01;
    return count;
}

2

2
“最快的位计数方法”通常的答案是“查找数组中的字节”。这对于字节来说有点起作用,但你需要实际访问内存。 如果您只偶尔执行此操作,则可能是最快的,但是如果您只偶尔执行此操作,则不需要最快的速度。
如果您经常执行此操作,则最好将字节批处理成单词或双字,并对其进行快速位计数操作。这些往往是纯算术运算,因为您无法实际上查找32位值以获取其位数。相反,您通过聪明地移位和屏蔽来组合值。
一个伟大的源泉,提供了这样做的聪明技巧:Bit Hacks
以下是在C中计算32位字中位数的方案。
 unsigned int v; // count bits set in this (32-bit value)
 unsigned int c; // store the total here

 v = v - ((v >> 1) & 0x55555555);                    // reuse input as temporary
 v = (v & 0x33333333) + ((v >> 2) & 0x33333333);     // temp
 c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; // count

这意味着我的代码不可移植,对吗?一般来说,我的问题是指如果编译器支持模板编程,哪个更好? - user466534
你的目标计算机在编译时可以轻松地知道本地字长。如果你的代码基于处理位数,那么在可预见的未来,你最多会有4个例程:8、16、32、64位(如果你认为128位很快就会出现,那也可以)。我认为这非常便携,并且它将在你编译的任何目标架构上运行得很快。[对于这个问题,我不确定你是否想使用模板元编程]。 - Ira Baxter
这是针对32位数字的...针对8位数字更简单(请检查我的答案)。 - Zibri

1
在gcc中,您可以使用__builtin_popcount(unsigned)函数。
它应该有效地使用目标硬件平台的最佳解决方案。
使用-march = core-avx2(与我的CPU兼容的最高级别),将使用popcntl x86_64汇编指令,在硬件中执行此操作。
对于默认的x86_64指令集,将调用popcntl函数,该函数实现了最佳的C(聪明的黑客)算法。
还有__builtin_popcountl和__builtin_popcountll用于无符号长整型和无符号长长整型。

1
#include <iostream>
#include <climits> // for CHAR_BIT (most likely to be 8)
#include <cstring> // for memset
#include <new> 

static const int DUMMY = -1;

// first approch : activate the O(8) function in first get try... after that its O(1);
class bitsInByteflyLUT
{
    typedef unsigned char byte;

    public:
        bitsInByteflyLUT();     //CTOR - throws std::bad_alloc
        ~bitsInByteflyLUT();    //DTOR


        int Get_bitsInByte(byte _byte);     


    private:
        // CLASS DATA
        int*    flyLUT;

        // PRIVATE FUNCTIONS
        int bitsInByte(byte _byte);
        // O(8) for finding how many bits are ON in a byte.
        // answer can be between 0 to CHAR_BIT.

        bitsInByteflyLUT(const bitsInByteflyLUT & _class); // COPY CTOR - forbidden
        const bitsInByteflyLUT & operator= (const bitsInByteflyLUT& _class);
        // ASSIGN OPERATOR - forbidden

};

bitsInByteflyLUT::bitsInByteflyLUT()
{
    size_t nIndexes = 1 << CHAR_BIT;
    try
    {
        flyLUT =  new int[nIndexes];
    }
    catch (std::bad_alloc& ba)
    {
        throw;
    }
    memset(flyLUT, DUMMY, sizeof(int)*nIndexes);
}


bitsInByteflyLUT::~bitsInByteflyLUT()
{
    delete[] flyLUT;
}


int bitsInByteflyLUT::Get_bitsInByte(byte _byte)
{
    if (flyLUT[_byte] == DUMMY) // if its first time we try to get answer for this char.
    {
        flyLUT[_byte] = bitsInByte(_byte); // O(8)
    }
    return flyLUT[_byte]; // O(1) 
}

int bitsInByteflyLUT::bitsInByte(byte _byte)
{   
    byte nBits = CHAR_BIT;
    byte counter = 0;
    byte mask = 1;
    while(nBits--)
    {
        if(mask & _byte)
        {
            ++counter;
        }
        mask <<= 1;
    }
    return counter;
}





int main ()
{
    using std::cout;
    using std::endl;

    bitsInByteflyLUT flut;

    for (unsigned int i = 0; i < (1 << CHAR_BIT); i += 1)
    {   
        cout << i << " " << flut.Get_bitsInByte(i) << endl;
    }

    return 0;
}

1
使用C++17,您可以使用constexpr lambda预计算查找表。这样更容易理解其正确性,而不是使用已准备好的复制粘贴表格。
#include <array>
#include <cstdint>

static constexpr auto bitsPerByteTable = [] {
  std::array<uint8_t, 256> table{};
  for (decltype(table)::size_type i = 0; i < table.size(); i++) {
    table.at(i) = table.at(i / 2) + (i & 1);
  }
  return table;
}();

您也可以离线运行表生成以生成表格,然后简单地使用它。实际上,这只适用于8位值。如果您想对更大的值执行此操作,则使用多个8位查找不会是最快的。请参见我的答案。 - Ira Baxter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接