在缓冲区中找到第一个未设置的位(优化)

时间:2010-07-30 16:21:48

标签: c performance algorithm optimization bit-manipulation

找到任意长度数组中第一个未设置位的位偏移的最快/最干净的方法是什么?

假设您的函数的原型类似于此size_t first_unset_bit(char unsigned const *buf, size_t bit_count, size_t start_bit);,并且可以在同一缓冲区上快速连续多次调用它。如果你能提供更好的原型,请证明合理。

如果您使用汇编任何程序集,请提供将在core2或更高版本上运行的x86示例。我将为解决方案提供答案,提供速度和美感的最佳组合。

Update0

这是我天真的实施。我不知道它是否真的正确,它还没有在现场系统中使用。

static size_t first_unset_bit(char unsigned const *buf, size_t bit_count, size_t start_bit)
{
    for (; start_bit < bit_count; ++start_bit)
    {
        size_t buf_index = start_bit / CHAR_BIT;
        int bit_index = start_bit % CHAR_BIT;
        if (!((buf[buf_index] >> bit_index) & 1))
            return start_bit;
    }
    return -1;
}

9 个答案:

答案 0 :(得分:3)

经常被忽视, strings.h (是的,那个标准的标题)包含一堆函数: ffs ffsl 等等,有关详细信息,请参阅here。至少使用gcc和x86,这将编译为相应的一个周期指令,例如BSFL。

我建议:

  1. 在数组末尾添加一个标记0xFFFF
  2. 将bit_count除以4(所以你的迭代超过4字节块而不是字节)
  3. 使用while循环查找具有第一个设置位的块
  4. 例如:

    cursor = start_pos;
    while(position = ffsl(buf))
      cursor++;
    return (cursor - startpos) * 32 + pos;
    

    (除非你必须测试你是否到达了哨兵,在这种情况下缓冲区是空白的。)

    虽然你应该考虑到这一点,因为我并不是一个装配专家...你基本上每32位使用3个周期(一个增量,一个比较,一个BSFL)指示),并想象你可以使用该函数的长版本做得更好。

答案 1 :(得分:2)

x-86 assembly language,

REPE SCAS 0xFFFFFFFF

......很可能是答案的重要组成部分!

你别无选择,只能检查第一个未设置位之前的每一位,所以这只是你能以多快的速度做到这一点。一次比较32位是一个好的开始,一旦你知道哪个WORD包含第一个未设置位,你可以使用移位/查找表的组合来找到该单词中的第一个未设置位。

答案 2 :(得分:2)

优化提示:创建查找表,将字节值映射到第一个未设置位而不是循环字节,但不是位。

答案 3 :(得分:1)

Linux有我想象的是一个名为“find_first_zero_bit”的高度优化的实现。

答案 4 :(得分:1)

不使用任何汇编语言,但使用GCC内置函数,并假设bit_countlong中位数的倍数,这样的事情应该有效。我更改了您的函数以获取void*缓冲区参数以避免别名问题。完全未经测试,我可能搞砸了数学,特别是在领先的“if(start_bit%LONG_BIT)块中。

#include <stddef.h>
#include <limits.h>
#define LONG_BIT (CHAR_BIT * sizeof(unsigned long))

size_t
first_unset_bit(const void *buf, size_t bit_count, size_t start_bit)
{
    size_t long_count = bit_count / LONG_BIT;
    size_t start_long = start_bit / LONG_BIT;

    const unsigned long *lbuf = (const unsigned long *)buf;

    if (start_bit % LONG_BIT)
    {
        size_t offset = start_bit % LONG_BIT;
        unsigned long firstword = lbuf[start_long];
        firstword = ~(firstword | ~((1UL << offset) - 1));
        if (firstword)
            return start_bit - offset + __builtin_clzl(firstword);

        start_long += 1;
    }

    for (size_t i = start_long; i < long_count; i++)
    {
        unsigned long word = lbuf[i];
        if (~word)
            return i*LONG_BIT + __builtin_clzl(~word);
    }
    return bit_count + 1; // not found
}

答案 5 :(得分:0)

显而易见的解决方案是从start_bit循环直到你到达数组的末尾或找到未设置的位。

因为它可以是任意长度,你不能把它变成一个数字并找到那个值,因为它可能比双倍的大小更高。

答案 6 :(得分:0)

我假设您的缓冲区已对齐,例如malloc返回的缓冲区。如果不是,您首先需要在开头扫描未对齐的部分。

