Как достичь теоретического максимума 4 FLOP за цикл?

Как можно достичь теоретической пиковой производительности 4 операций с плавающей точкой (двойная точность) за цикл на современном процессоре Intel x86-64?

Насколько я понимаю, для добавления SSE add три цикла и пять циклов для завершения работы mul на большинстве современных процессоров Intel (см., Например , «Таблицы инструкций Agner Fog» ). Из-за конвейерной обработки можно получить пропускную способность одного add за цикл, если алгоритм имеет как минимум три независимых суммирования. Так как это верно для упакованных addpd а также для скалярных версий addsd а регистры SSE могут содержать два double , пропускная способность может достигать двух флопов за цикл.

Более того, кажется, что (хотя я не видел никакой надлежащей документации по этому вопросу), add «s и mul » может выполняться параллельно, давая теоретическую максимальную пропускную способность четырех флопов за цикл.

Тем не менее, я не смог воспроизвести эту производительность с помощью простой программы на C / C ++. Моя лучшая попытка привела к 2,7 флопам / циклу. Если кто-то может внести вклад в простую программу C / C ++ или ассемблера, которая демонстрирует максимальную производительность, которая была бы весьма признательна.

Моя попытка:

 #include  #include  #include  #include  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 \n", argv[0]); printf("number of operations:  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 ГГц.

 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 ) mulpd бы количество mulpd не изменяя время выполнения, и поэтому я бы mulpd всего 2.8 флопа за такт. Есть ли простой пример, который обеспечивает четыре флопа за цикл?

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

  • gcc -O2 -march=nocona : 5.6 gcc -O2 -march=nocona из 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 похоже, сохраняет порядок операций с плавающей запятой, как определено в источнике C ++.

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

  • Мой Core i5 750 ( архитектура Nahelem ) не похож на переменные add и mul и, похоже, не может выполнять обе операции параллельно. Однако, если сгруппировано в 3, это внезапно срабатывает подобно магии.

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

  • Хотя трудно признать, но в моей системе cl /O2 делает намного лучшую работу в низкоуровневых операциях оптимизации для моей системы и достигает близкой к максимальной производительности для небольшого примера C ++ выше. Я измерил между 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, до которого я доволен.

В коде 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; } 

И теперь assembly выглядит как

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

Раньше я выполнял эту задачу. Но это было главным образом для измерения энергопотребления и температуры процессора. Следующий код (который довольно длинный) приближается к оптимальному на моем Core i7 2600K.

Ключевым моментом здесь является массивное количество ручных циклов, а также чередование множителей и добавление …

Полный проект можно найти на моем GitHub: https://github.com/Mysticial/Flops

Предупреждение:

Если вы решили скомпилировать и запустить это, обратите внимание на температуру процессора!
Убедитесь, что вы не перегреваете его. И убедитесь, что дросселирование CPU не влияет на ваши результаты!

Кроме того, я не несу ответственности за любой ущерб, который может возникнуть в результате запуска этого кода.

Заметки:

  • Этот код оптимизирован для 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). Это выходит за frameworks этого теста.
 #include  #include  #include  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 stream, итерации 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 streamов, итераций 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  #include  #include  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 stream, итерации 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 streamов, итераций 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 Гфлопс. Фактический - 138.2 GFlops .


Теперь для некоторых объяснений:

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

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

Для того, чтобы данные переполнялись / переполнялись, необходим этап нормализации. Это необходимо, так как код do-nothing будет медленно увеличивать / уменьшать величину данных.

Таким образом, на самом деле возможно сделать лучше, чем это, если вы просто используете все нули и избавляетесь от шага нормализации. Однако, поскольку я написал контрольный показатель для измерения энергопотребления и температуры, я должен был убедиться, что флопы были на «реальных» данных, а не в нулях - поскольку исполнительные устройства могут очень хорошо иметь специальную обработку регистра для нhive, которые используют меньшую мощность и производить меньше тепла.


Больше результатов:

  • Intel Core i7 920 @ 3,5 ГГц
  • Windows 7 Ultimate x64
  • Visual Studio 2010 с пакетом обновления 1 (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 Гфлопс . Фактически - 97,9 GFlops .

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

Если вы посмотрите на архитектуру Nehalem / Sandy Bridge здесь http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6, то совершенно ясно, что происходит.

В отличие от этого, должно быть проще достичь максимальной производительности на AMD (Bulldozer), поскольку каналы INT и FP / SIMD имеют отдельные порты проблем с собственным планировщиком.

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

Филиалы, безусловно, могут препятствовать поддержанию максимальной теоретической производительности. Вы видите разницу, если вы вручную выполняете цикл? Например, если вы поместили в 5 или 10 раз больше опций на каждую итерацию цикла:

 for(int i=0; i 

Используя Intels icc Version 11.1 на 2.4 ГГц Intel Core 2 Duo, я получаю

 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 

EDIT2:

Как просили:

 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 

EDIT3:

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

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

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

  • Big O, как вы его вычисляете / приближаете?
  • ограниченная оптимизация в R
  • Петля с нулевым временем выполнения
  • Как определить неиспользуемые определения css
  • Приращение: x ++ vs x + = 1
  • Почему Java API использует int вместо короткого или байтового?
  • Давайте будем гением компьютера.