Как достичь теоретической пиковой производительности 4 операций с плавающей запятой (двойной точности) за такт на современном процессоре Intel x86-64?
 Насколько я понимаю, это займет три цикла для SSE add и пять циклов для mul, чтобы закончить на большинстве современных процессоров Intel (смотри, например, "Инструкция Таблицы" Agner FOG). Благодаря конвейерной обработке можно получить пропускную способность, равную одному add за цикл, если алгоритм имеет как минимум три независимых суммирования. Так как это верно для упакованного addpd а также для скалярных версий addsd и регистров SSE может содержать два double пропускная способность может достигать двух флопов за цикл.
 Кроме того, кажется (хотя я не видел надлежащей документации по этому вопросу) add и mul могут выполняться параллельно, давая теоретическую максимальную пропускную способность в четыре флопа за такт.
Однако я не смог воспроизвести эту производительность с помощью простой программы на 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
 выдает следующий вывод на 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 без изменения времени выполнения, и поэтому мне хватило бы лишь 2,8 флопов за цикл. Есть ли простой пример, который достигает четырех флопов за цикл?
Хорошая маленькая программа от Mysticial; Вот мои результаты (хотя бы на несколько секунд):
-  
gcc -O2 -march=nocona: 5,6 Гфлопс из 10,66 Гфлопс (2,1 флопс/цикл) -  
cl/O2, openmp удалено: 10,1 Гфлоп из 10,66 Гфлоп (3,8 Флоп/цикл) 
Все это кажется немного сложным, но мои выводы пока:
-  
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 (архитектура Nehalem) не любит чередование add и mul и, по-видимому, не может выполнять обе операции параллельно. Однако, если сгруппировать в 3, это внезапно работает как волшебство.
 -  
Другие архитектуры (возможно, Sandy Bridge и другие), по-видимому, могут выполнять add/mul параллельно без проблем, если они чередуются в коде сборки.
 -  
Хотя это трудно признать, но в моей системе
cl/O2намного лучше справляется с низкоуровневыми операциями оптимизации для моей системы и достигает почти максимальной производительности для небольшого примера C++ выше. Я измерял между 1,85-2,01 флопс/цикл (использовал clock() в Windows, что не так точно. Я думаю, нужно использовать лучший таймер - спасибо Mackie Messer). -  
Лучшее, что мне удалось сделать с помощью
gcc- это вручную развернуть цикл и развернуть сложения и умножения в группах по три. Сg++ -O2 -march=nocona addmul_unroll.cppя получаю в лучшем случае0.207s, 4.825 Gflopsчто соответствует 1,8 флопс/цикл, что меня вполне устраивает сейчас. 
 В коде 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;
   }
 И сборка теперь выглядит так
.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
...