【问题标题】:Are If Thens faster than multiplication and assignment?If Thens 比乘法和赋值更快吗?
【发布时间】:2011-04-30 18:23:53
【问题描述】:

我有一个简单的问题,假设我有以下代码,例如,它以类似的方式重复了 10 次。

if blah then
    number = number + 2^n
end if

评估是否会更快:

number = number + blah*2^n?

这也带来了一个问题,你能不能将一个布尔值乘以一个整数(虽然我不确定从 2^n 返回的类型,它是整数还是无符号等等)? (我在 Ada 工作,但让我们试着概括一下吧?)

编辑:对不起,我应该澄清一下,我正在查看 2 的 n 次方,并且我将 c 放在那里,因为如果我在 c 中遇到这个问题并且我认为那里有我对自己的学习感兴趣的未来这些板上的 c 程序员比 Ada 多(我假设你知道这意味着什么),但是我目前的问题是 Ada 语言,但问题应该是与语言无关的(我希望)。

【问题讨论】:

  • 插入符号 ^ 运算符在 C 中表示 XOR,所以请记住这一点。
  • 小心。由于 C 没有内置布尔类型,因此无法保证 blah 等于 1 或 0。某些返回 true 或 false 的函数可能会选择返回 1 以外的值来代替 true。
  • @Brian 谢谢,我没有意识到布尔值并不总是意味着 0 和 1,这可能是一个很难学习的课程。
  • 没有几个 Ada 程序员在 StackOverflow 上闲逛,我们几乎都设置了 RSS 提要(或类似的东西)来监视“Ada”标签,所以不用担心关于 Ada 程序员没有注意到 Ada 问题 :-)
  • @Marc C - 这很漂亮。我只是手动检查。他是对的,尽管这确实是一个与语言无关的问题。 Ada 补充的唯一问题是它的编译器有更多信息,可以更好地优化工作。所以对于 C 来说是正确的(不要像这样进行微优化)对于 Ada 来说更是如此。

标签: c optimization ada branch-prediction


【解决方案1】:

此类问题没有通用答案,这在很大程度上取决于您的编译器和 CPU。现代 CPU 有条件移动指令,所以一切皆有可能。

了解这里的唯一方法是检查生成的汇编器(通常-S 作为编译器选项)并进行测量。

