如何在现代 x86-64 Intel CPU 上实现每个周期 4 个浮点运算(双精度)的理论峰值性能?
据我了解,在大多数现代 Intel CPU 上完成 SSE add
需要三个周期,mul
需要五个周期(参见示例 Agner Fog's 'Instruction Tables' )。由于流水线,如果算法具有至少三个独立的求和,则每个周期可以获得一个 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
的汇编代码,主循环对我来说似乎是最佳选择。
.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
)更改标量版本将使 flop 计数增加一倍,而不会改变执行时间,因此每个周期我会得到 2.8 次 flop。有没有一个简单的例子可以实现每个周期四次翻转?
Mysticial 的小程序不错;这是我的结果(虽然只运行了几秒钟):
gcc -O2 -march=nocona:10.66 Gflops 中的 5.6 Gflops(2.1 flops/cycle)
cl /O2, openmp 已移除:10.66 Gflops 中的 10.1 Gflops(3.8 flops/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 的组与三个 mulpd 的组交替(嗯,至少在我的系统和我的简单程序中) )。
我的 Core i5 750(Nehalem 架构)不喜欢交替使用 add 和 mul,并且似乎无法同时运行这两个操作。但是,如果将其分组为 3,它会突然像魔术一样起作用。
如果其他架构(可能是 Sandy Bridge 和其他架构)在汇编代码中交替执行,它们似乎能够毫无问题地并行执行 add/mul。
虽然很难承认,但在我的系统上,cl /O2 在我的系统的低级优化操作方面做得更好,并且在上面的小 C++ 示例中实现了接近峰值的性能。我在 1.85-2.01 flops/cycle 之间测量(在 Windows 中使用了 clock() 并不那么精确。我想,需要使用更好的计时器 - 感谢 Mackie Messer)。
我用 gcc 管理的最好的方法是手动循环展开并以三人一组的方式安排加法和乘法。使用 g++ -O2 -march=nocona addmul_unroll.cpp 我最多可以得到 0.207 秒,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
...
-funroll-loops
)。尝试使用 gcc 版本 4.4.1 和 4.6.2,但 asm 输出看起来还可以吗?
-O3
,它启用了 -ftree-vectorize
?也许与 -funroll-loops
结合,但如果真的有必要,我不会。毕竟,如果其中一个编译器进行矢量化/展开,那么比较似乎有点不公平,而另一个不是因为它不能,而是因为它被告知不要。
-funroll-loops
可能值得尝试。但我认为 -ftree-vectorize
不是重点。 OP 只是试图维持 1 mul + 1 add 指令/周期。指令可以是标量或向量 - 没关系,因为延迟和吞吐量是相同的。因此,如果您可以用标量 SSE 维持 2 个/周期,那么您可以用向量 SSE 替换它们,您将实现 4 个触发器/周期。在我的回答中,我就是从 SSE 做的-> AVX。我用 AVX 替换了所有 SSE——相同的延迟、相同的吞吐量、两倍的失败率。
我以前做过这个确切的任务。但主要是测量功耗和CPU温度。下面的代码(相当长)在我的 Core i7 2600K 上实现了接近最优。
这里要注意的关键是大量的手动循环展开以及乘法和加法的交错......
完整的项目可以在我的 GitHub 上找到:https://github.com/Mysticial/Flops
警告:
如果您决定编译并运行它,请注意您的 CPU 温度!!!确保不要过热。并确保 CPU 节流不会影响您的结果!
此外,我对运行此代码可能造成的任何损害不承担任何责任。
笔记:
此代码针对 x64 进行了优化。 x86 没有足够的寄存器来编译。
此代码已经过测试,可在 Visual Studio 2010/2012 和 GCC 4.6 上正常运行。 ICC 11 (Intel Compiler 11) 出人意料地难以很好地编译它。
这些适用于 FMA 之前的处理器。为了在 Intel Haswell 和 AMD Bulldozer 处理器(及更高版本)上实现峰值 FLOPS,将需要 FMA(Fused Multiply Add)指令。这些超出了本基准的范围。
#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 个周期,因此足以避免延迟停止。
需要规范化步骤来防止数据上溢/下溢。这是必需的,因为无操作代码将缓慢增加/减少数据的大小。
因此,如果您只使用全零并摆脱标准化步骤,实际上可以做得比这更好。然而,由于我编写了测量功耗和温度的基准,我必须确保触发器是在“真实”数据上,而不是零——因为执行单元很可能对使用较少功率的零进行特殊处理并产生更少的热量。
更多结果:
英特尔酷睿 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。
在多线程运行中,我的处理器温度达到 76C!如果您运行这些,请确保结果不受 CPU 限制的影响。
2 x 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.2 GHz = 102.4 GFlops。实际是 97.9 GFlops。
人们经常忘记英特尔架构中的一点,调度端口在 Int 和 FP/SIMD 之间共享。这意味着在循环逻辑将在浮点流中创建气泡之前,您只会获得一定数量的 FP/SIMD 突发。 Mystical 从他的代码中得到了更多的失败,因为他在展开的循环中使用了更长的步幅。
如果您查看此处的 Nehalem/Sandy Bridge 架构http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6,就会很清楚发生了什么。
相比之下,在 AMD(推土机)上达到峰值性能应该更容易,因为 INT 和 FP/SIMD 管道具有独立的问题端口和自己的调度程序。
这只是理论上的,因为我没有这些处理器要测试。
inc
、cmp
和 jl
。所有这些都可以转到端口#5,并且不会干扰矢量化 fadd
或 fmul
。我宁愿怀疑解码器(有时)会碍事。每个周期它需要维持两到三个指令。我不记得确切的限制,但指令长度、前缀和对齐方式都在起作用。
cmp
和 jl
肯定会去端口 5,inc
不太确定,因为它总是与其他 2 个一起出现。但你是对的,很难说瓶颈在哪里,解码器也可能是其中的一部分。
分支绝对可以使您无法维持最高的理论性能。如果您手动进行一些循环展开,您会看到区别吗?例如,如果您在每次循环迭代中放置 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;
}
-funroll-loops
选项,该选项甚至不包含在 -O3
中。见g++ -c -Q -O2 --help=optimizers | grep unroll
。
在 2.4GHz Intel Core 2 Duo 上使用 Intels 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 语义代码不再矢量化:
Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000
addmul: 0.516 s, 1.938 Gflops, res=1.326463
编辑2:
按照要求:
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
编辑3:
最后,有两个建议:首先,如果您喜欢这种类型的基准测试,请考虑使用 rdtsc
指令而不是 gettimeofday(2)
。它更加准确,并以周期为单位提供时间,这通常是您感兴趣的。对于 gcc 和朋友,您可以像这样定义它:
#include <stdint.h>
static __inline__ uint64_t rdtsc(void)
{
uint64_t rval;
__asm__ volatile ("rdtsc" : "=A" (rval));
return rval;
}
其次,您应该多次运行您的基准测试程序并只使用最佳性能。在现代操作系统中,许多事情是并行发生的,cpu 可能处于低频省电模式等。重复运行程序会给你一个更接近理想情况的结果。
addsd
和 mulsd
混合在一起,或者它们是否像我的程序集输出一样分组?当编译器混合它们时,我也得到了大约 1 个 flop/cycle(我没有 -march=native
)。如果在函数 addmul(...)
的开头添加一行 add=mul;
,性能会如何变化?
addsd
和 subsd
指令确实混合在精确版本中。我也尝试过 clang 3.0,它不会混合指令,并且在 core 2 duo 上非常接近 2 次失败/周期。当我在笔记本电脑 core i5 上运行相同的代码时,混合代码没有任何区别。在任何一种情况下,我都会得到大约 3 次失败/周期。
icc
中看到的那样进行优化,您可以仔细检查程序集吗?
不定期副业成功案例分享
1.814s, 5.292 Gflops, sum=0.448883
超出峰值 10.68 Gflops 或每个周期仅短于 2.0 flops。似乎add
/mul
没有并行执行。当我更改您的代码并始终使用相同的寄存器进行加法/乘法运算时,例如rC
,它突然几乎达到峰值:0.953s, 10.068 Gflops, sum=0
或 3.8 次翻转/周期。很奇怪。cl /O2
(来自 windows sdk 的 64 位)在 Windows 7 上确认您的结果,甚至我的示例在该处的标量操作(1.9 次触发器/周期)运行接近峰值。编译器循环展开和重新排序,但这可能不是需要更多研究的原因。节流不是问题我对我的 cpu 很好,并将迭代保持在 100k。 :)using namespace std;
is a bad practice,永远不要使用它。