【问题标题】:Use of logical AND/OR without conditional/branching使用没有条件/分支的逻辑 AND/OR
【发布时间】:2018-05-04 08:33:59
【问题描述】:

我正在尝试编写一个函数来计算一些位标志,同时避免使用分支或条件:

uint8_t count_descriptors(uint8_t n)
{
    return 
    ((n & 2)   && !(n & 1))   +
    ((n & 4)   && !(n & 1))   +
    ((n & 8)   && !(n & 1))   +
    ((n & 16)  && !(n & 1))   +
    ((n & 32)  && 1       )   +
    ((n & 64)  || (n & 128))  ;
}

bit 0 不直接计数,bit 0 不设置只考虑bit 1-4,bit 5 无条件考虑,bit 6-7 只能计数一次。

但是,我知道布尔值 && 和 ||使用短路评估。这意味着它们的使用会创建一个条件分支,如您在此类示例中所见:if( ptr != nullptr && ptr->predicate()) 保证如果从第一个子表达式对结果进行短路评估,则不会执行第二个子表达式中的代码。

问题的第一部分:我需要做什么吗?由于这些是没有副作用的纯算术运算,编译器会创建条件分支吗?

第二部分:我知道按位布尔运算符不会短路评估,但唯一的问题是位不对齐。屏蔽第 n 位的结果是 2^n 或零。

使 (n & 16) 等表达式计算为 1 或 0 的最佳方法是什么?

【问题讨论】:

  • 好的。所以你真的很聪明,可以用比特做聪明的事情。没有其他人会知道您的版本实际上应该做什么(即使您确实注释了它,注释最终也会偏离代码。他们总是这样做。)为什么不把它写清楚可维护的开始方式?
  • Apple LLVM 8.1.0 将例程优化为没有任何分支的代码(针对其默认的 macOS 目标)。但是,这不是您可以依赖的行为。不同的编译器会做不同的事情,甚至一个编译器在不同的情况下也可能会做不同的事情。大多数情况下,您应该编写清晰的代码,并避免让人们或编译器不清楚正在发生的事情。
  • 第二部分的答案:(n & x) << x 将为您提供 x = 2^k 的 0 或 1
  • 延伸 Eric 的观点:启用优化的现代编译器非常聪明,可以为此类函数生成最佳代码。避免分支也不一定更快。我只是写了这个函数,这样人们就可以不费吹灰之力地理解它,并让编译器发挥它的魔力。

标签: c++ bit-manipulation bitwise-operators boolean-logic short-circuiting


【解决方案1】:

使 (n & 16) 等表达式求值的最佳方法是什么 为 1 还是 0?

通过将其右移所需的位数:(n>>4)&1(n&16)>>4

我可能会为所有 256 个值或至少为 4 个值的组使用查找表。

nbits[16]={0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
//count bits 1..4 iff bit 0 is 0, bit 5 always, and bit 6 or 7
return (!(n&1) * nbits[(n>>1)&0xF]) + ((n>>5)&1) + (((n>>6)|(n>>7))&1)

【讨论】:

    【解决方案2】:

    我认为将 (n & 16) 转换为 0 或 1 的最简洁方法是使用 int(bool(n & 16))。如果您在算术表达式(如bool(n & 2) + bool(n & 4))中使用它们,则可以删除对int 的强制转换。
    对于计数位集的功能,我建议使用 popcount 内在函数,在 gcc 上以 __builtin_popcount 提供,在 MSVC 上以 __popcnt 提供。下面是我对你描述的函数的理解,改为使用popcount。

    f(n & 1)
    {
      //clear out first 4 bits and anything > 255
      n &= 0xF0;
    }
    else
    {
      //clear out anything > 255 and bottom bit is already clear
      n &= 0xFF;
    }
    
    return __builtin_popcount(n); //intrinsic function to count the set bits in a number
    

    这与您编写的函数不太匹配,但希望您从这里得到想法。

    【讨论】:

      【解决方案3】:

      我假设“第 6-7 位只能计算一次”是指其中只有一个被计算

      在这种情况下,这样的事情应该可以工作

      uint8_t count_descriptors(uint8_t n)
      {
          uint8_t retVar;
      
          retVar = (n&1)*(n&2 >> 1) + 
                   (n&1)*(n&4 >> 2) + 
                   (n&1)*(n&8 >> 3) +
                   (n&1)*(n&16 >> 4) + 
                   (n&32 >> 5) + 
                   (int)((n&64 >> 6) + (n&128 >> 7) >= 1)
      
          return retVar;
      
      }
      

      【讨论】:

      • 我很好奇,对于像 (n&1)*(n&4 >> 2) 这样的操作,布尔比较只在第 0 位进行,为什么乘法比按位与更可取?
      • @stands2reason 它可能会用& 而不是* 做同样的事情,但我发现这样更容易阅读。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-08-05
      • 2011-03-10
      • 2018-10-29
      • 1970-01-01
      相关资源
      最近更新 更多