涉及sin()的两个非常相似的函数表现出截然不同的性能 - 为什么?

时间:2013-01-22 21:49:41

标签: c performance gcc floating-point x86

考虑以下两个以两种不同方式执行相同计算的程序:

// v1.c
#include <stdio.h>
#include <math.h>
int main(void) {
   int i, j;
   int nbr_values = 8192;
   int n_iter = 100000;
   float x;
   for (j = 0; j < nbr_values; j++) {
      x = 1;
      for (i = 0; i < n_iter; i++)
         x = sin(x);
   }
   printf("%f\n", x);
   return 0;
}

// v2.c
#include <stdio.h>
#include <math.h>
int main(void) {
   int i, j;
   int nbr_values = 8192;
   int n_iter = 100000;
   float x[nbr_values];
   for (i = 0; i < nbr_values; ++i) {
      x[i] = 1;
   }
   for (i = 0; i < n_iter; i++) {
      for (j = 0; j < nbr_values; ++j) {
         x[j] = sin(x[j]);
      }
   }
   printf("%f\n", x[0]);
   return 0;
}

当我使用带有-O3 -ffast-math的gcc 4.7.2编译它们并在Sandy Bridge框中运行时,第二个程序的速度是第一个程序的两倍。

为什么?

一个嫌疑人是iv1循环的连续迭代之间的数据依赖性。但是,我不太明白完整的解释是什么。

(问题受Why is my python/numpy example faster than pure C implementation?启发)

修改

以下是v1生成的程序集:

        movl    $8192, %ebp
        pushq   %rbx
LCFI1:
        subq    $8, %rsp
LCFI2:
        .align 4
L2:
        movl    $100000, %ebx
        movss   LC0(%rip), %xmm0
        jmp     L5
        .align 4
L3:
        call    _sinf
L5:
        subl    $1, %ebx
        jne     L3
        subl    $1, %ebp
        .p2align 4,,2
        jne     L2

v2

        movl    $100000, %r14d
        .align 4
L8:
        xorl    %ebx, %ebx
        .align 4
L9:
        movss   (%r12,%rbx), %xmm0
        call    _sinf
        movss   %xmm0, (%r12,%rbx)
        addq    $4, %rbx
        cmpq    $32768, %rbx
        jne     L9
        subl    $1, %r14d
        jne     L8

2 个答案:

答案 0 :(得分:15)

一起忽略循环结构,只考虑sin的调用顺序。 v1执行以下操作:

x <-- sin(x)
x <-- sin(x)
x <-- sin(x)
...

也就是说,sin( )的每次计算都不能开始,直到前一次调用的结果可用为止;它必须等待以前的整个计算。这意味着,对于sin的N次调用,所需的总时间是单次sin评估的延迟时间的819200000倍。

相比之下,在v2中,您可以执行以下操作:

x[0] <-- sin(x[0])
x[1] <-- sin(x[1])
x[2] <-- sin(x[2])
...

请注意,对sin的每次通话都不依赖于之前的通话。实际上,对sin的调用都是独立的,只要必要的寄存器和ALU资源可用,处理器就可以在每个调用上开始(无需等待先前的计算完成)。因此,所需的时间是sin函数的吞吐量的函数,而不是延迟,因此v2可以在相当短的时间内完成。


我还应该注意到DeadMG是正确的,v1v2在形式上是等价的,在完美的世界中,编译器会将它们优化为100000 sin的单个链。评估(或简单地在编译时评估结果)。可悲的是,我们生活在一个不完美的世界。

答案 1 :(得分:0)

在第一个例子中,它运行100000个sin循环,8192次。

在第二个例子中,它运行了8192个sin循环,100000次。

除此之外,以不同方式存储结果,我看不出任何差异。

然而,有意义的是,在第二种情况下,每个循环的输入都在改变。所以我怀疑发生的事情是,在循环中的某些时间,sin值变得更容易计算。这可以产生很大的不同。计算sin并不是完全无关紧要的,而是一系列计算循环直到退出条件被击中。

相关问题