【问题标题】:Performance wise, how fast are Bitwise Operators vs. Normal Modulus?性能方面,按位运算符与普通模数的速度有多快?
【发布时间】:2013-12-05 06:52:58
【问题描述】:

在正常流程或条件语句(如for、if 等)中使用按位运算会提高整体性能吗?在可能的情况下使用它们会更好吗?例如:

if(i++ & 1) {

}

对比

if(i % 2) {

}

【问题讨论】:

  • 这是一个人人都知道的技巧,包括编译器。在其他情况下,使用按位运算会有所帮助。这取决于替代方案是什么。
  • 为什么第一个条件包含i的后缀增量而第二个不包含?当两段代码的性能不同时,比较两段代码的性能是毫无意义的。
  • 涉及至少一个编译时常数的整数除法/模数将由编译器优化为一系列乘法和位移。
  • 我个人喜欢 (i & 0xFF) vs (i % 0x100)。如果我说“你的出生年份的正确两​​位数是多少”,以十为基数,你不会认为,“哦,电话接线员想知道我的出生年份模 100 ......”你只会吐出数字忘记它。为什么人们认为 mod 更容易阅读?每次看到它时,我都必须检查变量是否已签名。这并不容易阅读。使用 (&) 根本不是过早的优化。

标签: c++ bit-manipulation bitwise-operators


【解决方案1】:

除非您使用的是古老的编译器,否则它已经可以自行处理这种级别的转换。也就是说,现代编译器可以并且将使用按位的AND 指令实现i % 2,前提是在目标CPU 上这样做是有意义的(公平地说,它通常会这样做)。

换句话说,不要期望看到它们之间的任何性能差异,至少对于具有相当称职的优化器的相当现代的编译器而言。在这种情况下,“合理”也有一个相当广泛的定义——即使是相当多的几十年前的编译器也可以毫无困难地处理这种微优化。

【讨论】:

  • 如果所有值都是有符号整数类型,则表达式 a=b & 1; 在我所知道的每个符合标准的实现上都会比 a=b % 2; 计算得更快,因为后一个表达式等效于 a= b < 0 ? -(b & 1) : b & 1;。如果只对结果进行测试为零,优化器可能能够识别出b<0 和b>=0 的情况是等价的,但我不会特别期望这种优化。
  • @supercat 这里有一种避免分支的方法,只需在掩码中添加符号位:a = b & 0b1000....0001
  • @potato:这不会在二进制补码机器上产生正确的结果。有无分支的方法可以实现结果,编译器实际上会生成它们,但它们比b & 1 的代码更复杂。
【解决方案2】:

TL;DR 首先编写语义,然后优化测量的热点。

在 CPU 级别,整数模数和除法是最慢的操作之一。但是您不是在 CPU 级别编写,而是在 C++ 中编写,您的编译器将其转换为中间表示,最后根据您正在编译的 CPU 模型将其转换为汇编。

在这个过程中,编译器会应用Peephole Optimizations,其中图形Strength Reduction Optimizations如(courtesy of Wikipedia):

Original Calculation  Replacement Calculation
y = x / 8             y = x >> 3
y = x * 64            y = x << 6
y = x * 2             y = x << 1
y = x * 15            y = (x << 4) - x

最后一个例子可能是最有趣的一个。虽然乘以或除以 2 的幂很容易(手动)转换为位移操作,但编译器通常被教导执行更智能的转换,您可能会自己考虑并且不容易识别(在非常至少,我个人不会立即认识到(x &lt;&lt; 4) - x 意味着x * 15)。

