Jak osiągnąć teoretyczne maksimum 4 flopów na cykl?
Jak można osiągnąć teoretyczną maksymalną wydajność 4 operacji zmiennoprzecinkowych (Podwójna precyzja) na cykl na nowoczesnym procesorze Intel x86 - 64?
Z tego co rozumiem to potrzeba trzech cykli dla SSE add
i pięć cykli dla mul
, aby zakończyć na większości nowoczesnych procesorów Intela (patrz na przykład Agner Fog 's'Instruction Tables' ). Ze względu na pipelining można uzyskać przepustowość jednego add
na cykl, jeśli algorytm ma co najmniej trzy niezależne podsumowania. Ponieważ dotyczy to zarówno wersji spakowanych addpd
, jak i wersji skalarnych addsd
i rejestrów SSE może zawierać dwa double
, przepustowość może wynosić nawet dwa flopy na cykl.
Co więcej, wydaje się (chociaż nie widziałem odpowiedniej dokumentacji na ten temat) add
i mul
mogą być wykonywane równolegle, dając teoretyczną maksymalną przepustowość czterech flopów na cykl.
Jednak nie byłem w stanie odtworzyć tej wydajności za pomocą prostego programu C/C++. My best próba zaowocowała około 2.7 flops / cykl. Gdyby ktoś mógł dodać prosty program do C / C++ lub asemblera, który demonstruje szczytową wydajność, byłby bardzo mile widziany.
Moja próba:
#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;
}
Skompilowane z
g++ -O2 -march=native addmul.cpp ; ./a.out 1000
Produkuje następujące wyjście na Intel Core i5-750, 2.66 GHz.
addmul: 0.270 s, 3.707 Gflops, res=1.326463
To znaczy, tylko o 1.4 flops na cykl. Patrząc na kod asemblera z
g++ -S -O2 -march=native -masm=intel addmul.cpp
główna pętla wydaje się jakby
optymalne dla mnie:
.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
Zmiana wersje skalarne z spakowanymi wersjami (addpd
i mulpd
) podwoiłyby liczbę flopów bez zmiany czasu wykonania, więc dostałbym tylko 2.8 flopów na cykl. Czy istnieje prosty przykład, który osiąga cztery flopy na cykl?
Fajny mały program by Mysticial; oto moje wyniki (chociaż uruchom tylko przez kilka sekund):
-
gcc -O2 -march=nocona
: 5.6 Gflops z 10.66 Gflops (2.1 flops/cykl) -
cl /O2
, OpenMP usunięty: 10.1 Gflops z 10.66 Gflops (3.8 flops / cykl)
To wszystko wydaje się trochę skomplikowane, ale moje wnioski jak na razie:
gcc -O2
zmienia kolejność niezależnych operacji zmiennoprzecinkowych z celem naprzemiennegoaddpd
imulpd
, jeśli to możliwe. To samo dotyczygcc-4.6.2 -O2 -march=core2
.gcc -O2 -march=nocona
wydaje się zachować kolejność operacji zmiennoprzecinkowych zdefiniowaną w źródło C++.cl /O2
, 64-bitowy kompilator z SDK dla Windows 7 czy rozwijanie pętli automatycznie i wydaje się próbować zorganizować operacje tak, że grupy trzechaddpd
's naprzemiennie z trzemamulpd
' s (no, przynajmniej w moim systemie i dla mojego prostego programu).My Core i5 750 (Architektura Nahelem ) nie lubi przemiennych add 'ów i mul' ów i wydaje się niemożliwy prowadzenie obu operacji równolegle. Jednak, jeśli zgrupowane w 3 to nagle działa jak magia.
Inne architektury (ewentualnie Sandy Bridge i inne) wydają się Możliwość równoległego uruchamiania add / mul bez problemów jeśli występują naprzemiennie w kodzie złożenia.
Chociaż trudno to przyznać, ale na moim systemie
cl /O2
wykonuje znacznie lepszą pracę przy niskopoziomowych operacjach optymalizacyjnych dla mojego systemu i osiąga prawie szczytową wydajność dla małego przykładu C++ powyżej. Mierzyłem między 1.85-2.01 flops/cycle (użyłem clock () w Windows, co nie jest takie precyzyjne. Chyba trzeba użyć lepszego timera-dzięki Mackie Messer).Najlepsze, co udało mi się zrobić
gcc
, to ręcznie rozwinąć pętlę i uporządkować dodawanie i mnożenie w grupach po trzy. Zg++ -O2 -march=nocona addmul_unroll.cpp
Dostaję co najwyżej {[30] } co odpowiada 1.8 flopów / cykl z czego jestem teraz całkiem zadowolony.
W kodzie C++ zamieniłem pętlę for
na
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;
}
A Zgromadzenie wygląda teraz jak
.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
...
4 answers
Wykonywałem już to zadanie. Ale chodziło głównie o pomiar zużycia energii i temperatury PROCESORA. Poniższy kod (który jest dość długi) osiąga prawie optymalne na moim Core i7 2600K.
Kluczową rzeczą do odnotowania jest ogromna ilość ręcznego rozwijania pętli, jak również przeplatania mnożenia i dodawania...
Pełny projekt można znaleźć na moim Githubie: https://github.com/Mysticial/Flops
Ostrzeżenie:
Jeśli zdecydujesz się na skompiluj i uruchom to, zwróć uwagę na temperaturę procesora!!!
upewnij się, że go nie przegrzejesz. I upewnij się, że ograniczenie CPU nie wpływa na wyniki!
Ponadto, nie biorę odpowiedzialności za jakiekolwiek szkody, które mogą wyniknąć z uruchomienia tego kodu.
Uwagi:
- ten kod jest zoptymalizowany dla x64. x86 nie ma wystarczającej ilości rejestrów, aby to dobrze skompilować.
- ten kod został przetestowany, aby dobrze działał na Visual Studio 2010/2012 i GCC 4.6.
ICC 11 (Intel Compiler 11) zaskakująco ma problemy z kompilacją go dobrze.
Są one przeznaczone dla procesorów pre-FMA. Aby osiągnąć szczytowe klapy na procesorach Intel Haswell i AMD Bulldozer (i późniejszych), potrzebne będą instrukcje FMA (Fused Multiply Add). Są one poza zakresem tego wskaźnika referencyjnego.
#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");
}
Wyjście (1 Wątek, 10000000 iteracji) - skompilowane z Visual Studio 2010 SP1-x64 Release:
Seconds = 55.5104
FP Ops = 960000000000
FLOPs = 1.7294e+010
sum = 2.22652
The maszyna jest Core i7 2600k @ 4.4 GHz. Teoretyczny szczyt SSE wynosi 4 flops * 4,4 GHz = 17,6 GFlops. Ten kod osiąga 17,3 GFlops - nieźle.
Wyjście (8 wątków, 10000000 iteracji) - skompilowane z Visual Studio 2010 SP1-x64 Release:
Seconds = 117.202
FP Ops = 7680000000000
FLOPs = 6.55279e+010
sum = 17.8122
Teoretyczny szczyt SSE to 4 flops * 4 rdzenie * 4,4 GHz = 70,4 GFlops. rzeczywista jest 65,5 GFlops.
Pójdźmy o krok dalej. 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");
}
Wyjście (1 Wątek, 10000000 iteracji) - skompilowane z Visual Studio 2010 SP1-x64 Release:
Seconds = 57.4679
FP Ops = 1920000000000
FLOPs = 3.34099e+010
sum = 4.45305
Teoretyczny szczyt AVX to 8 flops * 4,4 GHz = 35,2 GFlops. Rzeczywista jest 33,4 GFlops .
Wyjście (8 wątków, 10000000 iteracji) - skompilowane z Visual Studio 2010 SP1-x64 Release:
Seconds = 111.119
FP Ops = 15360000000000
FLOPs = 1.3823e+011
sum = 35.6244
Teoretyczny szczyt AVX to 8 flopów * 4 rdzenie * 4,4 GHz = 140,8 GFlops. rzeczywista jest 138,2 GFlops .
A teraz kilka wyjaśnień:
Krytyczną częścią wydajności jest oczywiście 48 instrukcji wewnątrz wewnętrznej pętli. Zauważysz, że jest podzielony na 4 bloki po 12 Instrukcji każdy. Każdy z tych 12 bloków instrukcji jest całkowicie niezależny od siebie - i zajmuje średnio 6 cykli do wykonania.
Więc jest 12 instrukcji i 6 cykli między wydaniem do użycia. Opóźnienie mnożenia wynosi 5 cykli, więc to tylko wystarczająco, aby uniknąć opóźnień.
Krok normalizacji jest potrzebny, aby dane nie przepełniały się. Jest to konieczne, ponieważ kod do-nothing powoli zwiększy / zmniejszy wielkość danych.
Więc w rzeczywistości jest to możliwe, aby zrobić lepiej niż to, jeśli po prostu użyć wszystkich zer i pozbyć się kroku normalizacji. Jednak, ponieważ napisałem benchmark do pomiaru zużycia energii i temperatury, musiałem upewnić się, że flopy są na "prawdziwych" danych, a nie zer - ponieważ jednostki wykonawcze mogą mieć specjalną obsługę przypadków dla zer, które zużywają mniej energii i wytwarzają mniej ciepła.
Więcej Wyników:
- Intel Core i7 920 @ 3.5 GHz
- Windows 7 Ultimate x64
- Visual Studio 2010 SP1-x64 Release
Wątki: 1
Seconds = 72.1116
FP Ops = 960000000000
FLOPs = 1.33127e+010
sum = 2.22652
Teoretyczny szczyt SSE: 4 flops * 3,5 GHz = 14,0 GFlops . Rzeczywista jest 13,3 GFlops .
Wątki: 8
Seconds = 149.576
FP Ops = 7680000000000
FLOPs = 5.13452e+010
sum = 17.8122
Teoretyczny szczyt SSE: 4 flops * 4 rdzenie * 3,5 GHz = 56,0 GFlops. Rzeczywista jest 51,3 GFlops.
mój procesor trafił w 76C na wielowątkowym biegu! Jeśli je uruchomisz, upewnij się, że Dławienie procesora nie wpływa na wyniki.
- 2 x Intel Xeon X5482 Harpertown @ 3.2 GHz
- Ubuntu Linux 10 x64
- GCC 4.5.2 x64 - (- O2 - msse3-fopenmp)
Wątki: 1
Seconds = 78.3357
FP Ops = 960000000000
FLOPs = 1.22549e+10
sum = 2.22652
Teoretyczny szczyt SSE: 4 flops * 3,2 GHz = 12,8 GFlops . Rzeczywista jest 12,3 GFlops .
Wątki: 8
Seconds = 78.4733
FP Ops = 7680000000000
FLOPs = 9.78676e+10
sum = 17.8122
Teoretyczny szczyt SSE: 4 flops * 8 Rdzeni * 3,2 GHz = 102,4 GFlops . Rzeczywista jest 97,9 GFlops.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-16 16:46:41
Jest pewien punkt w architekturze Intela, o którym ludzie często zapominają, porty dyspozytorskie są współdzielone między Int i FP / SIMD. Oznacza to, że uzyskasz tylko pewną ilość impulsów FP/SIMD, zanim logika pętli utworzy pęcherzyki w strumieniu zmiennoprzecinkowym. Mystic miał więcej flopów ze swojego kodu, ponieważ używał dłuższych kroków w swojej rozwiniętej pętli.
Jeśli spojrzeć na Nehalem/Sandy Bridge architektury tutaj http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 to jasne, co się dzieje.
W przeciwieństwie do tego, powinno być łatwiej osiągnąć szczytową wydajność na AMD (Bulldozer), ponieważ rury INT i FP/SIMD mają oddzielne porty emisji z własnym schedulerem.
Jest to tylko teoretyczne, ponieważ nie mam żadnego z tych procesorów do przetestowania.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-06 16:05:20
Gałęzie mogą zdecydowanie powstrzymać cię od utrzymania szczytowej teoretycznej wydajności. Czy widzisz różnicę, jeśli ręcznie rozwijasz pętlę? Na przykład, jeśli umieścisz 5 lub 10 razy więcej ops na iterację pętli:
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;
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-05 18:04:38
Używając Intels icc w wersji 11.1 na 2.4 GHz Intel Core 2 Duo dostaję
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
To jest bardzo bliskie ideałowi 9.6 Gflops.
EDIT:
UPS, patrząc na kod montażu wydaje się, że icc nie tylko wektoryzował mnożenie, ale także wyciągnął dodatki z pętli. W związku z tym, że kod nie jest już wektoryzowany, kod nie jest już wektoryzowany:
Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000
addmul: 0.516 s, 1.938 Gflops, res=1.326463
EDIT2:
Zgodnie z życzeniem:
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
Wewnętrzna pętla kodu clanga wygląda jak to:
.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:
Na koniec dwie sugestie: po pierwsze, Jeśli podoba Ci się tego typu benchmarking, rozważ użycie instrukcji rdtsc
z gettimeofday(2)
. Jest o wiele dokładniejszy i zapewnia czas w cyklach, co zwykle jest tym, co i tak cię interesuje. Dla gcc i znajomych można go zdefiniować w następujący sposób:
#include <stdint.h>
static __inline__ uint64_t rdtsc(void)
{
uint64_t rval;
__asm__ volatile ("rdtsc" : "=A" (rval));
return rval;
}
Po drugie, powinieneś uruchomić swój program porównawczy kilka razy i używać tylko najlepszej wydajności . W nowoczesnych systemach operacyjnych dzieje się wiele równolegle procesor może być w trybie oszczędzania energii o niskiej częstotliwości itp. Wielokrotne uruchamianie programu daje wynik, który jest bliższy idealnemu przypadku.Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-06 15:36:53