uint32_t *p = (void *)buf;
while (!(*p+1)) p++;
size_t cnt = (unsigned char *)p - buf << CHAR_BIT;
if (*p>=0xFFFF0000)
  if (*p>=0xFFFFFF00)
    if (*p>=0xFFFFFFF0)
      if (*p>=0xFFFFFFFC)
        if (*p>=0xFFFFFFFE) cnt+=31;
        else cnt+=30;
      else
        if (*p>=0xFFFFFFF9) cnt+=29;
        else cnt+=28;
    else
      if (*p>=0xFFFFFFC0)
        if (*p>=0xFFFFFFE0) cnt+=27;
        else cnt+=26;
      else
        if (*p>=0xFFFFFF90) cnt+=25;
        else cnt+=24;
  else
    ...

我将把二进制搜索的其余部分留给你。

答案 7 :(得分:0)

正如其他人所提到的,汇编语言可能会提供最佳性能。如果这不是一个选项,您可能希望考虑以下(未经测试)例程。你要求的并不是那么多,但它应该足够接近你可以根据你的需要进行调整。

size_t findFirstNonFFbyte (
    unsigned char const *buf,       /* ptr to buffer in which to search */
    size_t               bufSize,   /* length of the buffer */
    size_t               startHint  /* hint for the starting byte (<= bufSize) */
    ) {
    unsigned char * pBuf = buf + startHint;
    size_t          bytesLeft;

    for (bytesLeft = bufSize - startHint;
         bytesLeft > 0;
         bytesLeft = startHint, pBuf = buf) {
        while ((bytesLeft > 0) && (*pBuf == 0xff)) {
            *pBuf++;
            bytesLeft--;
        }

        if (bytesLeft > 0) {
            return ((int) (pBuf - buf));
        }
    }
    return (-1);
}

使用......

index = findFirstNonFFbyte (...);
bit_index = index + bitTable[buffer[index]];

附加说明:

以上代码将一次检查8位。如果你知道你的缓冲区将是4字节对齐的,并且它的长度是4字节的偶数倍,那么你可以通过一点调整一次测试32位(不要忘记返回值计算)。

如果您的起始位不是提示而是绝对,那么您可以跳过for循环。

您需要提供自己的位查找表。它应该是一个256字节长的数组。每个条目标识索引该条目的字节的第一个清除位。个人经验告诉我,不同的人会以不同的方式编号。有些将位0称为字节中最重要的位;其他人将位0称为字节的最低有效位。无论你挑选什么样的风格,一定要保持一致。

希望这有帮助。

答案 8 :(得分:0)

使用gcc内置的相当于Microsoft的_BitScanReverse,我使用这样的东西来找到我的内存系统的第一个空闲位(代表块使用):

        __forceinline DWORD __fastcall GetNextFreeBlockIndex(PoolBlock* pPoolBlock)
        {
            DWORD dwIndex;
            DWORD dwOffset = 0;
            DWORD* pUsage = &pPoolBlock->fUsage[0];
            while(dwOffset < MMANAGER_BLOCKS_PER_POOL)
            {
                DWORD dwUsage = *pUsage;
                if(dwUsage != 0xFFFFFFFF && _BitScanForward(&dwIndex,~dwUsage))
                {
                    #if !( MMANAGER_ATOMIC_OPS )
                        pPoolBlock->pSync.Enter();
                    #endif

                    ATOMIC_Write(DWORD,pPoolBlock->dwFreeIndex,dwOffset);
                    ATOMIC_Write(DWORD*,pPoolBlock->pFreeUsage,pUsage);

                    #if !( MMANAGER_ATOMIC_OPS )
                        pPoolBlock->pSync.Leave();
                    #endif

                    return dwIndex + dwOffset;
                }

                pUsage++;
                dwOffset += 32;
            }

            return 0xFFFFFFFF;
        }

        __forceinline DWORD __fastcall GetFreeBlockIndex(PoolBlock* pPoolBlock)
        {
            DWORD dwIndex;
            DWORD dwUsage = *pPoolBlock->pFreeUsage;
            if(dwUsage == 0xFFFFFFFF)
                return GetNextFreeBlockIndex(pPoolBlock);

            if(_BitScanForward(&dwIndex,~dwUsage))
                return dwIndex + pPoolBlock->dwFreeIndex;

            return 0xFFFFFFFF;
        }

原谅标签,这是一些#if /#endif VS代码。 ofc此代码仅适用于DWORDS 你可以block_size & 3查找是否有奇数字节,将这些奇数字节复制到DWORD并扫描DWORD,然后剪切大于(block_size & 3) << 3的任何结果