【讨论】:

    【解决方案3】:

    这显然与 CPU 相关,但您可以预期按位运算永远不会花费更多的 CPU 周期,而且通常会花费更少的 CPU 周期来完成。一般来说,整数/ 和% 是著名的慢,因为CPU 指令去。也就是说,现代 CPU 流水线具有较早完成的特定指令并不意味着您的程序一定运行得更快。

    最佳做法是编写易于理解、可维护且能表达其实现逻辑的代码。这种微优化很少会产生明显的差异,因此只有在分析表明存在关键瓶颈并且证明会产生显着差异时才应使用它。此外,如果在某些特定平台上确实产生了显着差异,那么您的编译器优化器可能已经在替换按位运算,因为它可以看到它是等效的(这通常要求您是 /-ing 或 %-ing一个常数)。

    不管它值多少钱,特别是在 x86 指令上 - 并且 当除数是运行时变量值时 所以不能简单地优化为例如移位或按位与运算,/ 和 % 运算在 CPU 周期中所花费的时间可以查到 here。此处列出的 x86 兼容芯片太多,但作为最新 CPU 的任意示例 - 如果我们采用 Agner 的“Sunny Cove (Ice Lake)”(即第 10 代 Intel Core)数据,DIV 和 IDIV 指令之间存在延迟12 和 19 个周期,而按位与有 1 个周期。在许多较旧的 CPU 上,DIV 可能会差 40-60 倍。

    【讨论】:

    • +1 强调可读性;通常优化出现在最后阶段,我们最多可以尽量不让我们的代码变得非常陌生。
    【解决方案4】:

    默认情况下,您应该使用最能表达您预期含义的操作,因为您应该针对可读代码进行优化。 (今天大多数时候最稀缺的资源是人类程序员。)

    因此,如果您提取位,请使用&amp;,如果您测试可分性(即该值是偶数还是奇数),则使用%。

    对于无符号值,两种运算的效果完全相同,您的编译器应该足够聪明,可以用相应的位运算代替除法。如果你担心你可以检查它生成的汇编代码。

    不幸的是,整数除法在有符号值上略微不规则,因为它向零舍入,并且 % 的结果根据第一个操作数改变符号。另一方面,位运算总是向下舍入。所以编译器不能仅仅用简单的位操作来代替除法。相反,它可以调用整数除法例程,或者用具有额外逻辑的位操作替换它来处理不规则性。这可能取决于优化级别以及哪些操作数是常量。

    这种零不规则性甚至可能是一件坏事,因为它是一种非线性。例如,我最近有一个案例,我们对来自 ADC 的有符号值使用除法,这在 ARM Cortex M0 上必须非常快。在这种情况下,最好将其替换为右移,以提高性能并消除非线性。

    【讨论】:

    • 我想知道有多少现有代码依赖于/ 和% 的有符号数行为的哪些方面?我所知道的% 的负数结果的唯一用途是当% 产生负数时想要从/ 的结果中减去一个。在几十年的编程中,我想我曾经遇到过这样一种情况,即向零截断的对称性实际上是有用的,任何周期性行为都会更好的情况。特别具有讽刺意味的是,ANSI 开始强制截断为零的时间大约是...
    • ...在大多数常见情况下,它可能不再是更快的行为(因为较新的处理器可以快速执行长乘法,并且可以使用长乘法比截断除法更有效地评估常数的下限除法)。
    • 很遗憾,虽然这是唯一正确的答案,但它却出现在页面的最底部。太糟糕了,我的单 +1 并不能解决这个问题。 “始终相信你的编译器会做最优化的事情”的口头禅并非完全错误,但如果盲目遵循它可能会产生很大的误导。
    【解决方案5】:

    C 运算符不能在“性能”方面进行有意义的比较。在语言级别上没有“更快”或“更慢”的运算符。只能分析生成的编译机器代码的性能。在您的具体示例中,生成的机器代码通常完全相同(如果我们忽略第一个条件由于某种原因包括后缀增量的事实),这意味着性能不会有任何差异。

    【讨论】:

      【解决方案6】:

      这是编译器 (GCC 4.6) 为这两个选项生成的优化 -O3 代码:

      int i = 34567;
      int opt1 = i++ & 1;
      int opt2 = i % 2;
      

      为 opt1 生成的代码:

      l     %r1,520(%r11)
      nilf  %r1,1
      st    %r1,516(%r11)
      asi   520(%r11),1
      

      为 opt2 生成的代码:

      l     %r1,520(%r11)
      nilf  %r1,2147483649
      ltr   %r1,%r1
      jhe  .L14
      ahi   %r1,-1
      oilf  %r1,4294967294
      ahi   %r1,1
      .L14: st %r1,512(%r11)
      

      所以还有 4 条额外的指令……这对于 prod 环境来说毫无意义。这将是一个过早的优化,只会引入复杂性

      【讨论】:

      • 这是什么架构?你是否在编译时进行了优化?
      • 这是在 IBM z/OS 上使用 GCC 4.6 编译器生成的。 -O3 优化级别
      • 我不知道这些说明是什么。它们不像 x86 甚至 PowerPC 那样普遍,而且助记符甚至不可读。此外,计算指令的数量并不是一个好的指标,因为具有更多指令的 sn-p 可能比较短的运行得更快。重要的是指令的重量,例如div 的吞吐量和延迟与班次相比如何。这不是一个好的答案,缺乏解释,也没有向读者提供任何信息
      【解决方案7】:

      总是这些关于编译器有多聪明的答案,人们甚至不应该考虑他们的代码的性能,他们不应该敢于质疑她的聪明编译器,那个bla bla bla......结果是人们被说服了每次他们使用% [SOME POWER OF TWO] 时,编译器都会神奇地将他们的代码转换为&amp; ([SOME POWER OF TWO] - 1)。这是不正确的。如果共享库有这个功能:

      int modulus (int a, int b) {
          return a % b;
      }
      

      一个程序启动modulus(135, 16),在编译后的代码中没有任何地方会出现任何位魔法的痕迹。原因?编译器很聪明,但是编译库的时候没有水晶球。它看到了一个通用模数计算,没有任何关于仅涉及 2 的幂的信息,它就这样离开了。

      但是你可以知道是否只有2的幂会被传递给函数。如果是这种情况,优化代码的唯一方法是将函数重写为

      unsigned int modulus_2 (unsigned int a, unsigned int b) {
          return a & (b - 1);
      }
      

      编译器无法为您做到这一点。

      【讨论】:

        【解决方案8】:

        按位运算要快得多。 这就是编译器将为您使用按位运算的原因。 实际上,我认为实现它会更快:

        ~i & 1
        

        同样,如果您查看编译器生成的汇编代码,您可能会看到 x ^= x 而不是 x=0。但是(我希望)你不会在你的 C++ 代码中使用它。

        总之,做你自己,以及任何需要维护你的代码的人,帮个忙。使您的代码可读,并让编译器进行这些微优化。它会做得更好。

        【讨论】:

        • !比 & 具有更高的优先级,并且布尔值是等于 0 或 1 的数字,因此 (! i &amp; 1) 与 !i 相同。
        • (!i) 将反转所有位。机智的 &1 将检查最低有效位是否打开。因此,如果 i 是偶数,则其租用有效位将为 0,(!i) 的最低有效位将为 1,因此按位执行 (&1) 将为真。
        • 你想到的是~,而不是!。
        • @yosim: !i 是布尔值,返回 0 或 1。它不是按位计算的。你可能在想~。
        猜你喜欢
        • 1970-01-01
        • 2016-04-12
        • 1970-01-01
        • 1970-01-01
        • 2020-06-03
        • 2018-05-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多