1サイクルあたり4回の浮動小数点演算(倍精度)の理論上のピーク性能を、最新のx86-64 Intel CPUでどのように達成することができますか?
私が理解している限りでは、 SSEadd
name__には3サイクル、mul
name__には5サイクルかかります。例えば Agner Fogの 'Instruction Tables' )です。パイプライン化により、アルゴリズムが少なくとも3つの独立した合計を持つ場合、1サイクルあたり1つのadd
name__のスループットが得られます。これは、パックaddpd
name__、スカラーaddsd
name__バージョン、およびSSEレジスタに2つのdouble
name __を含めることができる場合にも当てはまります。スループットは、1サイクルあたり最大2フロップになります。
さらに、(これに関する適切なドキュメントは見ていませんが)add
name__とmul
name__は並列に実行でき、1サイクルあたり4フロップの理論上の最大スループットが得られます。
しかし、私は単純な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
2.66 GHz、Intel Core i5-750で以下の出力を生成します。
addmul: 0.270 s, 3.707 Gflops, res=1.326463
つまり、1サイクルあたり約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
name__とmulpd
name__)でスカラーバージョンを変更すると、実行時間を変更せずにフロップカウントが2倍になるので、1サイクルあたり2.8フロップに達することはありません。サイクルごとに4つのフロップを達成する簡単な例はありますか?
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
name__とmulpd
name__を交互に使用することを目的として、独立した浮動小数点演算の順序を変更します。同じことがgcc-4.6.2 -O2 -march=core2
にも当てはまります。
gcc -O2 -march=nocona
は、C++ソースで定義されているように浮動小数点演算の順序を維持するようです。
SDK for Windows 7 の64ビットコンパイラであるcl /O2
は、自動的にループ展開を行い、3つのaddpd
name__のグループが交互になるように操作を試行して調整するようです。 3つのmulpd
name __があります(まあ、少なくとも私のシステムでは、私の単純なプログラムでは)。
私の Core i5 750 ( Nehalemアーキテクチャ )は交互のアドが好きではありませんまた、両方の操作を並行して実行することはできません。しかし、3のものに分類されるならば、それは突然魔法のように働きます。
他のアーキテクチャ(場合によっては Sandy Bridge など)も、アセンブリコードを交互に使用すれば、問題なくadd/mulを並列に実行できるように見えます。
認めるのは難しいですが、私のシステムではcl /O2
は私のシステムのための低レベルの最適化操作ではるかに良い仕事をし、そして上の小さなC++の例でピークに近いパフォーマンスを達成します。私は1.85-2.01フロップ/サイクルの間で測定しました(Windowsではclock()を使っていましたが、それほど正確ではありません。もっといいタイマーを使う必要があると思います - ありがとうMackie Messer)。
私がgcc
name__を使って管理した最良の方法は、手動で展開をループし、加算と乗算を3つのグループにまとめることでした。 g++ -O2 -march=nocona addmul_unroll.cpp
を使って、私はせいぜい0.207s, 4.825 Gflops
を得ます。これは1.8フロップ/サイクルに相当します。
C++コードで、私はfor
name__ループを次のように置き換えました。
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で見つけることができます。 https://github.com/Mysticial/Flops
これをコンパイルして実行する場合は、CPUの温度に注意してください!!!
過熱しないように注意してください。そして、CPUスロットルがあなたの結果に影響しないことを確認してください!
さらに、私はこのコードを実行することによって生じるいかなる損害についても責任を負いません。
注:
#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。
#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の命令です。あなたはそれがそれぞれ12の命令の4つのブロックに分割されていることに気付くでしょう。これら12個の命令ブロックはそれぞれ完全に独立しており、実行に平均6サイクルかかります。
したがって、発行から使用までの間に12の命令と6サイクルがあります。乗算のレイテンシは5サイクルなので、レイテンシストールを回避するのに十分です。
正規化ステップは、データがオーバーフローまたはアンダーフローしないようにするために必要です。何もしないコードはデータの大きさをゆっくり増減させるのでこれが必要です。
したがって、すべてゼロを使用して正規化手順を削除すれば、実際にこれよりもうまくいくことがあります。しかし、消費電力と温度を測定するためのベンチマークを書いたので、フロップがゼロではなく「実際の」データであることを確認しなければなりませんでした - 実行ユニットは、より少ない電力を使用し、より少ない熱を生成するゼロのための特別なケース処理を非常によく有することがあるので。
スレッド数: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です。
私のプロセッサ温度はマルチスレッド実行で76℃に達しました!これらを実行する場合は、結果がCPUスロットルの影響を受けないことを確認してください。
スレッド数: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 何が起こるのかは明らかです。
対照的に、INTおよびFP/SIMDパイプには独自のスケジューラを備えた別々の発行ポートがあるため、AMD(Bulldozer)でピークパフォーマンスを達成するのは簡単です。
私がテストするこれらのプロセッサのどちらも持っていないので、これは理論的にすぎません。
枝は間違いなくあなたがピークの理論的性能を維持するのを妨げることができます。手動でループ展開を行った場合、違いがありますか?たとえば、ループの反復ごとに5倍または10倍のopsを設定したとします。
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で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:
最後に、2つの提案:最初に、このタイプのベンチマークが好きなら、gettimeofday(2)
の代わりにrdtsc
name__命令の使用を検討してください。それははるかに正確で、サイクルで時間を提供します。それは通常あなたがとにかく興味を持っているものです。 gccと友達のためにあなたはこのように定義することができます:
#include <stdint.h>
static __inline__ uint64_t rdtsc(void)
{
uint64_t rval;
__asm__ volatile ("rdtsc" : "=A" (rval));
return rval;
}
次に、ベンチマークプログラムを数回実行して、最高のパフォーマンスのみを使用する必要があります。最近のオペレーティングシステムでは、多くのことが並行して行われます。CPUは低周波数の省電力モードなどになることがあります。プログラムを繰り返し実行すると、理想的なケースに近い結果が得られます。