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

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

Насколько я понимаю, это займет три цикла дляSSE add и пять циклов дляmul завершить на большинстве современных процессоров Intel (см., например,«Таблицы инструкций» Агнера Фога ). Благодаря конвейерной обработке можно получить пропускную способность одного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) удвоит количество флопов без изменения времени выполнения, поэтому мне не хватит 2,8 флопов за цикл. Есть ли простой пример, который достигает четырех флопов за цикл?

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

gcc -O2 -march=nocona: 5,6 Gflops из 10,66 Gflops (2,1 флоп / цикл)cl /O2openmp удалено: 10,1 Гфлоп из 10,66 Гфлоп (3,8 Флоп / цикл)

Все это кажется немного сложным, но мои выводы пока:

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

gcc -O2 -march=nocona похоже, сохраняет порядок операций с плавающей запятой, как определено в источнике C ++.

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

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

Другие архитектуры (возможноПесчаный мост и другие), по-видимому, могут выполнять 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
...

Ответы на вопрос(4)

Ваш ответ на вопрос