我如何达到每个周期4个FLOP的理论最大值?
如何在现代的x86-64 Intel CPU上实现4个浮点运算(双精度)的理论峰值性能?
据我所知,在大多数现代英特尔CPU上,需要三个周期才能完成SSE add
和五个周期的完成(例如参见Agner Fog的“指令表” )。 由于stream水线化,如果algorithm至less有三个独立的求和,每个周期可以获得一个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
在Intel Core 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
查看汇编代码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次。 有一个简单的例子,每个周期可以达到四个触发器吗?
Mysticial的一个不错的小程序; 这里是我的结果(虽然运行了几秒钟):
-
gcc -O2 -march=nocona
:10.66 Gflops中的5.6 Gflops(2.1个触发器/周期) -
cl /O2
,openmp被移除:10.66 Gflops中的10.1 Gflops(3.8个flop / cycle)
这一切似乎有点复杂,但我的结论迄今为止:
-
gcc -O2
改变了独立浮点操作的顺序,以便交替addpd
和mulpd
(如果可能的话)。 同样适用于gcc-4.6.2 -O2 -march=core2
。 -
gcc -O2 -march=nocona
似乎保持了C ++源代码中定义的浮点操作顺序。 -
cl /O2
,来自SDK for Windows 7的64位编译器会自动循环展开,并且似乎尝试安排操作,以便三个addpd
组与三个addpd
交替(至less在我的系统上为我简单的程序)。 -
我的Core i5 750 ( Nahelem体系结构 )不喜欢交替的add和mul,似乎不能同时运行这两个操作。 但是,如果将其分组为3,则突然变得像魔术一样。
-
其他体系结构(可能是Sandy Bridge等)似乎能够并行执行add / mul,而不会在汇编代码中交替出现问题。
-
虽然很难承认,但在我的系统上,
cl /O2
在我的系统的低级优化操作上做得好得多,并且达到了上述小C ++示例的接近最佳性能。 我测量了1.85-2.01个触发器/周期(在Windows中使用过clock(),这并不是那么精确,我想,需要使用更好的定时器 – 感谢Mackie Messer)。 -
我用
gcc
pipe理的最好的办法是手动循环展开,并按三个组来安排加法和乘法。 用g++ -O2 -march=nocona addmul_unroll.cpp
可以得到0.207s, 4.825 Gflops
,相当于0.207s, 4.825 Gflops
/周期,我现在很满意。
在C ++代码中,我用for
代替了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 ...
我之前完成了这个确切的任务。 但主要是测量功耗和CPU温度。 下面的代码(这是相当长的)在我的Core i7 2600K上实现接近最佳。
在这里要注意的关键是大量的手动循环展开以及交错的乘法和增加…
完整的项目可以在我的GitHub上find: https : //github.com/Mysticial/Flops
警告:
如果你决定编译和运行这个,注意你的CPU温度!
确保你不会过热。 并确保CPU-throttling不会影响您的结果!
此外,我对运行此代码可能导致的任何损害概不负责。
笔记:
- 此代码针对x64进行了优化。 x86没有足够的寄存器来编译。
- 此代码已经过testing,在Visual Studio 2010/2012和GCC 4.6上运行良好。
ICC 11(英特尔编译器11)出人意料地编译有问题。 - 这些是用于FMA前处理器的。 为了在英特尔®Haswell和AMD推土机处理器(及更高版本)上实现峰值FLOPS,需要使用FMA(熔合多路添加)指令。 这些超出了这个基准的范围。
#include <emmintrin.h> #include <omp.h> #include <iostream> using namespace std; typedef unsigned long long uint64; double test_dp_mac_SSE(double x,double y,uint64 iterations){ register __m128d r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,rA,rB,rC,rD,rE,rF; // Generate starting data. r0 = _mm_set1_pd(x); r1 = _mm_set1_pd(y); r8 = _mm_set1_pd(-0.0); r2 = _mm_xor_pd(r0,r8); r3 = _mm_or_pd(r0,r8); r4 = _mm_andnot_pd(r8,r0); r5 = _mm_mul_pd(r1,_mm_set1_pd(0.37796447300922722721)); r6 = _mm_mul_pd(r1,_mm_set1_pd(0.24253562503633297352)); r7 = _mm_mul_pd(r1,_mm_set1_pd(4.1231056256176605498)); r8 = _mm_add_pd(r0,_mm_set1_pd(0.37796447300922722721)); r9 = _mm_add_pd(r1,_mm_set1_pd(0.24253562503633297352)); rA = _mm_sub_pd(r0,_mm_set1_pd(4.1231056256176605498)); rB = _mm_sub_pd(r1,_mm_set1_pd(4.1231056256176605498)); rC = _mm_set1_pd(1.4142135623730950488); rD = _mm_set1_pd(1.7320508075688772935); rE = _mm_set1_pd(0.57735026918962576451); rF = _mm_set1_pd(0.70710678118654752440); uint64 iMASK = 0x800fffffffffffffull; __m128d MASK = _mm_set1_pd(*(double*)&iMASK); __m128d vONE = _mm_set1_pd(1.0); uint64 c = 0; while (c < iterations){ size_t i = 0; while (i < 1000){ // Here's the meat - the part that really matters. r0 = _mm_mul_pd(r0,rC); r1 = _mm_add_pd(r1,rD); r2 = _mm_mul_pd(r2,rE); r3 = _mm_sub_pd(r3,rF); r4 = _mm_mul_pd(r4,rC); r5 = _mm_add_pd(r5,rD); r6 = _mm_mul_pd(r6,rE); r7 = _mm_sub_pd(r7,rF); r8 = _mm_mul_pd(r8,rC); r9 = _mm_add_pd(r9,rD); rA = _mm_mul_pd(rA,rE); rB = _mm_sub_pd(rB,rF); r0 = _mm_add_pd(r0,rF); r1 = _mm_mul_pd(r1,rE); r2 = _mm_sub_pd(r2,rD); r3 = _mm_mul_pd(r3,rC); r4 = _mm_add_pd(r4,rF); r5 = _mm_mul_pd(r5,rE); r6 = _mm_sub_pd(r6,rD); r7 = _mm_mul_pd(r7,rC); r8 = _mm_add_pd(r8,rF); r9 = _mm_mul_pd(r9,rE); rA = _mm_sub_pd(rA,rD); rB = _mm_mul_pd(rB,rC); r0 = _mm_mul_pd(r0,rC); r1 = _mm_add_pd(r1,rD); r2 = _mm_mul_pd(r2,rE); r3 = _mm_sub_pd(r3,rF); r4 = _mm_mul_pd(r4,rC); r5 = _mm_add_pd(r5,rD); r6 = _mm_mul_pd(r6,rE); r7 = _mm_sub_pd(r7,rF); r8 = _mm_mul_pd(r8,rC); r9 = _mm_add_pd(r9,rD); rA = _mm_mul_pd(rA,rE); rB = _mm_sub_pd(rB,rF); r0 = _mm_add_pd(r0,rF); r1 = _mm_mul_pd(r1,rE); r2 = _mm_sub_pd(r2,rD); r3 = _mm_mul_pd(r3,rC); r4 = _mm_add_pd(r4,rF); r5 = _mm_mul_pd(r5,rE); r6 = _mm_sub_pd(r6,rD); r7 = _mm_mul_pd(r7,rC); r8 = _mm_add_pd(r8,rF); r9 = _mm_mul_pd(r9,rE); rA = _mm_sub_pd(rA,rD); rB = _mm_mul_pd(rB,rC); i++; } // Need to renormalize to prevent denormal/overflow. r0 = _mm_and_pd(r0,MASK); r1 = _mm_and_pd(r1,MASK); r2 = _mm_and_pd(r2,MASK); r3 = _mm_and_pd(r3,MASK); r4 = _mm_and_pd(r4,MASK); r5 = _mm_and_pd(r5,MASK); r6 = _mm_and_pd(r6,MASK); r7 = _mm_and_pd(r7,MASK); r8 = _mm_and_pd(r8,MASK); r9 = _mm_and_pd(r9,MASK); rA = _mm_and_pd(rA,MASK); rB = _mm_and_pd(rB,MASK); r0 = _mm_or_pd(r0,vONE); r1 = _mm_or_pd(r1,vONE); r2 = _mm_or_pd(r2,vONE); r3 = _mm_or_pd(r3,vONE); r4 = _mm_or_pd(r4,vONE); r5 = _mm_or_pd(r5,vONE); r6 = _mm_or_pd(r6,vONE); r7 = _mm_or_pd(r7,vONE); r8 = _mm_or_pd(r8,vONE); r9 = _mm_or_pd(r9,vONE); rA = _mm_or_pd(rA,vONE); rB = _mm_or_pd(rB,vONE); c++; } r0 = _mm_add_pd(r0,r1); r2 = _mm_add_pd(r2,r3); r4 = _mm_add_pd(r4,r5); r6 = _mm_add_pd(r6,r7); r8 = _mm_add_pd(r8,r9); rA = _mm_add_pd(rA,rB); r0 = _mm_add_pd(r0,r2); r4 = _mm_add_pd(r4,r6); r8 = _mm_add_pd(r8,rA); r0 = _mm_add_pd(r0,r4); r0 = _mm_add_pd(r0,r8); // Prevent Dead Code Elimination double out = 0; __m128d temp = r0; out += ((double*)&temp)[0]; out += ((double*)&temp)[1]; return out; } void test_dp_mac_SSE(int tds,uint64 iterations){ double *sum = (double*)malloc(tds * sizeof(double)); double start = omp_get_wtime(); #pragma omp parallel num_threads(tds) { double ret = test_dp_mac_SSE(1.1,2.1,iterations); sum[omp_get_thread_num()] = ret; } double secs = omp_get_wtime() - start; uint64 ops = 48 * 1000 * iterations * tds * 2; cout << "Seconds = " << secs << endl; cout << "FP Ops = " << ops << endl; cout << "FLOPs = " << ops / secs << endl; double out = 0; int c = 0; while (c < tds){ out += sum[c++]; } cout << "sum = " << out << endl; cout << endl; free(sum); } int main(){ // (threads, iterations) test_dp_mac_SSE(8,10000000); system("pause"); }
输出(1个线程,10000000次迭代) – 使用Visual Studio 2010 SP1编译 – x64版本:
Seconds = 55.5104 FP Ops = 960000000000 FLOPs = 1.7294e+010 sum = 2.22652
该机是Core i7 2600K @ 4.4 GHz。 理论上的SSE峰值是4×4.4 GHz = 17.6 GFlops 。 这个代码实现了17.3 GFlops – 不错。
输出(8个线程,10000000次迭代) – 用Visual Studio 2010 SP1编译 – x64发布:
Seconds = 117.202 FP Ops = 7680000000000 FLOPs = 6.55279e+010 sum = 17.8122
理论上的SSE峰值是4触发器* 4核心* 4.4 GHz = 70.4 GFlops。 实际是65.5 GFlops 。
让我们再走一步。 AVX …
#include <immintrin.h> #include <omp.h> #include <iostream> using namespace std; typedef unsigned long long uint64; double test_dp_mac_AVX(double x,double y,uint64 iterations){ register __m256d r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,rA,rB,rC,rD,rE,rF; // Generate starting data. r0 = _mm256_set1_pd(x); r1 = _mm256_set1_pd(y); r8 = _mm256_set1_pd(-0.0); r2 = _mm256_xor_pd(r0,r8); r3 = _mm256_or_pd(r0,r8); r4 = _mm256_andnot_pd(r8,r0); r5 = _mm256_mul_pd(r1,_mm256_set1_pd(0.37796447300922722721)); r6 = _mm256_mul_pd(r1,_mm256_set1_pd(0.24253562503633297352)); r7 = _mm256_mul_pd(r1,_mm256_set1_pd(4.1231056256176605498)); r8 = _mm256_add_pd(r0,_mm256_set1_pd(0.37796447300922722721)); r9 = _mm256_add_pd(r1,_mm256_set1_pd(0.24253562503633297352)); rA = _mm256_sub_pd(r0,_mm256_set1_pd(4.1231056256176605498)); rB = _mm256_sub_pd(r1,_mm256_set1_pd(4.1231056256176605498)); rC = _mm256_set1_pd(1.4142135623730950488); rD = _mm256_set1_pd(1.7320508075688772935); rE = _mm256_set1_pd(0.57735026918962576451); rF = _mm256_set1_pd(0.70710678118654752440); uint64 iMASK = 0x800fffffffffffffull; __m256d MASK = _mm256_set1_pd(*(double*)&iMASK); __m256d vONE = _mm256_set1_pd(1.0); uint64 c = 0; while (c < iterations){ size_t i = 0; while (i < 1000){ // Here's the meat - the part that really matters. r0 = _mm256_mul_pd(r0,rC); r1 = _mm256_add_pd(r1,rD); r2 = _mm256_mul_pd(r2,rE); r3 = _mm256_sub_pd(r3,rF); r4 = _mm256_mul_pd(r4,rC); r5 = _mm256_add_pd(r5,rD); r6 = _mm256_mul_pd(r6,rE); r7 = _mm256_sub_pd(r7,rF); r8 = _mm256_mul_pd(r8,rC); r9 = _mm256_add_pd(r9,rD); rA = _mm256_mul_pd(rA,rE); rB = _mm256_sub_pd(rB,rF); r0 = _mm256_add_pd(r0,rF); r1 = _mm256_mul_pd(r1,rE); r2 = _mm256_sub_pd(r2,rD); r3 = _mm256_mul_pd(r3,rC); r4 = _mm256_add_pd(r4,rF); r5 = _mm256_mul_pd(r5,rE); r6 = _mm256_sub_pd(r6,rD); r7 = _mm256_mul_pd(r7,rC); r8 = _mm256_add_pd(r8,rF); r9 = _mm256_mul_pd(r9,rE); rA = _mm256_sub_pd(rA,rD); rB = _mm256_mul_pd(rB,rC); r0 = _mm256_mul_pd(r0,rC); r1 = _mm256_add_pd(r1,rD); r2 = _mm256_mul_pd(r2,rE); r3 = _mm256_sub_pd(r3,rF); r4 = _mm256_mul_pd(r4,rC); r5 = _mm256_add_pd(r5,rD); r6 = _mm256_mul_pd(r6,rE); r7 = _mm256_sub_pd(r7,rF); r8 = _mm256_mul_pd(r8,rC); r9 = _mm256_add_pd(r9,rD); rA = _mm256_mul_pd(rA,rE); rB = _mm256_sub_pd(rB,rF); r0 = _mm256_add_pd(r0,rF); r1 = _mm256_mul_pd(r1,rE); r2 = _mm256_sub_pd(r2,rD); r3 = _mm256_mul_pd(r3,rC); r4 = _mm256_add_pd(r4,rF); r5 = _mm256_mul_pd(r5,rE); r6 = _mm256_sub_pd(r6,rD); r7 = _mm256_mul_pd(r7,rC); r8 = _mm256_add_pd(r8,rF); r9 = _mm256_mul_pd(r9,rE); rA = _mm256_sub_pd(rA,rD); rB = _mm256_mul_pd(rB,rC); i++; } // Need to renormalize to prevent denormal/overflow. r0 = _mm256_and_pd(r0,MASK); r1 = _mm256_and_pd(r1,MASK); r2 = _mm256_and_pd(r2,MASK); r3 = _mm256_and_pd(r3,MASK); r4 = _mm256_and_pd(r4,MASK); r5 = _mm256_and_pd(r5,MASK); r6 = _mm256_and_pd(r6,MASK); r7 = _mm256_and_pd(r7,MASK); r8 = _mm256_and_pd(r8,MASK); r9 = _mm256_and_pd(r9,MASK); rA = _mm256_and_pd(rA,MASK); rB = _mm256_and_pd(rB,MASK); r0 = _mm256_or_pd(r0,vONE); r1 = _mm256_or_pd(r1,vONE); r2 = _mm256_or_pd(r2,vONE); r3 = _mm256_or_pd(r3,vONE); r4 = _mm256_or_pd(r4,vONE); r5 = _mm256_or_pd(r5,vONE); r6 = _mm256_or_pd(r6,vONE); r7 = _mm256_or_pd(r7,vONE); r8 = _mm256_or_pd(r8,vONE); r9 = _mm256_or_pd(r9,vONE); rA = _mm256_or_pd(rA,vONE); rB = _mm256_or_pd(rB,vONE); c++; } r0 = _mm256_add_pd(r0,r1); r2 = _mm256_add_pd(r2,r3); r4 = _mm256_add_pd(r4,r5); r6 = _mm256_add_pd(r6,r7); r8 = _mm256_add_pd(r8,r9); rA = _mm256_add_pd(rA,rB); r0 = _mm256_add_pd(r0,r2); r4 = _mm256_add_pd(r4,r6); r8 = _mm256_add_pd(r8,rA); r0 = _mm256_add_pd(r0,r4); r0 = _mm256_add_pd(r0,r8); // Prevent Dead Code Elimination double out = 0; __m256d temp = r0; out += ((double*)&temp)[0]; out += ((double*)&temp)[1]; out += ((double*)&temp)[2]; out += ((double*)&temp)[3]; return out; } void test_dp_mac_AVX(int tds,uint64 iterations){ double *sum = (double*)malloc(tds * sizeof(double)); double start = omp_get_wtime(); #pragma omp parallel num_threads(tds) { double ret = test_dp_mac_AVX(1.1,2.1,iterations); sum[omp_get_thread_num()] = ret; } double secs = omp_get_wtime() - start; uint64 ops = 48 * 1000 * iterations * tds * 4; cout << "Seconds = " << secs << endl; cout << "FP Ops = " << ops << endl; cout << "FLOPs = " << ops / secs << endl; double out = 0; int c = 0; while (c < tds){ out += sum[c++]; } cout << "sum = " << out << endl; cout << endl; free(sum); } int main(){ // (threads, iterations) test_dp_mac_AVX(8,10000000); system("pause"); }
输出(1个线程,10000000次迭代) – 使用Visual Studio 2010 SP1编译 – x64版本:
Seconds = 57.4679 FP Ops = 1920000000000 FLOPs = 3.34099e+010 sum = 4.45305
理论AVX峰值是8个触发器* 4.4 GHz = 35.2 GFlops 。 实际是33.4 GFlops 。
输出(8个线程,10000000次迭代) – 用Visual Studio 2010 SP1编译 – x64发布:
Seconds = 111.119 FP Ops = 15360000000000 FLOPs = 1.3823e+011 sum = 35.6244
理论上的AVX峰值是8个触发器* 4个内核* 4.4 GHz = 140.8 GFlops。 实际是138.2 GFlops 。
现在来解释一下:
性能关键部分显然是内部循环内部的48条指令。 你会注意到它被分成了4块12条指令。 这12个指令块中的每一个都是完全独立的 – 平均需要6个周期才能执行。
所以有12个指令和6个周期之间的问题。 乘法的延迟是5个周期,所以这足以避免等待延迟。
正常化步骤是必要的,以防止数据溢出/下溢。 这是必要的,因为“无所作为”代码将缓慢地增加/减less数据的大小。
所以如果你只是使用全零并摆脱正常化步骤,那么实际上可能做得比这更好。 但是,由于我写了测量功耗和温度的基准, 所以我必须确保触发器是“真实”的数据而不是零 ,因为执行单元可能对功耗较低的零进行特殊情况处理并产生较less的热量。
更多结果:
- 英特尔酷睿i7 920 @ 3.5 GHz
- Windows 7旗舰版x64
- Visual Studio 2010 SP1 – x64发布
主题:1
Seconds = 72.1116 FP Ops = 960000000000 FLOPs = 1.33127e+010 sum = 2.22652
理论SSE峰值:4触发器* 3.5 GHz = 14.0 GFlops 。 实际是13.3 GFlops 。
主题:8
Seconds = 149.576 FP Ops = 7680000000000 FLOPs = 5.13452e+010 sum = 17.8122
理论SSE峰值:4个触发器* 4个核心* 3.5 GHz = 56.0 GFlops 。 实际是51.3 GFlops 。
在multithreading运行中,我的处理器临时命中了76C! 如果您运行这些,请确保结果不受CPU限制。
- 2个Intel Xeon X5482 Harpertown @ 3.2 GHz
- Ubuntu Linux 10 x64
- GCC 4.5.2 x64 – (-O2 -msse3 -fopenmp)
主题:1
Seconds = 78.3357 FP Ops = 960000000000 FLOPs = 1.22549e+10 sum = 2.22652
理论SSE峰值:4触发器* 3.2 GHz = 12.8 GFlops 。 实际是12.3 GFlops 。
主题:8
Seconds = 78.4733 FP Ops = 7680000000000 FLOPs = 9.78676e+10 sum = 17.8122
理论SSE峰值:4个触发器* 8个核心* 3.2GHz = 102.4 GFlops 。 实际是97.9 GFlops 。
英特尔架构中有一点常常被人们遗忘,调度端口在Int和FP / SIMD之间共享。 这意味着在循环逻辑将在您的浮点数据stream中创build气泡之前,您只会获得一定数量的FP / SIMD连发。 神秘的代码中有更多的失败,因为他在展开的循环中使用了更长的步幅。
如果您在这里查看 Nehalem / Sandy Bridge架构, 请访问http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6,这很清楚会发生什么。;
相比之下,由于INT和FP / SIMDpipe道具有独立的调度端口,因此在AMD(推土机)上达到最高性能应该会更容易一些。
这只是理论上的,因为我没有这些处理器来testing。
分支机构绝对可以阻止你保持最佳的理论性能。 如果你手动做一些循环展开,你会看到不同吗? 例如,如果你每循环迭代5次或10次操作:
for(int i=0; i<loops/5; i++) { mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul; sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add; mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul; sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add; mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul; sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add; mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul; sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add; mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul; sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add; }
在2.4GHz Intel Core 2 Duo上使用英特尔icc版本11.1
Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc && ./addmul 1000 addmul: 0.105 s, 9.525 Gflops, res=0.000000 Macintosh:~ mackie$ icc -v Version 11.1
这非常接近理想的9.6 Gflops。
编辑:
糟糕的是,看着汇编代码,icc似乎不仅向量化了乘法,而且还把这些加法从循环中拉出来了。 强制更严格的fp语义代码不再是vector化的:
Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000 addmul: 0.516 s, 1.938 Gflops, res=1.326463
EDIT2:
按照要求:
Macintosh:~ mackie$ clang -O3 -mssse3 -oaddmul addmul.cc && ./addmul 1000 addmul: 0.209 s, 4.786 Gflops, res=1.326463 Macintosh:~ mackie$ clang -v Apple clang version 3.0 (tags/Apple/clang-211.10.1) (based on LLVM 3.0svn) Target: x86_64-apple-darwin11.2.0 Thread model: posix
clang代码的内部循环如下所示:
.align 4, 0x90 LBB2_4: ## =>This Inner Loop Header: Depth=1 addsd %xmm2, %xmm3 addsd %xmm2, %xmm14 addsd %xmm2, %xmm5 addsd %xmm2, %xmm1 addsd %xmm2, %xmm4 mulsd %xmm2, %xmm0 mulsd %xmm2, %xmm6 mulsd %xmm2, %xmm7 mulsd %xmm2, %xmm11 mulsd %xmm2, %xmm13 incl %eax cmpl %r14d, %eax jl LBB2_4
EDIT3:
最后有两点build议:首先,如果您喜欢这种types的基准testing,请考虑使用rdtsc
指令而不是gettimeofday(2)
。 这是更准确和循环的时间,这通常是你通常感兴趣的。 对于gcc和朋友,你可以像这样定义它:
#include <stdint.h> static __inline__ uint64_t rdtsc(void) { uint64_t rval; __asm__ volatile ("rdtsc" : "=A" (rval)); return rval; }
其次,你应该多次运行你的基准testing程序, 只使用最好的性能 。 在现代操作系统中,许多事情并行发生,CPU可能处于低频省电模式等等。重复运行程序会给您一个更接近理想情况的结果。