考虑以下两个以两种不同方式执行相同计算的程序:
// 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框中运行时,第二个程序的速度是第一个程序的两倍。
为什么?
一个嫌疑人是i
中v1
循环的连续迭代之间的数据依赖性。但是,我不太明白完整的解释是什么。
(问题受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
答案 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是正确的,v1
和v2
在形式上是等价的,在完美的世界中,编译器会将它们优化为100000 sin
的单个链。评估(或简单地在编译时评估结果)。可悲的是,我们生活在一个不完美的世界。
答案 1 :(得分:0)
在第一个例子中,它运行100000个sin循环,8192次。
在第二个例子中,它运行了8192个sin循环,100000次。
除此之外,以不同方式存储结果,我看不出任何差异。
然而,有意义的是,在第二种情况下,每个循环的输入都在改变。所以我怀疑发生的事情是,在循环中的某些时间,sin值变得更容易计算。这可以产生很大的不同。计算sin并不是完全无关紧要的,而是一系列计算循环直到退出条件被击中。