How do I achieve the theoretical maximum of 4 FLOPs per cycle?
如何在现代x86-64 Intel CPU上实现每周期4个浮点运算(双精度)的理论峰值性能?
据我所知,在大多数现代Intel CPU上,SSE
此外,似乎(尽管我没有看到任何关于这方面的适当文档)可以并行执行
但是,我不能用一个简单的C/C++程序来复制这种性能。我的最佳尝试导致大约2.7次失败/周期。如果任何人可以贡献一个简单的C/C++或汇编程序,这将显示出极大的性能,这将是非常值得赞赏的。
我的尝试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | #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> ", argv[0]); printf("number of operations: <num> millions "); 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 ", t, (double)n/t/1e9, x); return EXIT_SUCCESS; } |
编译用
1 | g++ -O2 -march=native addmul.cpp ; ./a.out 1000 |
在Intel Core i5-750、2.66 GHz上生成以下输出。
1 | addmul: 0.270 s, 3.707 Gflops, res=1.326463 |
也就是说,每个周期只有大约1.4次触发器。查看汇编程序代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .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 |
使用压缩版本(
很好的神秘小程序;以下是我的结果(不过只运行几秒钟):
gcc -O2 -march=nocona :10.66 gflops中有5.6 gflops(2.1次/周期)cl /O2 ,openmp删除:10.66 gflops中有10.1 gflops(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个组,它会突然像魔法一样工作。
其他建筑(可能是沙桥和其他建筑)似乎能够并行执行add/mul而不会出现问题如果它们在程序集代码中交替出现。
虽然很难承认,但是在我的系统EDOCX1中,12度在我的系统的低级优化操作方面做得更好,并且对于上面的C++实例达到了接近峰值的性能。我测量了1.85-2.01触发器/周期(在Windows中使用的是clock(),这不是很精确。我想,需要一个更好的计时器-谢谢麦基·梅瑟)。
我用
gcc 管理的最好方法是手动循环展开和排列三个一组的加法和乘法。用g++ -O2 -march=nocona addmul_unroll.cpp 我最多只能得到对应于1.8次/周期的0.207s, 4.825 Gflops 。我现在很高兴。
在C++代码中,我已经替换了EDCOX1的25个循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } |
现在大会看起来像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .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温度。下面的代码(相当长)在我的核心i7 2600K上实现了接近最优。
这里需要注意的关键是大量的手动循环展开以及乘法和加法的交错…
完整的项目可以在我的Github上找到:https://github.com/mysticial/flops
警告:如果您决定编译和运行这个,请注意您的CPU温度!!!!确保不要过热。确保CPU节流不会影响您的结果!
此外,对于运行此代码可能导致的任何损坏,我不承担任何责任。
笔记:
- 此代码针对x64进行了优化。x86没有足够的寄存器来很好地编译。
- 这段代码在Visual Studio 2010/2012和GCC 4.6.ICC 11(英特尔编译器11)上测试得很好,令人惊讶的是,它在编译时遇到了问题。
- 这些是用于FMA前处理器的。为了在Intel Haswell和AMD推土机处理器(以及更高版本)上实现峰值触发器,需要fma(fused multiple add)指令。这些超出了这个基准的范围。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | #include #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版本编译:
1 2 3 4 | Seconds = 55.5104 FP Ops = 960000000000 FLOPs = 1.7294e+010 sum = 2.22652 |
这台机器的核心是i7 [email protected] GHz。理论上,SSE峰值为4个触发器*4.4 GHz=17.6 gFlops。此代码实现17.3 gflops-不错。
输出(8个线程,10000000次迭代)-使用Visual Studio 2010 SP1-x64版本编译:
1 2 3 4 | 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…1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | #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版本编译:
1 2 3 4 | 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版本编译:
1 2 3 4 | 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个周期,所以它足以避免延迟暂停。
需要规范化步骤来防止数据过度/不足。这是必需的,因为"不做任何事情"代码将缓慢地增加/减少数据量。
所以实际上,如果您只使用所有的0并去掉标准化步骤,就可以做得更好。但是,由于我编写了用于测量功耗和温度的基准,因此我必须确保触发器是基于"真实"数据,而不是零——因为执行单元可能非常适合对使用较少功率和产生较少热量的零进行特殊情况处理。
更多的结果:- 英特尔酷睿i7 [email protected] GHz
- Windows 7旗舰版x64
- Visual Studio 2010 SP1-x64版本
线程:1
1 2 3 4 | 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
1 2 3 4 | 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。
我的处理器温度达到76C多线程运行!如果运行这些,请确保结果不受CPU限制的影响。
- 2个Intel Xeon X5482 [email protected] GHz
- Ubuntu Linux 10 x64
- 一般合同条款第4.5.2款X64-(-O2-MSSE3-FOPENMP)
线程:1
1 2 3 4 | 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
1 2 3 4 | Seconds = 78.4733 FP Ops = 7680000000000 FLOPs = 9.78676e+10 sum = 17.8122 |
理论SSE峰值:4个触发器*8个内核*3.2 GHz=102.4 gFlops。实际为97.9 gflops。
在英特尔体系结构中,有一点人们经常忘记,即调度端口在int和fp/simd之间共享。这意味着在循环逻辑在您的浮点流中创建气泡之前,您只会得到一定数量的fp/simd突发。神秘主义者从他的代码中得到了更多的失败,因为他在展开的循环中使用了更长的步幅。
如果你看看这里的尼哈勒姆/桑迪桥建筑http://www.realworldtech.com/page.cfm?文章ID=RWT091810191937&P=6很清楚会发生什么。
相比之下,在AMD(推土机)上更容易达到峰值性能,因为int和fp/simd管道具有各自的调度程序的独立问题端口。
这只是理论上的,因为我没有这些处理器要测试。
分支绝对可以阻止您维持峰值理论性能。如果手动展开某个循环,您会看到区别吗?例如,如果您在每个循环迭代中放置5或10倍的操作:
1 2 3 4 5 6 7 8 9 10 11 12 | 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上使用Intel ICC 11.1版
1 2 3 4 | 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.6gFlops。
编辑:
哎呀,看着汇编代码,似乎ICC不仅使乘法向量化,而且把加法从循环中拉了出来。强制使用更严格的fp语义,代码不再向量化:
1 2 | Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000 addmul: 0.516 s, 1.938 Gflops, res=1.326463 |
编辑2:
按要求:
1 2 3 4 5 6 | 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代码的内部循环如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | .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 |
EdTe3:
最后,有两个建议:第一,如果您喜欢这种基准测试,可以考虑使用
1 2 3 4 5 6 7 8 | #include <stdint.h> static __inline__ uint64_t rdtsc(void) { uint64_t rval; __asm__ volatile ("rdtsc" :"=A" (rval)); return rval; } |
第二,您应该多次运行基准测试程序,并且只使用最佳性能。在现代操作系统中,许多事情都是并行发生的,CPU可能处于低频节电模式,等等。反复运行程序会得到一个更接近理想情况的结果。