C语言 如何检查8位无符号字符中的设置位数?

4dbbbstv  于 2023-11-16  发布在  其他
关注(0)|答案(8)|浏览(128)

所以我必须在C中找到一个无符号字符变量的设置位(在1上)?
一个类似的问题是How to count the number of set bits in a 32-bit integer?,但它使用的算法不容易适应8位无符号字符(或其不明显)。

nqwrtyyt

nqwrtyyt1#

问题How to count the number of set bits in a 32-bit integer?中建议的算法通常适用于8位:

int NumberOfSetBits( uint8_t b )
{
     b = b - ((b >> 1) & 0x55);
     b = (b & 0x33) + ((b >> 2) & 0x33);
     return (((b + (b >> 4)) & 0x0F) * 0x01);
}

字符串
这是一个简单的情况下,缩短常数的最低有效的8位,并删除最后24位右移。同样,它可以适应16位使用8位移位。请注意,在8位的情况下,32位算法的机械适应导致冗余* 0x01可以省略。

lsmd5eda

lsmd5eda2#

对于8位变量,最快的方法是使用查找表。
构建一个包含256个值的数组,每个值对应一个8位组合。每个值都应包含其相应索引中的位数:

int bit_count[] = {
// 00 01 02 03 04 05 06 07 08 09 0a, ... FE FF
    0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, ..., 7, 8
};

字符串
获得组合的计数与从bit_count数组中查找值是相同的。这种方法的优点是非常快。
您可以使用一个简单的程序来生成数组,该程序以缓慢的方式逐位计数:

for (int i = 0 ; i != 256 ; i++) {
    int count = 0;
    for (int p = 0 ; p != 8 ; p++) {
        if (i & (1 << p)) {
            count++;
        }
    }
    printf("%d, ", count);
}


demo that generates the table)中的值。
如果要用一些CPU周期换取内存,可以使用16字节的查找表进行两次4位查找:

static const char split_lookup[] = {
    0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4
};

int bit_count(unsigned char n) {
    return split_lookup[n&0xF] + split_lookup[n>>4];
}


Demo的一个。

s3fp2yjn

s3fp2yjn3#

我想你正在寻找8位的汉明权重算法?如果是真的,下面是代码:

unsigned char in = 22; //This is your input number
unsigned char out = 0;
in = in - ((in>>1) & 0x55);
in = (in & 0x33) + ((in>>2) & 0x33);
out = ((in + (in>>4) & 0x0F) * 0x01) ;

字符串

exdqitrt

exdqitrt4#

计数不同于0的数字的个数也称为Hamming Weight。在本例中,您正在计数1的个数。
Dasblinkenlight为您提供了一个表驱动的实现,而Olaf为您提供了一个基于软件的解决方案。我认为您还有两个其他潜在的解决方案。第一个是使用编译器扩展,第二个是使用ASM特定的指令与C的内联汇编。
对于第一个替代方案,请参阅GCC的__builtin_popcount()。(感谢Artless Noise)。
对于第二种选择,您没有指定嵌入式处理器,但我将提供这种情况下,它的ARM为基础。
一些ARM处理器有VCNT指令,它会为你执行计数。所以你可以用C语言的内联汇编来完成:

inline
unsigned int hamming_weight(unsigned char value) {
    __asm__ __volatile__ (
            "VCNT.8"
            : "=value"
            : "value"
    );

    return value;
}

字符串
参见Fastest way to count number of 1s in a register, ARM assembly
为了完整起见,这里是Kernighan的位计数算法:

int count_bits(int n) {
    int count = 0;
    while(n != 0) {
        n &= (n-1);
        count++;
    }
    return count;
}


参见Please explain the logic behind Kernighan's bit counting algorithm

ia2d9nvy

ia2d9nvy5#

我做了一个优化的版本。使用32位处理器,利用乘法,移位和掩码可以为相同的任务编写更小的代码,特别是当输入域很小(8位无符号整数)时。

#include <limits.h>
#include <stdint.h>

unsigned int bit_count_uint8(uint8_t x)
{
    uint32_t n;
    n = (uint32_t)(x * 0x08040201UL);
    n = (uint32_t)(((n >> 3) & 0x11111111UL) * 0x11111111UL);
    /* The "& 0x0F" will be optimized out but I add it for clarity. */
    return (n >> 28) & 0x0F;
}

/*
This "parallel counting" version also works, but is more suitable for
"large" integers (usually > 32 bits) than small ones like 8-bit.

unsigned int bit_count_uint8_parallel(uint8_t x)
{
    x = x - ((x >> 1) & 0x55);
    x = (x & 0x33) + ((x >> 2) & 0x33);
    x = ((x + (x >> 4)) & 0x0F);
    return x;
}
*/

