【问题标题】:Find first unset bit in buffer (optimization)在缓冲区中查找第一个未设置位(优化)
【发布时间】:2010-07-30 16:21:48
【问题描述】:

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

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

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

更新0

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

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;
}

【问题讨论】:

  • 数组是否可能包含大量未设置位或大量设置位? IE。最常见的情况是设置所有位、未设置位、设置大多数位、未设置大多数位等。
  • 哈哈,不,这不是作业。常见的情况将倾向于大部分未设置:设置大的连续块,这里和那里有不同大小的孔(大多数较小)。缓冲区是文件系统中块分配的位图。
  • 另外,目标系统是什么字节序(如果这很重要)? IE。如果前 8 个字节是 128,255,255,255,255,255,255,255,那么它的“位偏移”是多少? 0 或 7 还是完全不同的?
  • 由于它们大部分未设置,因此任何假设大部分已设置的优化都是无用的,因为在找到未设置位之前您似乎不会循环太远。我错过了什么吗?
  • @James Black,很怀疑,但这就是我问题的重点。这些天我不在 C 或汇编中工作,但我想最常见的情况会决定哪些优化是有意义的,哪些没有。 “大部分未设置”可能会排除查看 32 或 64 位值(基于 CPU)的初始循环,因为跳过 4 或 8 个字节的“全部设置”位将比查看每个单独的字节快得多.我只是认为这样一个问题的答案很重要。

标签: c performance algorithm optimization bit-manipulation


【解决方案1】:

x-86 assembly language,

REPE SCAS 0xFFFFFFFF

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

您别无选择,只能检查第一个未设置位之前的每个位,因此这取决于您能多快完成此操作。一次比较 32 位是一个好的开始,一旦您了解哪个 WORD 包含第一个未设置位,您可以使用移位/查找表的组合来定位该字中的第一个未设置位。

【讨论】:

  • 这看起来很有希望。 GCC 在更高级别的优化中生成这个的机会是什么?我试试看。
  • 这是获得 repe scasd 的最佳机会,使用普通 C:wchar_t c[2] = { -1, 0 }; size_t cnt = wcsspn(buf, c) * sizeof(wchar_t);
  • 请注意,Linux 人员放弃了使用 repe scas 的例程,转而使用纯 C: LKML 线程:lkml.indiana.edu/hypermail/linux/kernel/0803.1/0344.html
  • @R..:聪明,在库函数中使用 32bit wchar_t,很好观察。
  • 是的,很遗憾,这取决于wcsspn 针对单字符第二个参数的情况进行了优化;我不确定这有多普遍。我之所以想到它,是因为我自己的实现有这种优化(但不是 asm;它是纯 C)。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    经常被忽视,strings.h(是的,那个标准头文件)包含一堆函数:ffsffsl 等等,请参阅here 了解更多信息。至少在 gcc 和 x86 上,这会编译为相应的 one-cycle 指令,例如 BSFL。

    因此我建议:

    1. 在数组末尾添加一个标记 0xFFFF
    2. 将 bit_count 除以 4(这样您就可以迭代 4 字节块而不是字节)
    3. 使用 while 循环查找第一个设置位的块

    例如:

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

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

    尽管您应该对此持保留态度,因为我并不声称自己是组装专家...对于每 32 位,您基本上会使用不超过 3 个周期(一个增量,一个比较,一个 BSFL指令),想象一下你可以使用长版本的函数做得更好。

    【讨论】:

    • 您的代码似乎有点不对劲,但很高兴知道 C 库中存在此代码。我将发布使用中的更新。
    • 我在数组末尾添加了一个哨兵,所以我只需要在while循环中进行一次测试:我只需要测试“position = ffsl(buf)”是否不为零(如果缓冲区中没有设置位,这就是 ffsl 将返回的内容)。此外,如果没有哨兵,我还必须确保自己不会越界。代码中确实存在明显的错误:我没有增加缓冲区的地址。实际上,您应该摆脱“光标”并将“光标”的所有实例替换为“buf”。 (或者至少是这样的想法。)
    【解决方案4】:

    Linux 具有我想象的高度优化的实现,名为“find_first_zero_bit”。

    【讨论】:

    【解决方案5】:

    不使用任何汇编语言,但使用 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
    }
    

    【讨论】:

    • 您能解释一下您要避免哪些别名问题吗?
    • 我可能记错了规则。我有编译器反对将 char* 转换为 long* 然后取消引用转换后的指针,但刚才 gcc 似乎并不介意。
    • 我最终实现了类似的东西。
    【解决方案6】:

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

    因为它可以是任意长度,所以你不能只是将它变成一个数字然后以这种方式找到值,因为它可能比双精度数的大小更大。

    【讨论】:

      【解决方案7】:

      我假设你的缓冲区是对齐的,例如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
          ...
      

      剩下的二分搜索由你来填写。

      【讨论】:

        【解决方案8】:

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

        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 ​​称为字节的最低有效位。无论您选择哪种风格,请务必保持一致。

        希望这会有所帮助。

        【讨论】:

          【解决方案9】:

          使用与 Microsoft 的 _BitScanReverse 等效的 gcc 内置功能,我使用类似的东西来为我的内存系统找到第一个空闲位(代表块使用):

                  __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 &amp; 3 来查找是否有任何奇数字节,将这些奇数字节复制到一个 DWORD 并扫描 DWORD,然后剪切任何大于 (block_size &amp; 3) &lt;&lt; 3 的结果

          【讨论】:

          • 如果您强制代码内联,__fastcall 的意义何在? :P 众所周知,fastcall 实际上并不快
          • 它仅用于调试目的(也就是在调试模式下禁用内联),我只是更喜欢在 ollydbg 中运行 __fastcall 函数,而不是 __stdcall(不要问,只是我在我在 MSVC 中写内联...)
          猜你喜欢
          • 2018-04-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-10-22
          • 2015-06-17
          • 1970-01-01
          • 2020-02-14
          • 1970-01-01
          相关资源
          最近更新 更多