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


642

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

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

Мила маленька програма Mysticial; ось мої результати (запустити всього кілька секунд):

  • gcc -O2 -march=nocona: 5,6 Gflops з 10,66 Gflops (2,1 флопа / цикл)
  • cl /O2, openmp видалено: 10,1 Gflops з 10,66 Gflops (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 ) не любить чергування добавок та мулів і, здається, не може паралельно виконувати обидві операції. Однак якщо їх згрупувати в 3-х, це раптом працює як магія.

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

  • Хоча це важко визнати, але в моїй системі cl /O2робиться набагато краща робота при оптимізації низьких рівнів для моєї системи та досягає максимальної продуктивності для маленького прикладу С ++ вище. Я виміряв між 1,85-2,01 флопами / циклом (використовував годинник () у 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
...

15
Покладання часу на стінні годинники, ймовірно, є причиною. Якщо припустити, що ви працюєте в такій ОС, як Linux, то ви можете будь-коли планувати процес у будь-який час. Така зовнішня подія може вплинути на ваші показники продуктивності.
tdenniston

Яка ваша версія GCC? Якщо ви користуєтеся комп'ютером Mac, використовуючи типовий режим, ви зіткнетеся з проблемами (це старий 4.2).
півмісяця

2
Так, запуск Linux, але немає навантаження на систему, і повторення її багато разів робить незначні відмінності (наприклад, діапазони 4,0-4,2 Gflops для скалярної версії, але тепер з -funroll-loops). Пробували з версіями gcc 4.4.1 та 4.6.2, але вихід ASM виглядає нормально?
user1059432

Ви спробували -O3для gcc, який дозволяє -ftree-vectorize? Можливо, поєднуються з тим, -funroll-loopsщо я цього не роблю, якщо це дійсно необхідно. Якщо взагалі порівняння не виглядає несправедливим, якщо один із компіляторів робить векторизацію / розгортання, а інший - не тому, що не може, а тому, що йому сказано не надто.
Грізлі

4
@Grizzly -funroll-loops- це, мабуть, щось спробувати. Але я думаю, що -ftree-vectorizeце крім суті. ОП намагається просто підтримувати 1 мкл + 1 додавати інструкцію / цикл. Інструкції можуть бути скалярними або векторними - це не має значення, оскільки затримка та пропускна здатність однакові. Отже, якщо ви можете підтримувати 2 / цикл скалярними SSE, то ви можете замінити їх на векторні SSE і ви досягнете 4 флопів / цикл. У своїй відповіді я робив саме те, що йшов від SSE -> AVX. Я замінив всі SSE на AVX - ті ж затримки, однакові пропускні здатності, 2х флопи.
Містичний

Відповіді:


517

Я робив це точне завдання раніше. Але це було головним чином для вимірювання енергоспоживання та температури процесора. Наведений нижче код (який досить довгий) досягає оптимального для мого 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 GFlops . Цей код досягає 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 GFlops . Фактична - 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 GFlops. Фактична - 138,2 GFlops .


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

Критична частина продуктивності - це, очевидно, 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 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 .

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


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


13
Ваші результати дуже вражають. Я склав ваш код з g ++ у моїй старшій системі, але не отримав майже таких хороших результатів: 100 к ітерацій, 1.814s, 5.292 Gflops, sum=0.448883з пікових 10,68 Gflops або не вистачає 2,0 флопів за цикл. Здається add/ mulне виконуються паралельно. Коли я змінюю ваш код і завжди додаю / помножую один і той же регістр, скажімо rC, він раптом досягає майже піку: 0.953s, 10.068 Gflops, sum=0або 3,8 флопа / цикл. Дуже дивно.
user1059432

11
Так, оскільки я не використовую вбудовану збірку, продуктивність дійсно дуже чутлива до компілятора. Код, який я тут налаштував, налаштований на VC2010. І якщо я пригадую правильно, Intel Compiler дає такі ж хороші результати. Як ви вже помітили, вам, можливо, доведеться трохи підправити його, щоб добре скласти.
Містичне

8
Я можу підтвердити ваші результати в Windows 7, використовуючи cl /O2(64-розрядні з Windows sdk), і навіть мій приклад працює близько до піку для скалярних операцій (1,9 флопа / цикл) там. Цикл компілятора розкручується та змінюється впорядкування, але це може бути причиною необхідності детальніше розглянути це. Закидання не проблема Я приємний своєму процесору і підтримую ітерації в 100k. :)
користувач1059432

