如何在现代x86-64 Intel CPU上实现每个周期4次浮点运算(双精度)的理论峰值性能?
据我所知,在大多数现代英特尔CPU上,上交所add
需要三个周期,mul
需要五个周期才能完成(例如Agner Fog的“指令表”)。由于流水线,如果算法至少有三个独立的求和,每个周期可以获得一个add
的吞吐量。由于打包的addpd
以及标量addsd
版本和SSE寄存器都可以包含两个double
,因此每个周期的吞吐量可能多达两个翻牌。
此外,似乎(尽管我没有看到任何适当的留档)add
和mul
可以并行执行,理论上每个周期的最大吞吐量为四个翻牌。
然而,我无法用一个简单的C/C++程序来复制这种性能。我最好的尝试是大约2.7次/周期。如果有人能贡献一个简单的C/C++或汇编器程序来展示最佳性能,那将不胜感激。
我的尝试:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/time.h>
double stoptime(void) {
struct timeval t;
gettimeofday(&t,NULL);
return (double) t.tv_sec + t.tv_usec/1000000.0;
}
double addmul(double add, double mul, int ops){
// Need to initialise differently otherwise compiler might optimise away
double sum1=0.1, sum2=-0.1, sum3=0.2, sum4=-0.2, sum5=0.0;
double mul1=1.0, mul2= 1.1, mul3=1.2, mul4= 1.3, mul5=1.4;
int loops=ops/10; // We have 10 floating point operations inside the loop
double expected = 5.0*add*loops + (sum1+sum2+sum3+sum4+sum5)
+ pow(mul,loops)*(mul1+mul2+mul3+mul4+mul5);
for (int i=0; i<loops; i++) {
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
}
return sum1+sum2+sum3+sum4+sum5+mul1+mul2+mul3+mul4+mul5 - expected;
}
int main(int argc, char** argv) {
if (argc != 2) {
printf("usage: %s <num>\n", argv[0]);
printf("number of operations: <num> millions\n");
exit(EXIT_FAILURE);
}
int n = atoi(argv[1]) * 1000000;
if (n<=0)
n=1000;
double x = M_PI;
double y = 1.0 + 1e-8;
double t = stoptime();
x = addmul(x, y, n);
t = stoptime() - t;
printf("addmul:\t %.3f s, %.3f Gflops, res=%f\n", t, (double)n/t/1e9, x);
return EXIT_SUCCESS;
}
编译于:
g++ -O2 -march=native addmul.cpp ; ./a.out 1000
在英特尔酷睿i5-750,2.66 GHz上产生以下输出:
addmul: 0.270 s, 3.707 Gflops, res=1.326463
也就是说,每个周期只有大约1.4个浮点数。查看带有
g++ -S -O2 -march=native -masm=intel addmul.cpp
主循环似乎有点
最适合我的
.L4:
inc eax
mulsd xmm8, xmm3
mulsd xmm7, xmm3
mulsd xmm6, xmm3
mulsd xmm5, xmm3
mulsd xmm1, xmm3
addsd xmm13, xmm2
addsd xmm12, xmm2
addsd xmm11, xmm2
addsd xmm10, xmm2
addsd xmm9, xmm2
cmp eax, ebx
jne .L4
使用打包版本(addpd
和mulpd
)更改标量版本将使翻牌次数翻倍,而不会更改执行时间,因此每个周期的翻牌次数不到2.8次。有没有一个简单的例子可以实现每个周期四次翻牌?
不错的小程序由神秘;这是我的结果(运行几秒钟虽然):
gcc -O2 -march=nocona
:10.66个Gflps中的5.6个Gflps(2.1个浮点数/周期)cl /O2
,删除了openmp:10.66个Gflps中的10.1个Gflps(3.8个浮点/周期)这一切似乎有点复杂,但到目前为止我的结论是:
gcc -O2
更改独立浮点运算的顺序
交替的目的
addpd
和mulpd
如果可能的话。同样适用于gcc-4.6.2 -O2 -march=core2
。
gcc -O2 -march=nocona
似乎保持了浮点运算的顺序,如
C++来源
cl /O2
,来自
Windows 7的SDK
自动执行循环展开,似乎尝试并安排操作
因此,三个addpd
的组与三个mulpd
交替(好吧,至少在我的系统和我的简单程序中)。
我的酷睿i5 750(Nehalem建筑) 不喜欢交替添加和mul,似乎无法 并行运行两个操作。然而,如果分组为3,它突然像魔术一样工作。
其他架构(可能是Sandy Bridge和其他)似乎 能够并行执行add/mul而不会出现问题 如果它们在汇编代码中交替。
虽然很难承认,但在我的系统上cl /O2
在系统的低级优化操作方面做得更好,并且在上面的小C++示例中实现了接近峰值的性能。我测量了
1.85-2.01翻转/周期(在Windows中使用了时钟(),这不是那么精确。我想,需要使用更好的计时器-感谢Mackie Messer)。
我使用gcc
管理的最好的方法是手动循环展开和排列
三个一组的加法和乘法。随着
g++ -O2 -march=nocona addmul_unroll.cpp
我最多得到0.207s, 4.825 Gflops
,对应于1.8个翻牌/周期
“我现在很高兴”
在C++代码中,我将for
循环替换为:
for (int i=0; i<loops/3; i++) {
mul1*=mul; mul2*=mul; mul3*=mul;
sum1+=add; sum2+=add; sum3+=add;
mul4*=mul; mul5*=mul; mul1*=mul;
sum4+=add; sum5+=add; sum1+=add;
mul2*=mul; mul3*=mul; mul4*=mul;
sum2+=add; sum3+=add; sum4+=add;
mul5*=mul; mul1*=mul; mul2*=mul;
sum5+=add; sum1+=add; sum2+=add;
mul3*=mul; mul4*=mul; mul5*=mul;
sum3+=add; sum4+=add; sum5+=add;
}
现在的组件看起来像:
.L4:
mulsd xmm8, xmm3
mulsd xmm7, xmm3
mulsd xmm6, xmm3
addsd xmm13, xmm2
addsd xmm12, xmm2
addsd xmm11, xmm2
mulsd xmm5, xmm3
mulsd xmm1, xmm3
mulsd xmm8, xmm3
addsd xmm10, xmm2
addsd xmm9, xmm2
addsd xmm13, xmm2
...