Як досягти теоретичного максимуму 4 FLOP за цикл?

Як теоретична пікова продуктивність чотирьох операцій з плаваючою комою (подвійна точність) за цикл буде досягнута на сучасному процесорі Intel x86-64?

Наскільки я розумію, для більшості сучасних процесорів Intel потрібно три цикли для SSE add і п'ять циклів для mul см., Наприклад, Таблиці інструкцій Agner Fog ' ). Через конвеєрної обробки можна отримати пропускну здатність одного add за цикл, якщо алгоритм має як мінімум три незалежних підсумовування. Так як це вірно для упакованих addpd , а також для скалярних версій addsd , а регістри SSE можуть містити два double , пропускна здатність може досягати двох флопов за цикл.

Крім того, здається (хоча я не бачив ніякої відповідної документації з цього приводу) add і mul можуть виконуватися паралельно, даючи теоретичну максимальну пропускну спроможність чотирьох флопов за цикл.

Однак, я не зміг відтворити цю продуктивність за допомогою простої програми C / С ++. Моя найкраща спроба призвела до 2,7 флоп / циклу. Якщо хтось може внести вклад в просту програму C / С ++ або асемблера, яка демонструє максимальну продуктивність, яка буде дуже вдячна.

Моя спроба:

 #include <stdio.h> #include <stdlib.h> #include <math.h> #include <sys/time.h> double stoptime(void) { struct timeval t; gettimeofday( 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; } away #include <stdio.h> #include <stdlib.h> #include <math.h> #include <sys/time.h> double stoptime(void) { struct timeval t; gettimeofday( 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 ) призведе до подвоєння кількості флопов без зміни часу виконання, і тому я б втратив всього 2.8 флопа за цикл. Чи є простий приклад, який досягає чотирьох флопов за цикл?

Хороша невелика програма Mystical; ось мої результати (біжіть за все на кілька секунд):

  • gcc -O2 -march=nocona : 5.6 Gflops з 10.66 Gflops (2.1 flops / cycle)
  • cl /O2 , openmp видалений: 10.1 Gflops з 10.66 Gflops (3.8 flops / cycle)

Все здається трохи складним, але мої висновки досі:

  • gcc -O2 змінює порядок незалежних операцій з плаваючою комою з мета чергування addpd і mulpd , якщо це можливо. Те ж саме відноситься до gcc-4.6.2 -O2 -march=core2 .

  • gcc -O2 -march=nocona , схоже, зберігає порядок операцій з плаваючою комою, як визначено в джерело С ++.

  • cl /O2 , 64-розрядний компілятор з SDK для Windows 7 робить цикл-розгортання автоматично і, здається, намагається організувати операції так що групи з трьох addpd чергуються з трьома mulpd (ну, принаймні, в моїй системі і для моєї простої програми ).

  • Мій Core i5 750 ( архітектура Наелема ) не подобається чергується add і mul і здається нездатним для паралельної роботи обох операцій. Однак, якщо згруповано в 3, воно раптово працює як магія.

  • Інші архітектури (можливо, Sandy Bridge і інші) з'являються мати можливість виконувати додавання / mul паралельно без проблем якщо вони чергуються в коді збірки.

  • Хоча важко визнати, але в моїй системі cl /O2 набагато краще працює на низькорівневих операціях оптимізації для моєї системи і досягає майже максимальної продуктивності для невеликого прикладу С ++ вище. Я вимірював між 1.85-2.01 flops / cycle (використовували годинник () в Windows, що не так точно. Думаю, вам потрібно використовувати кращий таймер - спасибі Mackie Messer).

  • Найкраще, що мені вдалося за допомогою gcc , - це ручне розгортання і впорядкування доповнення і множення в групах по три. З g++ -O2 -march=nocona addmul_unroll.cpp Я отримую в кращому випадку 0.207s, 4.825 Gflops , що відповідає 1.8 flops / cycle яким я задоволений зараз.