6
@Mysticial: Він з’явився на subreddit r / кодування сьогодні.
greyfade

2
@haylem Він або плавиться, або злітає. Ніколи і те й інше. Якщо достатньо охолодження, він отримає ефірний час. В іншому випадку він просто плавиться. :)
Містичний

33

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

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

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

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


2
Є тільки три інструкції накладних петель: inc, cmpі jl. Усі вони можуть перейти до порту №5 і не заважати ні векторизованому, faddні fmul. Я б скоріше підозрював, що декодер (іноді) заважає. Він повинен підтримувати між двома та трьома інструкціями за цикл. Я не пам’ятаю точних обмежень, але довжина інструкцій, префікси та вирівнювання все-таки грають.
Маккі Мессер

cmpі, jlзвичайно, переходьте до порту 5, incне настільки впевнений, оскільки він завжди йде в групі з двома іншими. Але ви праві, важко сказати, де знаходиться вузьке місце і декодери також можуть бути його частиною.
Патрік Шлютер

3
Я трохи погрався з базовим циклом: впорядкування інструкцій має значення. Деякі домовленості займають 13 циклів замість мінімальних 5 циклів. Час подивитися на лічильники подій, які я думаю ...
Mackie Messer

16

Галузі точно можуть уберегти вас від збереження пікових теоретичних показників. Ви бачите різницю, якщо вручну робите певну розмотування циклу? Наприклад, якщо ви помітите в 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;
   }

4
Можливо, я помиляюся, але я вважаю, що g ++ за допомогою -O2 спробує автоматично розкрутити цикл (я думаю, він використовує пристрій Даффа).
Вівер

6
Так, завдяки дійсно дещо покращується. Зараз я отримую приблизно 4,1-4,3 Gflops, або 1,55 флопів за цикл. І ні, в цьому прикладі -O2 не циклічно розкручувався.
user1059432

1
Вір вважає правильним щодо розгортання циклу, я вважаю. Тому розгортання вручну, мабуть, не потрібно
jim mcnamara

5
Дивіться висновок збірки вище, немає ознак розкручування циклу.
user1059432

14
Автоматична розгортання також покращує в середньому до 4,2 Gflops, але вимагає -funroll-loopsопції, яка навіть не включена -O3. Див g++ -c -Q -O2 --help=optimizers | grep unroll.
user1059432

7

Використовуючи Intel icc версії 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

Внутрішня петля коду Кланг виглядає так:

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

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


2
і як виглядає розбирання?
Бахбар

1
Цікаво, що це менше 1 флопа / циклу. Чи компілює компілятор addsdmulsd', або вони в групах, як у моїй збірці? Я також отримую приблизно 1 флоп / цикл, коли компілятор змішує їх (без чого я отримую -march=native). Як змінюється продуктивність, якщо додати рядок add=mul;на початку функції addmul(...)?
user1059432

1
@ User1059432: addsdі subsdінструкції дійсно змішані в точної версії. Я також спробував clang 3.0, він не змішує інструкції, і він дуже близький до 2 флопів / циклу на core 2 дует. Коли я запускаю один і той же код на своєму i5 ноутбука, змішування коду не має ніякої різниці. У будь-якому випадку я отримую близько 3 флопів / циклів.
Маккі Мессер

1
@ user1059432: Врешті-решт, справа в тому, щоб обмацувати компілятор у створенні "значущого" коду для синтетичного еталону. Це важче, ніж здається на перший погляд. (тобто icc перекреслює ваш орієнтир) Якщо все, що вам потрібно, це запустити якийсь код у 4 флопи / цикл, найпростіше - написати невеликий цикл складання. Набагато менше головного болю. :-)
Маккі Мессер

1
Гаразд, ви наближаєтесь до 2 флопів / циклів з кодом складання, подібним до того, що я цитував вище? Як близько до 2? Я отримую лише 1,4, так що це важливо. Я не думаю, що у вас на ноутбуці є три флопи / цикли, якщо компілятор не робить оптимізації, як ви бачили iccраніше, чи можете ви двічі перевірити збірку?
user1059432
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.