Ich habe genau diese Aufgabe schon einmal erledigt. Dabei ging es aber hauptsächlich darum, den Stromverbrauch und die CPU-Temperaturen zu messen. Der folgende Code (der ziemlich lang ist) erreicht nahezu das Optimum auf meinem Core i7 2600K.
Das Wichtigste dabei ist die massive manuelle Schleifenentrollung sowie die Verschachtelung von Multiplizierungen und Additionen...
Das vollständige Projekt ist auf meinem GitHub zu finden: https://github.com/Mysticial/Flops
Warnung:
Wenn Sie sich entscheiden, dies zu kompilieren und auszuführen, achten Sie auf Ihre CPU-Temperaturen!!!
Achten Sie darauf, dass Sie es nicht überhitzen. Und stellen Sie sicher, dass die CPU-Drosselung Ihre Ergebnisse nicht beeinträchtigt!
Außerdem übernehme ich keine Verantwortung für Schäden, die durch die Ausführung dieses Codes entstehen könnten.
Anmerkungen:
- Dieser Code ist für x64 optimiert. x86 hat nicht genug Register, um ihn gut zu kompilieren.
- Dieser Code wurde auf Visual Studio 2010/2012 und GCC 4.6 getestet und funktioniert gut.
ICC 11 (Intel Compiler 11) hat überraschenderweise Schwierigkeiten, es gut zu kompilieren.
-
Diese sind für Vor-FMA-Prozessoren bestimmt. Um Spitzen-FLOPS auf Intel Haswell- und AMD Bulldozer-Prozessoren (und später) zu erreichen, werden FMA-Anweisungen (Fused Multiply Add) benötigt. Dies würde den Rahmen dieses Benchmarks sprengen.
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");
}
Ausgabe (1 Thread, 10000000 Iterationen) - Kompiliert mit Visual Studio 2010 SP1 - x64 Release:
Seconds = 55.5104
FP Ops = 960000000000
FLOPs = 1.7294e+010
sum = 2.22652
Der Rechner ist ein Core i7 2600K @ 4,4 GHz. Theoretische SSE-Spitze ist 4 Flops * 4,4 GHz = 17,6 GFlops . Mit diesem Code wird Folgendes erreicht 17,3 GFlops - nicht schlecht.
Ausgabe (8 Threads, 10000000 Iterationen) - Kompiliert mit Visual Studio 2010 SP1 - x64 Release:
Seconds = 117.202
FP Ops = 7680000000000
FLOPs = 6.55279e+010
sum = 17.8122
Die theoretische SSE-Spitze beträgt 4 Flops * 4 Kerne * 4,4 GHz = 70,4 GFlops. Tatsächlich ist 65,5 GFlops .
Gehen wir noch einen Schritt weiter. 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");
}
Ausgabe (1 Thread, 10000000 Iterationen) - Kompiliert mit Visual Studio 2010 SP1 - x64 Release:
Seconds = 57.4679
FP Ops = 1920000000000
FLOPs = 3.34099e+010
sum = 4.45305
Die theoretische AVX-Spitze beträgt 8 Flops * 4,4 GHz = 35,2 GFlops . Tatsächlich ist 33,4 GFlops .
Ausgabe (8 Threads, 10000000 Iterationen) - Kompiliert mit Visual Studio 2010 SP1 - x64 Release:
Seconds = 111.119
FP Ops = 15360000000000
FLOPs = 1.3823e+011
sum = 35.6244
Die theoretische AVX-Spitze beträgt 8 Flops * 4 Kerne * 4,4 GHz = 140,8 GFlops. Tatsächlich ist 138,2 GFlops .
Nun zu einigen Erklärungen:
Der leistungsrelevante Teil sind natürlich die 48 Anweisungen innerhalb der inneren Schleife. Sie werden feststellen, dass sie in 4 Blöcke zu je 12 Anweisungen unterteilt ist. Jeder dieser 12 Anweisungsblöcke ist völlig unabhängig voneinander und benötigt im Durchschnitt 6 Zyklen zur Ausführung.
Es gibt also 12 Anweisungen und 6 Zyklen zwischen Ausgabe und Verwendung. Die Latenzzeit der Multiplikation beträgt 5 Zyklen, was gerade ausreicht, um Latenzstörungen zu vermeiden.
Der Normalisierungsschritt ist erforderlich, um zu verhindern, dass die Daten über- oder unterlaufen werden. Dies ist erforderlich, da der Nichtstun-Code den Umfang der Daten langsam erhöht/verringert.
Es ist also möglich, eine bessere Leistung zu erzielen, wenn man einfach alle Nullen verwendet und den Normalisierungsschritt weglässt. Da ich den Benchmark jedoch geschrieben habe, um den Stromverbrauch und die Temperatur zu messen, Ich musste sicherstellen, dass die Flops auf "echten" Daten und nicht auf Nullen beruhen. - da die Ausführungseinheiten sehr wohl über eine spezielle Behandlung von Nullen verfügen können, die weniger Strom verbraucht und weniger Wärme erzeugt.
Weitere Ergebnisse:
- Intel Core i7 920 @ 3,5 GHz
- Windows 7 Ultimate x64
- Visual Studio 2010 SP1 - x64-Version
Fäden: 1
Seconds = 72.1116
FP Ops = 960000000000
FLOPs = 1.33127e+010
sum = 2.22652
Theoretische SSE-Spitze: 4 Flops * 3,5 GHz = 14,0 GFlops . Tatsächlich ist 13,3 GFlops .
Fäden: 8
Seconds = 149.576
FP Ops = 7680000000000
FLOPs = 5.13452e+010
sum = 17.8122
Theoretische SSE-Spitze: 4 Flops * 4 Kerne * 3,5 GHz = 56,0 GFlops . Tatsächlich ist 51,3 GFlops .
Meine Prozessortemperaturen erreichten 76°C beim Multi-Thread-Lauf! Wenn Sie diese Tests durchführen, stellen Sie sicher, dass die Ergebnisse nicht durch CPU-Drosselung beeinflusst werden.
- 2 x Intel Xeon X5482 Harpertown @ 3,2 GHz
- Ubuntu Linux 10 x64
- GCC 4.5.2 x64 - (-O2 -msse3 -fopenmp)
Fäden: 1
Seconds = 78.3357
FP Ops = 960000000000
FLOPs = 1.22549e+10
sum = 2.22652
Theoretische SSE-Spitze: 4 Flops * 3,2 GHz = 12,8 GFlops . Tatsächlich ist 12,3 GFlops .
Fäden: 8
Seconds = 78.4733
FP Ops = 7680000000000
FLOPs = 9.78676e+10
sum = 17.8122
Theoretische SSE-Spitze: 4 Flops * 8 Kerne * 3,2 GHz = 102,4 GFlops . Tatsächlich ist 97,9 GFlops .
17 Stimmen
Die Tatsache, dass man sich auf die Wanduhrzeit verlässt, ist wahrscheinlich ein Teil der Ursache. Wenn man davon ausgeht, dass Sie das Programm innerhalb eines Betriebssystems wie Linux ausführen, kann es Ihren Prozess jederzeit aus dem Zeitplan nehmen. Diese Art von externem Ereignis kann sich auf Ihre Leistungsmessungen auswirken.
0 Stimmen
Was ist Ihre GCC-Version? Wenn Sie auf einem Mac mit dem Standard sind, werden Sie in Probleme laufen (es ist eine alte 4.2).
2 Stimmen
Ja, unter Linux, aber das System ist nicht ausgelastet, und die mehrfache Wiederholung macht kaum Unterschiede (z. B. zwischen 4,0 und 4,2 Gflops für die skalare Version, aber jetzt mit
-funroll-loops
). Versucht mit gcc Version 4.4.1 und 4.6.2, aber asm Ausgabe sieht ok?0 Stimmen
Haben Sie versucht
-O3
für gcc, die Folgendes ermöglicht-ftree-vectorize
? Vielleicht kombiniert mit-funroll-loops
obwohl ich nicht weiß, ob das wirklich notwendig ist. Immerhin scheint der Vergleich etwas unfair, wenn einer der Compiler Vektorisierung/Unrolling durchführt und der andere nicht, weil er es nicht kann, sondern weil er es nicht soll.4 Stimmen
@Grizzly
-funroll-loops
ist wahrscheinlich etwas, das man versuchen sollte. Aber ich denke-ftree-vectorize
ist nicht von Belang. Der OP versucht lediglich, 1 Mul + 1 Additionsanweisung/Zyklus aufrechtzuerhalten. Die Anweisungen können skalar oder vektoriell sein - es spielt keine Rolle, da die Latenz und der Durchsatz gleich sind. Wenn man also 2/Zyklus mit skalarer SSE aufrechterhalten kann, dann kann man sie durch vektorielle SSE ersetzen und erreicht 4 Flops/Zyklus. In meiner Antwort habe ich genau das getan, indem ich von SSE -> AVX wechselte. Ich habe alle SSE durch AVX ersetzt - gleiche Latenzzeiten, gleicher Durchsatz, 2x die Flops.0 Stimmen
Hat jemand versucht dies mit dem CLANG LLVM-Compiler?
0 Stimmen
@Vinícius: gcc 4.9.2 macht keine automatische Vektorisierung.
clang-3.5
wird auch nicht automatisch vektorisiert: ~895M Zyklen auf meinem i5-2500k, für Iterationen = 500000.clang-3.8
tut, mit vielen Umschichtungen außerhalb der Schleife, um die ungerade Anzahl von Variablen zu behandeln. Er führt Iterationen = 500000 in ~114,01M Taktzyklen aus. (-std=gnu11 -march=native -Ofast -ffast-math
also wurde AVX1 verwendet). Beachten Sie, dass die FPU von Sandybridge nicht durch Denormale verlangsamt wird, aber ältere FPUs tun das normalerweise.0 Stimmen
Versuchen Sie, Ihre Benchmarks länger laufen zu lassen. Etwa 10-30 Sekunden oder so.