У коді С ++ я замінив цикл 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 ... 
509
05 дек. заданий user1059432 05 дек. 2011-12-05 20:54 '11 о 20:54 2011-12-05 20:54
@ 4 відповідей

Я робив цю точну завдання раніше. Але це було головним чином для вимірювання енергоспоживання та температури процесора. Наступний код (який досить довгий) наближається до оптимального на моєму Core i7 2600K.

Ключовим моментом тут є масивне кількість ручних циклів, а також чергування умножений і додавання ...

Повний проект можна знайти на моєму GitHub: https://github.com/Mysticial/Flops

Увага:

Якщо ви вирішили скомпілювати і запустити це, зверніть увагу на температуру процесора.
Переконайтеся, що ви не перегріваєте його. І переконайтеся, що дросселирование ЦП не впливає на ваші результати!

Крім того, я не несу відповідальності за будь-які збитки, який може виникнути в результаті виконання цього коду.

Примітки:

  • Цей код оптимізований для x64. x86 не має достатньої кількості регістрів для компіляції.
  • Цей код був добре протестований на Visual Studio 2010/2012 і GCC 4.6.
    ICC 11 (Intel Compiler 11) несподівано має проблеми з його компіляцією.
  • Це для процесорів pre-FMA. Щоб досягти пікових FLOPS на процесорах Intel Haswell і AMD Bulldozer (і пізніше), будуть потрібні інструкції 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*) __m128d vONE = _mm_set1_pd(1.0); uint64 c = 0; while (c < iterations){ size_t i = 0; while (i < 1000){ // Here 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*) out += ((double*) 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 Release:

 Seconds = 55.5104 FP Ops = 960000000000 FLOPs = 1.7294e+010 sum = 2.22652 

Апарат Core i7 2600K @ 4.4 ГГц. Теоретичний пік SSE становить 4 флопа * 4.4 ГГц = 17.6 GFlops. Цей код досягає 17.3 GFlops - непогано.

Висновок (8 потоків, ітерацій 10000000) - скомпілювати з Visual Studio 2010 SP1 - x64 Release:

 Seconds = 117.202 FP Ops = 7680000000000 FLOPs = 6.55279e+010 sum = 17.8122 

Теоретичний пік SSE становить 4 флопа * 4 ядра * 4.4 ГГц = 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*) __m256d vONE = _mm256_set1_pd(1.0); uint64 c = 0; while (c < iterations){ size_t i = 0; while (i < 1000){ // Here 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*) out += ((double*) out += ((double*) out += ((double*) 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 Release:

 Seconds = 57.4679 FP Ops = 1920000000000 FLOPs = 3.34099e+010 sum = 4.45305 

Теоретичний пік AVX - 8 флопов * 4.4 ГГц = 35.2 GFlops. Фактично 33.4 GFlops.

Висновок (8 потоків, ітерацій 10000000) - скомпілювати з Visual Studio 2010 SP1 - x64 Release:

 Seconds = 111.119 FP Ops = 15360000000000 FLOPs = 1.3823e+011 sum = 35.6244 

Теоретичний пік AVX - 8 флопов * 4 ядра * 4.4 ГГц = 140,8 GFlops. Фактичний 138.2 GFlops.


Тепер для деяких пояснень:

Критична частина продуктивності - це, мабуть, 48 інструкцій всередині внутрішнього циклу. Ви помітите, що він розбитий на 4 блоки по 12 інструкцій кожен. Кожен з цих 12 блоків інструкцій повністю незалежний один від одного - і приймає в середньому 6 циклів для виконання.

Таким чином, існує 12 інструкцій і 6 циклів між випуском. Затримка множення становить 5 тактів, тому цього достатньо, щоб уникнути латентних ларьків.

Крок нормалізації необхідний для того, щоб дані перевантажували / переповнялися. Це необхідно, оскільки код do-nothing буде повільно збільшувати / зменшувати величину даних.

Таким чином, насправді можна зробити краще, ніж це, якщо ви просто використовуєте всі нулі і рятуєтеся від кроку нормалізації. Однак, оскільки я написав контрольний показник для вимірювання енергоспоживання та температури,, я повинен був переконатися, що флоп були на "реальних" даних, а не нулі, оскільки виконавчі пристрої можуть дуже добре мати спеціальний випадок-обробки для нулів, які споживають менше енергії і виробляють менше тепла.


Додаткові результати:

  • Intel Core i7 920 @ 3.5 ГГц
  • Windows 7 Ultimate x64
  • Visual Studio 2010 SP1 - випуск x64

Теми: 1

 Seconds = 72.1116 FP Ops = 960000000000 FLOPs = 1.33127e+010 sum = 2.22652 

Теоретичний пік SSE: 4 флопа * 3.5 ГГц = 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 ГГц = 56.0 GFlops. Фактично 51.3 GFlops.

Моє процесорний час потрапило на 76C при багатопотоковому запуску! Якщо ви їх використовуєте, переконайтеся, що на результати не впливає дросселирование ЦП.


  • 2 x Intel Xeon X5482 Harpertown @ 3.2 ГГц
  • 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 ГГц = 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 ГГц = 102.4 GFlops. Фактично 97.9 GFlops.

415
05 дек. відповідь дан Mysticial 05 дек. 2011-12-05 23:43 '11 о 23:43 2011-12-05 23:43

В архітектурі Intel, яку люди часто забувають, є точка, порти відправлення поділяються між Int і FP / SIMD. Це означає, що ви отримаєте тільки певну кількість пакетів FP / SIMD до того, як логіка циклу створить бульбашки в потоці з плаваючою точкою. Містик отримав більше провалів зі свого коду, тому що він використовував більш тривалі кроки в своїй розгорнутій петлі.

Якщо ви подивіться на архітектуру Nehalem / Sandy Bridge тут http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937> це абсолютно ясно, що відбувається.

border=0

На відміну від цього, має бути простіше досягти максимальної продуктивності на AMD (Bulldozer), оскільки канали INT і FP / SIMD мають окремі порти проблем з власним планувальником.

Це теоретично, оскільки я не тестую жоден з цих процесорів.

27
06 дек. відповідь дан Patrick Schlüter 06 дек. 2011-12-06 19:05 '11 о 19:05 2011-12-06 19:05

Філії, безумовно, можуть перешкоджати підтриманню максимальної теоретичної продуктивності. Ви бачите різницю, якщо ви вручну виконуєте цикл-розворот? Наприклад, якщо ви помістили в 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; } 
14
05 дек. відповідь дан TJD 05 дек. 2011-12-05 21:04 '11 о 21:04 2011-12-05 21:04

Використовуючи Intels icc Version 11.1 на Intel Core 2 Duo з тактовою частотою 2,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.6 Gflops.

EDIT:

Упс, дивлячись на код збірки, здається, що 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 

EDIT2:

Відповідно із запитом:

 Macintosh:~ mackie$ c> 

Внутрішній цикл коду c>

  .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:

Нарешті, дві пропозиції: по-перше, якщо вам подобається цей тип бенчмаркінгу, подумайте про використання команди rdtsc istead gettimeofday(2) . Він набагато точніше і забезпечує час в циклах, що зазвичай є тим, що вас цікавить. Для gcc і друзів ви можете визначити його наступним чином:

 #include <stdint.h> static __inline__ uint64_t rdtsc(void) { uint64_t rval; __asm__ volatile ("rdtsc" : "=A" (rval)); return rval; } 

По-друге, ви повинні запускати свою тестову програму кілька разів і використовувати тільки кращу продуктивність. В сучасних операційних системах багато що відбувається паралельно, процесор може перебувати в режимі енергозбереження з низькою частотою і т.д. Запуск програми кілька разів дає вам результат, який ближче до ідеального випадку.

6
05 дек. відповідь дан Mackie Messer 05 дек. 2011-12-05 23:19 '11 о 23:19 2011-12-05 23:19

Інші питання по мітках або Задайте питання