/* C23 compatible interface */
#if CHAR_BIT == 8
static inline \
unsigned int stdc_count_ones_uc(unsigned char value) {
    return bit_count_uint8(value);
}
#endif

字符串

  • (编辑2023-11-07:C23标准将添加一个名为stdc_count_ones_uc()的新API(在头文件<stdbit.h>中定义)。我在上面的示例代码中添加了一个兼容性 Package 器。)*

技术说明

  • 这产生了最小的二进制代码IA-32,x86-64和AArch 32(没有 neon 指令集),就我所能找到的。对于ARM neon ,请看the other answer I posted
  • 对于x86-64,这并不使用最少数量的指令,但是位移位和向下转换避免了使用64位指令,因此在编译的二进制文件中保存了几个字节。
  • 有趣的是,在IA-32和x86-64中,使用模((((uint32_t)(x * 0x08040201U) >> 3) & 0x11111111U) % 0x0F)的上述算法的变体实际上产生更大的代码,因为需要在div指令之后移动返回值(mov eax,edx)的余数寄存器。

说明

我将字节x的8位从MSB到LSB表示为 abcdefgh

abcdefgh
*   00001000 00000100 00000010 00000001 (make 4 copies of x
---------------------------------------  with appropriate
abc defgh0ab cdefgh0a bcdefgh0 abcdefgh  bit spacing)
>> 3                                   
---------------------------------------
    000defgh 0abcdefg h0abcdef gh0abcde
&   00010001 00010001 00010001 00010001
---------------------------------------
    000d000h 000c000g 000b000f 000a000e
*   00010001 00010001 00010001 00010001
---------------------------------------
    000d000h 000c000g 000b000f 000a000e
... 000h000c 000g000b 000f000a 000e
... 000c000g 000b000f 000a000e
... 000g000b 000f000a 000e
... 000b000f 000a000e
... 000f000a 000e
... 000a000e
... 000e
    ^^^^ (Bits 31-28 will contain the sum of the bits
          a, b, c, d, e, f, g and h. Extract these
          bits and we are done.)

确认

我并不认为我的算法是完全原创的。
Sean Eron安德森的Bit Twiddling Hacks页面提到了这个算法的一个版本,使用64位处理器,可以计算14位整数的设置位数(14位限制来自模运算):

unsigned int v; // input
unsigned int c;
c = (v * 0x200040008001ULL & 0x111111111111111ULL) % 0x0F;


这就是我写一个使用32位处理器的版本的灵感。

xzv2uavs

xzv2uavs6#

也许不是最快的,但很简单:

int count = 0;

for (int i = 0; i < 8; ++i) {
    unsigned char c = 1 << i;
    if (yourVar & c) {
        //bit n°i is set
        //first bit is bit n°0
        count++;
    }
}

字符串

euoag5mw

euoag5mw7#

对于8/16位MCU,循环很可能比并行加法方法更快,因为这些MCU每个指令不能移位超过一位,因此:

size_t popcount(uint8_t val)
{
    size_t cnt = 0;
    do {
        cnt += val & 1U;    // or: if ( val & 1 ) cnt++;
    } while ( val >>= 1 ) ;
    return cnt;
}

字符串
对于cnt的增量,你可以分析一下。如果仍然太慢,一个assignment实现可能值得尝试使用carry flag(如果可用)。虽然我一般反对使用汇编优化,但这样的算法是少数几个好的例外之一(仍然是在C版本失败之后)。
如果你可以省略Flash,@dasblinkenlight提出的查找表可能是最快的方法。
只是一个提示:对于某些架构(特别是ARM和x86/64),gcc有一个内置的:__builtin_popcount(),如果可用的话,你也可以尝试一下(尽管它至少需要int)。这可能需要一个CPU指令-你不能得到更快,更紧凑。

iyzzxitl

iyzzxitl8#

请允许我发布第二个答案。这是具有高级SIMD扩展( neon )的ARM处理器的最小可能。它甚至比__builtin_popcount()更小(因为__builtin_popcount()针对unsigned int输入进行了优化,而不是uint8_t)。

#ifdef __ARM_NEON
/* ARM C Language Extensions (ACLE) recommends us to check __ARM_NEON before
   including <arm_neon.h> */
#include <arm_neon.h>

unsigned int bit_count_uint8(uint8_t x)
{
    /* Set all lanes at once so that the compiler won't emit instruction to
       zero-initialize other lanes. */
    uint8x8_t v = vdup_n_u8(x);
    /* Count the number of set bits for each lane (8-bit) in the vector. */
    v = vcnt_u8(v);
    /* Get lane 0 and discard other lanes. */
    return vget_lane_u8(v, 0);
}
#endif

字符串

相关问题