Как мне достичь теоретического максимума 4 FLOP за цикл? - PullRequest
604 голосов
/ 05 декабря 2011

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

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

  • My Core i5 750 ( Архитектура Nehalem ) не любит чередование аддов и мул и, кажется, не может запустить обе операции параллельно. Однако, если сгруппировать по 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 ]

491 голосов
/ 06 декабря 2011

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

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

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

Внимание:

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

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

Примечания:

  • Этот код оптимизирован для x64. x86 не имеет достаточно регистров для этого, чтобы хорошо скомпилировать.
  • Этот код был протестирован для правильной работы в Visual Studio 2010/2012 и GCC 4.6.
    ICC 11 (Intel Compiler 11) неожиданно испытывает проблемы с хорошей компиляцией.
  • Это для процессоров до 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*)&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 поток, 10000000 итераций) - Скомпилировано с Visual Studio 2010 SP1 - Выпуск x64:

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

Машина представляет собой Core i7 2600K @ 4,4 ГГц. Теоретический пик SSE составляет 4 флопа * 4,4 ГГц = 17,6 Гфлопс . Этот код достигает 17,3 GFlops - неплохо.

Вывод (8 потоков, 10000000 итераций) - Скомпилировано с Visual Studio 2010 SP1 - Выпуск x64:

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*)&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 поток, 10000000 итераций) - Скомпилировано с Visual Studio 2010 SP1 - Выпуск x64:

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

Теоретический пик AVX составляет 8 флопов * 4,4 ГГц = 35,2 Гфлопс . Фактический 33,4 GFlops .

Вывод (8 потоков, 10000000 итераций) - Скомпилировано с Visual Studio 2010 SP1 - Выпуск x64:

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

Теоретический пик AVX составляет 8 флопов * 4 ядра * 4,4 ГГц = 140,8 Гфлопс. Фактический 138,2 Гфлопс .


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

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

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

Шаг нормализации необходим для предотвращения переполнения / переполнения данных. Это необходимо, так как код бездействия будет медленно увеличивать / уменьшать величину данных.

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


Дополнительные результаты:

  • 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 Гфлопс . Фактический 13,3 GFlops .

Тем: 8

Seconds = 149.576
FP Ops  = 7680000000000
FLOPs   = 5.13452e+010
sum = 17.8122

Теоретический пик SSE: 4 флопа * 4 ядра * 3,5 ГГц = 56,0 ГФлоп . Фактический 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 Гфлопс . Фактический 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 .

31 голосов
/ 06 декабря 2011

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

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

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

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

16 голосов
/ 05 декабря 2011

Филиалы определенно могут удержать вас от поддержания максимальной теоретической производительности.Видите ли вы разницу, если вы выполняете ручное развертывание?Например, если вы добавили в 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;
   }
7 голосов
/ 06 декабря 2011

Использование Intel ICC версии 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 Гфлопс.

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$ 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 вместо gettimeofday(2). Это намного точнее и обеспечивает время в циклах, что обычно в любом случае вас интересует. Для gcc и друзей вы можете определить это так:

#include <stdint.h>

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...