为什么使用第三个变量比加法快?

时间:2018-06-28 19:03:34

标签: c optimization fibonacci

在计算斐波纳契数时,一种常见的方法是将数对(a, b)映射到(b, a + b)。通常可以通过定义第三个变量c并进行交换来完成此操作。但是,我意识到您可以执行以下操作,避免使用第三个整数变量:

b = a + b;  // b2 = a1 + b1
a = b - a;  // a2 = b2 - a1 = b1, Ta-da!

我希望它比使用第三个变量更快,因为在我看来,这种新方法只需要考虑两个内存位置。

所以我写了下面的C程序来比较这些过程。这些模拟斐波那契数的计算,但是请放心,我知道由于大小限制,它们将无法计算正确的值。

(注意:我现在意识到没有必要将n设为long int,但是我会保持原样,因为这是我第一次编译它的方式)

文件:PlusMinus.c

// Using the 'b=a+b;a=b-a;' method.
#include <stdio.h>

int main() {
    long int n = 1000000; // Number of iterations.
    long int a,b;

    a = 0; b = 1;
    while (n--) {
        b = a + b;
        a = b - a;
    }

    printf("%lu\n", a);
}

文件:ThirdVar.c

// Using the third-variable method.
#include <stdio.h>

int main() {
    long int n = 1000000; // Number of iterations.
    long int a,b,c;

    a = 0; b = 1;
    while (n--) {
        c = a;
        a = b;
        b = b + c;
    }

    printf("%lu\n", a);
}

当我在GCC上运行两者(未启用优化)时,我发现速度始终如一:

$ time ./PlusMinus
14197223477820724411

real    0m0.014s
user    0m0.009s
sys     0m0.002s

$ time ./ThirdVar
14197223477820724411

real    0m0.012s
user    0m0.008s
sys     0m0.002s

当我使用-O3在GCC上运行两者时,程序集输出相等。 (我怀疑我在陈述一个在以前的编辑中胜过另一个时有偏见。)

检查每个组件的程序集,我发现PlusMinus.s实际上比ThirdVar.s少一条指令,但运行速度始终较慢。

问题

为什么会出现这种时差?不仅如此,为什么加/减法的速度与我的预期相反?

1 个答案:

答案 0 :(得分:7)

  

为什么会出现这种时差?

使用优化进行编译(在最新版本的gcc和clang下)没有时间差异。例如,用于x86_64的gcc 8.1都可以编译为:

Live at Godbolt

.LC0:
        .string "%lu\n"
main:
        sub     rsp, 8
        mov     eax, 1000000
        mov     esi, 1
        mov     edx, 0
        jmp     .L2
.L3:
        mov     rsi, rcx
.L2:
        lea     rcx, [rdx+rsi]
        mov     rdx, rsi
        sub     rax, 1
        jne     .L3
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        mov     eax, 0
        add     rsp, 8
        ret
  

不仅如此,为什么加/减法的速度与我的预期相反呢?

加法和减法可能比移动要慢。但是,在大多数体系结构(例如x86 CPU)中,它基本上是相同的(1个周期加内存延迟)。所以这不能解释它。

真正的问题很可能是数据之间的依赖关系。参见:

b = a + b;
a = b - a;

要计算第二行,您必须完成计算第一行的值。如果编译器按原样使用表达式(在-O0下就是这种情况),那么CPU将会看到。

但是,在第二个示例中:

c = a;
a = b;
b = b + c;

您可以同时计算新的ab,因为它们彼此不依赖。并且,在现代处理器中,这些操作实际上可以并行计算。或者,换句话说,您不是通过等待上一个结果来“停止”处理器。这称为Instruction-level parallelism