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

NPE*_*NPE 11 c floating-point performance x86 gcc

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

// 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;
}
Run Code Online (Sandbox Code Playgroud)

// 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;
}
Run Code Online (Sandbox Code Playgroud)

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

这是为什么?

一个可疑的是i循环的连续迭代之间的数据依赖性v1.但是,我不太清楚完整的解释是什么.

(问题的灵感来自为什么我的python/numpy示例比纯C实现更快?)

编辑:

以下是生成的程序集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
Run Code Online (Sandbox Code Playgroud)

并为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
Run Code Online (Sandbox Code Playgroud)

Ste*_*non 15

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

x <-- sin(x)
x <-- sin(x)
x <-- sin(x)
...
Run Code Online (Sandbox Code Playgroud)

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

v2相比之下,你做到以下几点:

x[0] <-- sin(x[0])
x[1] <-- sin(x[1])
x[2] <-- sin(x[2])
...
Run Code Online (Sandbox Code Playgroud)

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


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

  • @StephenCanon:OSX Libm还在使用我的`sin`实现吗? (2认同)
  • @EricPostpischil:确实如此. (2认同)
  • 更重要的是,FSIN一般会给出错误的结果.它不能做减少参数. (2认同)
  • @R ..:是的,那也是; 我把"使用FSIN"简写为"使用FPREM1 + FSIN"(它不做无限pi减少,但至少做了一些事情). (2认同)