【问题标题】:Bit mask in CC中的位掩码
【发布时间】:2010-09-23 21:48:17
【问题描述】:

m设置位前面是k未设置位,然后是n未设置位,在C中构造位掩码的最佳方法是什么:

00..0 11..1 00..0
  k     m     n

例如,k=1、m=4、n=3 会产生位掩码:

01111000

【问题讨论】:

  • 对于许多诸如此类的小技巧的答案,一个非常好的在线资源是Bit Twiddling Hacks
  • 通常,位掩码宏定义在 inclusive 位索引上,例如 #define BITS(p,q) ... 其中 p = m + n - 1 和 q = n, p >= q
  • Hacker's Delight 更全面(1.8 千页)而且很棒。
  • @grigy 我真的不明白为什么你需要在这里有k。仅使用 mn 指定要设置的位范围更容易。

标签: c bit-manipulation


【解决方案1】:

虽然最佳答案简单有效,但它们没有为n=0m=31 的情况设置 MSB:

~(~0 << 31) << 0 = 0111 1111 1111 1111 1111 1111 1111 1111‬

((1 << 31)-1) << 0 = 0111 1111 1111 1111 1111 1111 1111 1111‬

我对 32 位无符号字的建议如下所示:

unsigned int create_mask(unsigned int n,unsigned int m) {
  // 0 <= start_bit, end_bit <= 31
  assert(n >=0 && m<=31);
  return (m - n == 31 ? ~0: ((1 << (m-n)+1)-1) << n);
}

这实际上得到了[m,n](闭合区间)范围内的位,因此create_mask(0,0) 将返回第一个位(位0)的掩码,create_mask(4,6) 返回位4 到6 的掩码,即... 00111 0000 .

