0

我有做很多这些比较操作的代码。我想知道哪个是最有效的使用。如果我故意选择“错误”的编译器,编译器是否会更正它?

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

其他想法?

4

6 回答 6

2

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

此外,第三种方法是唯一始终有效的方法。如果 a 和 b 为 1<<16,则第一个失败,而您已经知道的第二个不起作用。

于 2013-09-08T14:11:53.143 回答
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:

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

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

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

变体一:

#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

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

于 2013-09-08T14:57:11.177 回答
0

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

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

如果a为零,则将其向左移动任何量都不会改变其值。

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

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

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

希望这可以帮助。

于 2013-09-08T16:56:11.323 回答
0

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

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

但是这个练习证明了程序员优化的一个陷阱。这 3 名候选人在功能上并非对所有人都等效int


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

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

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

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

于 2013-09-08T16:56:20.267 回答
0

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

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

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

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

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

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

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

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

于 2013-09-08T17:18:38.453 回答
0

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

if ( !( a && b ) )

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

于 2013-09-08T14:14:34.217 回答