【讨论】:

    【解决方案2】:

    如果我们在谈论 C 并且 blah 不在你的控制范围内,那么就这样做:

    if(blah) 数字 += (1

    C 中确实没有布尔值,也不需要,false 为零,true 不为零,所以你不能假设不是 0 是 1,这是你的解决方案所需要的,你也不能假设设置 blah 中的任何特定位,例如:

    数字 += (blah&1)

    不一定会起作用,因为 0x2 或 0x4 或任何非零且位零清除的东西都被认为是真的。通常,您会发现 0xFFF...FFFF(减一或全一)被用作真值,但您不能依赖典型值。

    现在,如果您完全控制 blah 中的值,并将其严格控制为 0 表示假,1 表示真,那么您可以按照您的要求进行操作:

    数字+=废话

    并避免出现分支、额外缓存行填充等的可能性。

    回到通用案例,采用这个通用解决方案:

    unsigned int fun (int blah, unsigned int n, unsigned int number ) { if(blah) 数字 += (1

    并针对两个最流行/最常用的平台进行编译:

    测试 %edi, %edi movl %edx, %eax 杰.L2 移动 $1, %edx 移动 %esi, %ecx 销售 %cl, %edx 添加 %edx, %eax .L2:

    以上使用条件分支。

    下面的使用条件执行,没有分支,没有管道刷新,是确定性的。

    cmp r0,#0 移动 r3,#1 添加 r2,r2,r3,asl r1 移动 r0,r2 bx lr

    本可以通过重新排列函数调用中的参数来保存 mov r0,r2 指令,但这是学术性的,您通常不会对此进行函数调用。

    编辑:

    按照建议:

    unsigned int fun (int blah, unsigned int n, unsigned int number ) { 数字 += ((blah!=0)&1) 潜艇 r0, r0, #0 移动 r0, #1 添加 r0, r2, r0, asl r1 bx lr

    当然更便宜,而且代码看起来不错,但我不会假设 blah!=0 的结果,即零或编译器定义为 true 的任何内容总是具有 lsbit 集。它不必为编译器设置该位来生成工作代码。也许标准规定了 true 的具体值。通过重新排列函数参数,if(blah) number +=... 也将导致三个单时钟指令并且没有假设。

    EDIT2:

    看看我理解的 C99 标准:

    ==(等于)和!=(不等于) to) 运算符类似于 关系运算符,除了它们的 较低的优先级。每个 如果指定,则运算符产生 1 关系为真,如果为假,则为 0。

    这解释了为什么上述编辑有效,为什么你得到 movne r0,#1 而不是其他随机数。

    发帖人问的是关于 C 的问题,但也指出 ADA 是当前的语言,从语言独立的角度来看,你不应该假设像上面的 C 功能这样的“功能”并使用 if(blah) number = number + (1

    海报的假设也基本正确,如果您可以将 blah 转换为 0 或 1 形式,那么在没有分支的意义上,在数学中使用它会更快。把它变成那种形式而不比分支更昂贵是诀窍。

    【讨论】:

    • number += ((blah!=0)&1)<<n; 呢?
    • blah!=0 的结果要么是 0,要么是 false,要么是不确定的 true。
    • 阅读类似 SO 问题的答案,标准可能会规定中间比较确实返回 1 或 0,如果这是真的并且编译器在任何地方都满足该标准,那么只需执行 number +=(等等!=0)
    • 谢谢,记得为 simon 的贡献 +1。并向前支付(帮助其他人使用stackoverflow)
    【解决方案3】:

    在阿达...

    原来的表述:

    if Blah then
      Number := Number + (2 ** N);
    end if;
    

    另一种通用公式,假设 Blah 是 Boolean 类型并且 Number 和 N 是合适的类型:

    Number := Number + (Boolean'pos(Blah) * (2 ** N));
    

    (对于用户定义的整数或浮点类型的NNumber,可能需要合适的定义和类型转换,这里的关键是Boolean'pos()构造,Ada保证会给你一个0或 1 表示预定义的布尔类型。)

    至于这是否更快,我同意@Cthutu:

    我会保留它的条件。 你不应该担心低级 此时的优化细节。 编写描述你的代码 算法最好并相信你的 编译器。

    【讨论】:

    • 在 pos 部分很好,我没有想到这一点。如果 blah 是一个可预测的值,我可以理解你自己和 cthutu 所说的编译器部分,但由于这是一个来自硬件的离散值,我不确定编译器如何进一步优化它,你会(或Cthutu) 思维扩展?
    • Ada 真的保证 Boolean 类型为 0 和 1 吗? LRM 中对此的唯一评论是 False
    • 是的,对于预定义的布尔值,这是有保证的。是因为定义了'Pos属性,它返回枚举的位置数,即Boolean'Pos(False)为0,Boolean'Pos(True)为1。即使底层表示为 0,除 0 以外,'Pos 属性仍将保留(要获得实际表示,您必须使用 Unchecked_Conversion 实例化来获取它。)
    • 如果编译器生成一个布尔值,它肯定会有适当的 'Pos 行为。但是,如果您使用某种形式的未经检查的转换(例如,从 C 导入)生成“布尔”值,则它可能是无效的,并且大多数赌注都失败了。例如,对于 GCC Ada 编译器,42 在某些情况下可能看起来是假的(因为它的 LSB 是明确的),在其他情况下可能看起来是真的,或者导致 Constraint_Error。与以往一样,如果您是从外部环境导入,请在界面上进行验证。
    • & 西蒙:感谢您的意见。现在重新阅读LRM,这很清楚。我将 'Pos 与内部表示混淆了。有用的新信息。
    【解决方案4】:

    我会保留它的条件。此时您不应该担心低级优化细节。编写最能描述您的算法的代码并相信您的编译器。在某些 CPU 上,乘法速度较慢(例如,每条指令都有条件的 ARM 处理器)。您还可以使用 ?: 在某些编译器下优化得更好的表达式。例如:

    number += (blah ? 2^n : 0);
    

    如果由于某种原因这个小计算是分析后应用程序的瓶颈,那么请担心低级优化。

    【讨论】:

    • 在对嵌入式系统进行代码审查时,我通常会从稍微调整一下是否可以更快一点的角度来看待编写的代码,我不打算进行任何形式的大规模重写,或者几周的时间来调整小事情,但希望是显而易见的小事情,但也许编译器会处理这个问题。虽然我不认为它可以优化它,因为布尔值中的数据在这种情况下是离散的,所以直到运行时才知道。
    • 我真的建议坚持使用布尔检查是否在条件为真时触发逻辑,而不是使用整数/浮点数并将其相乘。当您在 6 个月后回到您的代码时,这对您来说会更加明显。
    • 对优化调整感到非常厌倦。你可能会让你的代码更难阅读,更糟糕的是让它变慢。在优化方面,直觉并不总是你最好的朋友。
    • 基于与运行此代码的函数相关的注释,如果无法轻松阅读代码,我会感到惊讶,但我绝对明白你的意思。我想一种快速查看这是否更快或更慢(即使使用编译器设置)的快速方法是运行类似的“函数”多次进行大量时间测量,这应该在我们的特定系统上告诉我不管是快还是慢。
    • 我试图解释你不应该担心那个级别的优化,我描述的是一种通用方法,而不是任何特定于示例代码的方法。经常分析您的代码,并将其用作您应该将优化工作重点放在哪里的指南,如果您的应用需要它。
    【解决方案5】:

    在 C 语言中,关于 blah*2^n:你有任何理由相信 blah 取值 0 和 1?该语言只承诺 0 FALSE 和(其他所有) TRUE。 C 允许您将“布尔”临时值与另一个数字相乘,但结果未定义,除非 result=0 bool 为假或数字为零。

    在 Ada 中,关于 blah*2^n:该语言没有在布尔类型上定义乘法运算符。因此 blah 不能是布尔值并且不能相乘。

    【讨论】:

    • 我与一位同事交谈,他提到您不能将布尔值与数字相乘。这是有道理的,但我不确定这是否只是 ada 限制,还是大多数语言都不允许。
    • Eric:这个答案具有误导性。 C 中的逻辑/比较运算符的结果是总是 0 或1。这是由标准明确定义的。您可以将其他非零值与条件一起使用,但这与您暗示“真”采用随机非零值完全不同。
    • @R..: 嗯...现在你让我试图记住在哪个环境中我惊讶地发现 true(明显)实现为 -1。我想我记得 !true==false 和 ~true==false 的“双关语”。
    【解决方案6】:

    如果您的语言允许在布尔值和数字之间进行乘法运算,那么可以,这比条件运算要快。条件需要分支,这会使 CPU 的管道无效。此外,如果分支足够大,它甚至会导致指令中的缓存未命中,尽管在您的小示例中不太可能发生这种情况。

    【讨论】:

    • 有趣的是,我正在考虑整个分支的事情。我忘记了流水线(真遗憾,我们在学校已经研究过很多次了,我应该知道的更多)
    【解决方案7】:

    一般而言,尤其是在使用 Ada 时,您不必担心此类微优化问题。编写您的代码,以便读者可以清楚地了解,并且仅在遇到性能问题时才担心性能,并将其跟踪到代码的那部分。

    不同的 CPU 有不同的需求,而且它们可能非常复杂。例如,在这种情况下,哪个更快取决于您的 CPU 的管道设置、当时缓存中的内容以及其分支预测单元的工作方式。你的编译器的一部分工作就是成为这些事情的专家,它会比最好的汇编程序员做得更好。当然比你(或我)更好。

    所以你只需要担心编写好的代码,而让编译器担心如何生成高效的机器代码。

    【讨论】:

      【解决方案8】:

      对于上述问题,C 中确实有简单的表达式可以产生高效的代码。

      如果n 小于int 中的值位数,则2n 次方可以使用<< 运算符计算为1 << n

      如果blah 是一个布尔值,即一个值为01int,则可以编写您的代码片段:

      number += blah << n;
      

      如果blah 是任何可以测试其真值的标量类型if (blah),则表达式会稍微复杂一些:

      number += !!blah << n;
      

      相当于number += (blah != 0) &lt;&lt; n;

      测试仍然存在,但对于现代架构,生成的代码不会有任何跳转,这可以使用Godbolt's compiler explorer 在线验证。

      【讨论】:

      • 很高兴您决定给出这个答案。不久前,我自己几乎给出了相同的答案,但这是一个老问题。不知何故,它一直保持活跃,所以应该有一个最佳答案。
      【解决方案9】:

      在任何一种情况下,您都无法避免分支(内部),所以不要尝试!

      number = number + blah*2^n
      

      总是需要计算完整的表达式,除非编译器足够聪明,可以在 blah 为 0 时停止。如果是,那么如果 blah 为 0,你会得到一个分支。如果不是,你总是会得到一个昂贵的乘。如果 blah 为 false,您还将获得不必要的 add 和 assignment。

      在“if then”语句中,只有当 blah 为真时,该语句才会进行加法和赋值。

      简而言之,在这种情况下,您的问题的答案是“是”。

      【讨论】:

      • 为什么每个人都忽略了这个乘法并不昂贵但实际上是免费的事实? (提示:它是 2 的幂。)
      • 仅仅因为它是 2 的幂并不比不做它更快。
      • 你可以避免它依赖于架构的分支。你无法避免某种条件执行,这是真的,除非知道 blah 不仅仅是一个通用布尔值,而是一个 1 或 0。
      【解决方案10】:

      这段代码显示它们的性能相似,但乘法通常稍快一些。

      @Test
      public void manual_time_trial()
      {
          Date beforeIfElse = new Date();
          if_else_test();
          Date afterIfElse = new Date();
          long ifElseDifference = afterIfElse.getTime() - beforeIfElse.getTime();
          System.out.println("If-Else Diff: " + ifElseDifference);
      
          Date beforeMultiplication = new Date();
          multiplication_test();
          Date afterMultiplication = new Date();
          long multiplicationDifference = afterMultiplication.getTime() - beforeMultiplication.getTime();
          System.out.println("Mult Diff   : " + multiplicationDifference);
      
      }
      
      private static long loopFor = 100000000000L;
      private static short x = 200;
      private static short y = 195;
      private static int z;
      
      private static void if_else_test()
      {
          short diff = (short) (y - x);
          for(long i = 0; i < loopFor; i++)
          {
              if (diff < 0)
              {
                  z = -diff;
              }
              else
              {
                  z = diff;
              }
          }
      }
      
      private static void multiplication_test()
      {
          for(long i = 0; i < loopFor; i++)
          {
              short diff = (short) (y - x);
              z = diff * diff;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-11-13
        • 2016-09-18
        • 2015-09-09
        • 1970-01-01
        • 2011-06-30
        • 1970-01-01
        • 2013-07-26
        相关资源
        最近更新 更多