【讨论】:

    【解决方案2】:

    (仅限)对于在支持 BMI2 的 x86 系统(Intel Haswell 或更新版本、AMD Excavator 或更新版本)上更高效的解决方案感兴趣的人:

    mask = _bzhi_u32(-1,m)<<n;
    

    bzhi 指令将从指定位位置开始的高位归零。 _bzhi_u32 内部函数编译为此指令。测试代码:

    #include <stdio.h>
    #include <x86intrin.h>
    /*  gcc -O3 -Wall -m64 -march=haswell bitmsk_mn.c   */
    
    unsigned int bitmsk(unsigned int m, unsigned int n)
    {
        return _bzhi_u32(-1,m)<<n;
    }
    
    int main() {
        int k = bitmsk(7,13);
        printf("k= %08X\n",k);
        return 0;
    }
    

    输出:

    $./a.out
    k= 000FE000
    

    代码片段_bzhi_u32(-1,m)&lt;&lt;n编译成三条指令

    movl    $-1, %edx
    bzhi    %edi, %edx, %edi
    shlx    %esi, %edi, %eax
    

    @Jonathan Leffler 的代码少一条指令 和@Darius Bacon。 在 Intel Haswell 处理器或更新的处理器上,bzhishlx 都有 1 个周期的延迟和 每个周期的吞吐量为 2。在 AMD Ryzen 上,这两条指令的吞吐量甚至达到了每周期 4 条。

    【讨论】:

      【解决方案3】:

      我喜欢这两种解决方案。这是我想到的另一种方式(可能不是更好)。

      ((~((unsigned int)0) << k) >> (k + n)) << n

      编辑: 我以前的版本中有一个错误(它没有 unsigned int 强制转换)。问题是~0 &gt;&gt; n在前面加了1而不是0。

      是的,这种方法有一个很大的缺点。它假定您知道默认整数类型的位数,或者换句话说,它假定您确实知道 k,而其他解决方案与 k 无关。这使得我的版本不那么便携,或者至少更难移植。 (它还使用了 3 次移位,以及加法和按位否定运算符,这是两个额外的操作。)

      因此,您最好使用其他示例之一。

      这是一个由 Jonathan Leffler 完成的小测试应用程序,用于比较和验证不同解决方案的输出:

      #include <stdio.h>
      #include <limits.h>
      
      enum { ULONG_BITS = (sizeof(unsigned long) * CHAR_BIT) };
      
      static unsigned long set_mask_1(int k, int m, int n)
      {
          return ~(~0 << m) << n;
      }
      
      static unsigned long set_mask_2(int k, int m, int n)
      {
          return ((1 << m) - 1) << n;
      }
      
      static unsigned long set_mask_3(int k, int m, int n)
      {
          return ((~((unsigned long)0) << k) >> (k + n)) << n;
      }
      
      static int test_cases[][2] =
      {
          { 1, 0 },
          { 1, 1 },
          { 1, 2 },
          { 1, 3 },
          { 2, 1 },
          { 2, 2 },
          { 2, 3 },
          { 3, 4 },
          { 3, 5 },
      };
      
      int main(void)
      {
          size_t i;
          for (i = 0; i < 9; i++)
          {
              int m = test_cases[i][0];
              int n = test_cases[i][1];
              int k = ULONG_BITS - (m + n);
              printf("%d/%d/%d = 0x%08lX = 0x%08lX = 0x%08lX\n", k, m, n,
                     set_mask_1(k, m, n),
                     set_mask_2(k, m, n),
                     set_mask_3(k, m, n));
          }
          return 0;
      }
      

      【讨论】:

      • 假设这个答案可以工作,与其他两个相比明显的缺点是存在第三班操作,这使得它更耗时。
      • 另一个问题是它使用了其他两个解决方案可以忽略的参数k(虽然它不使用m,所以它仍然只使用三个参数中的两个)。
      • 确实有一个错误,我现在修复了它并添加了其他解决方案更可取的评论。我还没有完全删除它,也许有人可以从我的错误中吸取教训,丢失你漂亮的测试代码会很遗憾:)。
      • 您应该能够使用“0U”来表示无符号零,或者使用“0UL”来表示无符号长整数,而不是强制转换。我同意保留您的答案 - 以及您所做的编辑。
      • 将此设为宏或内联函数,编译器将在编译时生成常量而不是代码。
      【解决方案4】:

      所以,您要求 m 个设置位,以 k 个复位位为前缀,然后是 n 个复位位?我们可以忽略 k,因为它在很大程度上会受到整数类型选择的限制。

      mask = ((1 << m) - 1) << n;
      

      【讨论】:

      • 它们都有效,但我发现乔纳森的答案更简单、更清晰。大流士的回答对我来说有点太倒退了。
      • Robert,我喜欢位掩码的 ~0 习语,因为它不依赖于 2 的补码,并且在这个意义上更简单,但它确实不太为人所知。尽我所能来改变它!
      • @Darius:如果您使用无符号算术/类型,就像在这些情况下应该使用的那样,2 的补码、1 的补码和符号幅度算术之间的区别是否无关紧要?
      • @Darius,你不应该首先对有符号类型执行按位算术,如果你这样做了,你的解决方案每次都会调用未定义的行为!
      • 是否未定义?我手头没有规范,但我认为它是实现定义的,即允许编译器按照他的意愿进行操作,但他必须始终以相同的方式进行操作。因此,当您知道(编译器的)处理方式时,您可以依赖它。
      【解决方案5】:

      ~(~0

      【讨论】:

      • 这很漂亮。但是,最好对这一行进行注释,以便 -next- 程序员处理它。
      • 如果这被编码为一个函数(@quinmar 的答案中的 set_mask_n 函数),会有一行注释说明函数的作用(并且没有参数“k”),并且用户该函数的名称将作为文档。作为一段代码中的随机表达式,它无疑是坏的!
      • 而且,我会加快(非常缓慢地)添加,如果它在一些代码中显示为未注释的表达式,我的解决方案将同样难以理解。
      • ~(~0 &lt;&lt; m) 位于 Brian Kernighan 和 Dennis Ritchie 的“C 编程语言,第二版”的第 2.9 段“位运算符”中。它也在 Brian W. Kernighan 和 Rob Pike 的“编程实践”的第 7.5 段“空间效率”中。
      • 这种方法不能创建一个包含最长无符号整数类型最高位的掩码,即通常用integer overflow in preprocessor expression之类的警告表示。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-07-21
      • 2023-03-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-20
      • 1970-01-01
      相关资源
      最近更新 更多