哪个是C中检查两个整数中至少有一个为零的最有效方法?

时间:2013-09-08 14:07:10

标签: c assembly if-statement performance

我的代码正在进行大量的这些比较操作。我想知道哪个是最有效的。如果我故意选择“错误的”,编译器是否可能会纠正它?

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) // ...

其他想法?

6 个答案:

答案 0 :(得分:2)

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

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

答案 1 :(得分:1)

可以看到哪个变体产生的装配指令更少,但是在更短的时间内看哪个变量实际执行是另一回事。

为了帮助您分析第一个问题,请学习使用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:

如果第一个数字为零,则第一个变量在所涉及的汇编指令数量方面的工作量较少 - 通过避免第二个寄存器移动。如果第一个数字不为零,则第二个变量通过避免第二个比较为零来减少工作量。

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

如果您要求编译器优化此代码,则示例过于简单;编译器在编译时确定 no 比较是必要的,并且只是将该代码压缩为无条件写入字符串的请求。更改代码以操作参数而不是常量是有趣的,并查看优化器如何以不同方式处理这种情况。

变体一:

#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

让我们知道您的发现。

答案 2 :(得分:0)

这对于应用程序的整体性能影响不大(如果有的话,给定现代编译器优化器)。如果你确实必须知道,你应该编写一些代码来测试你的编译器的每个性能。但是,作为最好的猜测,我会说......

if ( !( a && b ) )

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

答案 3 :(得分:0)

如果你想使用一个比较指令找到两个整数中的一个是否为零......

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

如果 a 为零,则向左移动的数量不会改变其值。

如果 b 为零,则表示没有进行换档。

如果 b 为负或非常大,有可能(我懒得检查)存在一些未定义的行为。

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

希望这有帮助。

答案 4 :(得分:0)

效率最高肯定是最明显的,如果效率很高,那么你就是在衡量程序员的时间。

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

但是这个练习证明了程序员优化的缺陷。对于所有int,3个候选者的功能等效。


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

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

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

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

答案 5 :(得分:0)

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

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

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

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

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

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

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

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