【问题标题】:Which is the most efficient way in C to check that at least one of two integers is zero?C 中检查两个整数中至少一个是否为零的最有效方法是什么?
【发布时间】:2013-09-08 14:07:09
【问题描述】:

我有执行大量此类比较操作的代码。我想知道哪个是最有效的使用。如果我故意选择“错误”的那个,编译器是否可能会更正它?

int a, b;
// Assign a value to a and b.

// Now check whether either is zero.

// The worst?
if (a * b == 0)       // ...
// The best?
if (a & b == 0)       // ...
// The most obvious?
if (a == 0 || b == 0) // ...

其他想法?

【问题讨论】:

  • 你可以查看它生成的汇编/字节码
  • a & b == 0 if true 如果a 中设置的任何位在b 中未设置,则与“ab 中至少一个为零”不同,除非您还有关于ab 的更多信息。同样,如果没有其他信息,a*b 可能会溢出,这是未定义的行为,可能导致条件看起来具有任何值(实际上,对于 32 位整数,a*b 在数学乘积为2^32 的倍数)。
  • Pascal,你是对的,我正要制造一个巨大的无法找到的错误!谢谢
  • @PascalCuoq:由于&的优先级低,表达式实际上被评估为a & (b == 0)(当然仍然不正确)

标签: c assembly if-statement performance


【解决方案1】:

一般来说,如果有一种快速的方法来做一件简单的事情,您可以假设编译器会以这种快速的方式完成它。请记住,编译器输出的是机器语言,而不是 C —— 最快的方法可能无法正确表示为一组 C 结构。

此外,第三种方法是唯一始终有效的方法。如果 a 和 b 为 1

