【问题标题】:Two very similar functions involving sin() exhibit vastly different performance -- why?涉及 sin() 的两个非常相似的函数表现出截然不同的性能——为什么?
【发布时间】:2013-01-06 06:41:07
【问题描述】:

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

// 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 机器上运行时,第二个程序的速度是第一个程序的两倍。

这是为什么呢?

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

(受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

【问题讨论】:

  • 尝试使用double 而不是float
  • @BasileStarynkevitch: double 没有区别。
  • 在第二个代码中,可能是因为所有 j >=1 的所有 x[j] = sin(x[j]) 在初始 sin-calc 之后将与 x[0] 相同?,因此整个内部循环可以被丢弃并用一个单一的 sin-calc 和一个填充代替? (只是在这里猜测)。第一个循环在每次内循环迭代中计算一个与值相关的 sin,第二个 sn-p 没有这样的限制。
  • @NPE 我认为数据依赖性可能是完整的解释。是什么让你认为还有另一个原因?由于第二个版本中的数据“独立性”,处理器可以在计算前一个值的同时预加载下一个计算值,等等。我不是 CPU 架构专家,但由于今天的处理器管道很长,我会考虑这实际上解释了这种性能差异,这似乎是合理的。
  • @NPE 我相信 Stephen Canon 本人 x86 架构方面的专家。

标签: c performance gcc floating-point x86


【解决方案1】:

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

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

sin( )的每一次计算只有在前一次调用的结果可用时才能开始;它必须等待整个先前的计算。这意味着对于 N 次对 sin 的调用,所需的总时间是单个 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 评估的单个链(或简单地评估编译时的结果)。可悲的是,我们生活在一个不完美的世界中。

【讨论】:

  • @DeadMG:当然可以。 x 的值是 v1 中循环携带的依赖。
  • @StephenCanon:OSX Libm 还在使用我的sin 实现吗?
  • @EricPostpischil:确实如此。
  • 更重要的是,FSIN 通常给出错误的结果。它不能做参数缩减。
  • @R..:是的,也是;我认为“使用 FSIN”是“使用 FPREM1 + FSIN”的简写(它不会做无限 pi 减少,但至少可以做一些事情)。
【解决方案2】:

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

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

除此之外并以不同的方式存储结果,我看不出有任何区别。

但是,真正不同的是,在第二种情况下,每个循环的输入都在更改。所以我怀疑会发生什么,在循环中的某些时候,sin 值变得更容易计算。这可以产生很大的不同。计算 sin 并不完全是微不足道的,它是一个循环计算,直到满足退出条件。

【讨论】:

  • 为什么要投反对票 - 我真的不喜欢没有反对意见的评论 - 我想了解哪里出了问题...
  • 我没有投反对票,但值得注意的是,两个程序计算的 sin 值完全相同,只是顺序不同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多