【讨论】:

    【解决方案2】:

    可以查看哪个变体生成的汇编指令更少,但查看哪个变体在更短的时间内实际执行是另一回事。

    为了帮助您分析第一件事,请学习使用 C 编译器的命令行标志来捕获其中间输出。 GCC 是 C 编译器的常见选择。让我们看看它的未优化两个不同程序的汇编代码。

    #include <stdio.h>
    
    void report_either_zero() 
       {
       int a = 1;
       int b = 0;
    
       if (a == 0 || b == 0)
          {
          puts("One of them is zero.");
          }
       }
    

    将该文本保存到 zero-test.c 等文件中,然后运行以下命令:

    gcc -S zero-test.c
    

    GCC 将发出一个名为 zero-test.s 的文件,这是它在生成目标代码时通常会提交给汇编器的汇编代码。

    让我们看一下汇编代码的相关片段。我在 Mac OS X 上使用 gcc 4.2.1 版生成 x86 64 位指令。

    _report_either_zero:
    Leh_func_begin1:
        pushq   %rbp
    Ltmp0:
        movq    %rsp, %rbp
    Ltmp1:
        subq    $32, %rsp
    Ltmp2:
        movl    %edi, -4(%rbp)
        movq    %rsi, -16(%rbp)
        movl    $1, -20(%rbp)     // a = 1
        movl    $0, -24(%rbp)     // b = 0
        movl    -24(%rbp), %eax   // Get ready to compare a.
        cmpl    $0, %eax          // Does zero equal a?
        je  LBB1_2                // If so, go to label LBB1_2.
        movl    -24(%rbp), %eax   // Otherwise, get ready to compare b.
        cmpl    $0, %eax          // Does zero equal b?
        jne LBB1_3                // If not, go to label LBB1_3.
    LBB1_2:
        leaq    L_.str(%rip), %rax
        movq    %rax, %rdi
        callq   _puts             // Otherwise, write the string to standard output.
    LBB1_3:
        addq    $32, %rsp
        popq    %rbp
        ret
    Leh_func_end1:
    

    您可以看到我们将整数值 1 和 0 加载到寄存器中的位置,然后准备将第一个与零进行比较,如果第一个非零,则再次与第二个进行比较。

    现在让我们尝试一种不同的比较方法,看看汇编代码是如何变化的。请注意,这是不同的谓词;这个检查是否两个数字都为零。

    #include <stdio.h>
    
    void report_both_zero() 
       {
       int a = 1;
       int b = 0;
       if (!(a | b))
          {
          puts("Both of them are zero.");
          }
       }
    

    汇编代码有点不同:

    _report_both_zero:
    Leh_func_begin1:
        pushq   %rbp
    Ltmp0:
        movq    %rsp, %rbp
    Ltmp1:
        subq    $16, %rsp
    Ltmp2:
        movl    $1, -4(%rbp)     // a = 1
        movl    $0, -8(%rbp)     // b = 0
        movl    -4(%rbp), %eax   // Get ready to operate on a.
        movl    -8(%rbp), %ecx   // Get ready to operate on b too.
        orl %ecx, %eax           // Combine a and b via bitwise OR.
        cmpl    $0, %eax         // Does zero equal the result?
        jne LBB1_2               // If not, go to label LBB1_2.
        leaq    L_.str(%rip), %rax
        movq    %rax, %rdi
        callq   _puts            // Otherwise, write the string to standard output.
    LBB1_2:
        addq    $16, %rsp
        popq    %rbp
        ret
    Leh_func_end1:
    

    如果第一个数字为零,则第一个变体通过避免第二次寄存器移动,就所涉及的汇编指令的数量而言,所做的工作更少。如果第一个数字不为零,则第二个变体通过避免与零进行第二次比较来减少工作量。

    现在的问题是“移动、移动、按位或比较”是否比“移动、比较、移动、比较”运行得更快。答案可能归结为处理器是否学会预测第一个整数为零的频率,以及它是否始终如一。

    如果你要求编译器优化这段代码,例子太简单了;编译器在编译时决定没有比较是必要的,并且只是将该代码压缩为写入字符串的无条件请求。将代码更改为对参数而不是常量进行操作是很有趣的,并看看优化器如何以不同的方式处理这种情况。

    变体一:

    #include <stdio.h>
    
    void report_either_zero(int a, int b) 
       {
       if (a == 0 || b == 0)
          {
          puts("One of them is zero.");
          }
       }
    

    变体二(同样,不同的谓词):

    #include <stdio.h>
    
    void report_both_zero(int a, int b) 
       {
       if (!(a | b))
          {
          puts("Both of them are zero.");
          }
       }
    

    使用此命令生成优化的汇编代码:

    gcc -O -S zero-test.c
    

    让我们知道您发现了什么。

    【讨论】:

    • 请注意 !(a | b) 是错误的 - 你真的运行了代码吗?
    • 你是对的。我确实运行了第一个示例;当我到达第二个变体时,我专注于生成代码的差异,而不是它的行为是否与第一个变体完全相同。我写答案的目的不是为作者的直接问题提供孤立的答案,而是让他有信心自己调查此事。我编辑了我的答案以澄清这些是不同的谓词。
    【解决方案3】:

    这可能不会对您应用的整体性能产生太大影响(如果有的话,考虑到现代编译器优化器)。如果你真的必须知道,你应该编写一些代码来测试每个你的编译器的性能。但是,作为一个最佳猜测,我会说......

    if ( !( a && b ) )
    

    如果第一个恰好是 0,这将短路。

    【讨论】:

    • 快捷方式并不总是对性能有好处,因为它引入了一个可能被错误预测的分支。
    【解决方案4】:

    如果您想使用一个比较指令来查找两个整数之一是否为零...

    if ((a << b) == a)
    

    如果 a 为零,那么无论向左移动多少都不会改变它的值。

    如果 b 为零,则不执行移位。

    有可能(我懒得检查)有一些未定义的行为应该 b 为负数或非常大。

    但是,由于不直观,强烈建议将其实现为宏(带有适当的注释)。

    希望这会有所帮助。

    【讨论】:

    • b &gt; size-in-bits(a) 依赖时,这种转变究竟做了什么,它在大多数平台上做错了事。典型的行为是在 shiftcount 中取一些低位,比如 5(x86,32 位),而忽略其余的。所以设置b = 1024 会产生误报。
    【解决方案5】:

    效率当然是最明显的,如果用效率来衡量程序员的时间。

    如果通过使用处理器的时间来衡量效率,则分析您的候选解决方案是最好的答案 - 对于您分析的目标机器。

    但是这个练习展示了程序员优化的一个陷阱。这 3 位候选者在功能上对于所有 int 并不相同。


    如果您是功能等效的替代品...
    我认为最后一位候选人和第四位候选人值得比较。

    if ((a == 0) || (b == 0))
    if ((a == 0) |  (b == 0))
    

    由于编译器、优化和 CPU 分支预测的变化,人们应该分析而不是自以为是来确定相对性能。 OTOH,一个好的优化编译器可能会为您提供相同的代码。

    我推荐最容易维护的代码。

    【讨论】:

      【解决方案6】:

      没有“在 C 中最有效的方法”,如果“效率”是指编译代码的效率。

      首先,即使我们假设编译器将 C 语言运算符翻译成它们“显而易见的”机器对应物(即 C 乘法到机器乘法等),每种方法的效率也会因硬件平台而异。即使我们将我们的考虑限制在非常特定的硬件平台上的非常特定的指令序列,它仍然可以在不同的周围环境中表现出不同的性能,例如,取决于整个事物与分支预测启发式的一致性程度。给定 CPU。

      其次,现代 C 编译器很少将 C 运算符翻译成它们“显而易见的”机器对应物。通常,机器代码中使用的指令与 C 代码几乎没有共同之处。在 C 级别执行检查的许多“完全不同”的方法实际上可能会被智能编译器翻译成相同的机器指令序列。同时,当周围的上下文不同时,相同的 C 代码可能会被翻译成不同序列的机器指令。

      换句话说,您的问题没有有意义的答案,除非您真的真的将它本地化到特定的硬件平台、特定的编译器版本和特定的编译设置集。这将使它过于本地化而无用。

      这通常意味着在一般情况下,最好的方法是编写最易读的代码。做吧

      if (a == 0 || b == 0)
      

      代码的可读性不仅会帮助人类读者理解它,还会增加编译器正确解释您的意图并生成最佳代码的可能性。

      但如果你真的需要从你的性能关键代码中挤出最后一个 CPU 周期,你必须尝试不同的版本并手动比较它们的相对效率。

      【讨论】:

        猜你喜欢
        • 2023-02-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-09-22
        • 1970-01-01
        • 2011-03-05
        • 1970-01-01
        • 2017-09-15
        相关资源
        最近更新 更多