Bagaimana cara mencapai maksimum 4 FLOP secara teoritis per siklus?

642

Bagaimana kinerja puncak teoritis dari 4 operasi floating point (presisi ganda) per siklus dapat dicapai pada CPU Intel x86-64 modern?

Sejauh yang saya mengerti, dibutuhkan tiga siklus untuk SSE add dan lima siklus untuk mulmenyelesaikan sebagian besar CPU Intel modern (lihat misalnya 'Instruction Tables' Agner Fog ). Karena pipelining, seseorang bisa mendapatkan throughput satu addper siklus jika algoritma memiliki setidaknya tiga penjumlahan independen. Karena itu berlaku untuk paket addpdserta addsdversi skalar dan register SSE dapat berisi duadouble , throughputnya dapat sebanyak dua jepit per siklus.

Selain itu, tampaknya (walaupun saya belum melihat dokumentasi yang tepat tentang ini) add dan muldapat dieksekusi secara paralel memberikan throughput maks teoretis empat jepit per siklus.

Namun, saya belum bisa meniru kinerja itu dengan program C / C ++ sederhana. Upaya terbaik saya menghasilkan sekitar 2,7 jepit / siklus. Kalau ada yang bisa berkontribusi C / C ++ atau program assembler sederhana yang menunjukkan kinerja puncak yang akan sangat dihargai.

Usaha saya:

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

Disusun dengan

g++ -O2 -march=native addmul.cpp ; ./a.out 1000

menghasilkan output berikut pada Intel Core i5-750, 2,66 GHz.

addmul:  0.270 s, 3.707 Gflops, res=1.326463

Artinya, hanya sekitar 1,4 jepit per siklus. Melihat kode assembler dengan g++ -S -O2 -march=native -masm=intel addmul.cpploop utama tampaknya agak optimal bagi saya:

.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

Mengubah versi skalar dengan versi paket (addpd dan mulpd) akan menggandakan jumlah kegagalan tanpa mengubah waktu eksekusi dan jadi saya akan mendapatkan 2,8 flop per siklus. Apakah ada contoh sederhana yang mencapai empat jepit per siklus?

Program kecil yang bagus oleh Mysticial; inilah hasil saya (jalankan hanya untuk beberapa detik saja):

  • gcc -O2 -march=nocona: 5.6 Gflops dari 10.66 Gflops (2.1 flops / cycle)
  • cl /O2, openmp dihapus: 10.1 Gflops dari 10.66 Gflops (3.8 flops / cycle)

Itu semua tampaknya agak rumit, tetapi kesimpulan saya sejauh ini:

  • gcc -O2mengubah urutan operasi floating point independen dengan tujuan bergantian addpddan mulpdjika memungkinkan. Hal yang sama berlaku untuk gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona tampaknya menjaga urutan operasi floating point sebagaimana didefinisikan dalam sumber C ++.

  • cl /O2, kompiler 64-bit dari SDK untuk Windows 7 melakukan loop-unrolling secara otomatis dan tampaknya mencoba dan mengatur operasi sehingga kelompok-kelompok addpdbergantian tiga dengan tiga mulpd(baik, setidaknya pada sistem saya dan untuk program sederhana saya) .

  • Saya Core i5 750 ( Nehalem arsitektur ) tidak seperti bolak add dan ini mul dan tampaknya tidak dapat menjalankan kedua operasi secara paralel. Namun, jika dikelompokkan dalam 3 itu tiba-tiba berfungsi seperti sihir.

  • Arsitektur lain (mungkin Sandy Bridge dan lainnya) tampaknya mampu mengeksekusi add / mul secara paralel tanpa masalah jika mereka berganti dalam kode assembly.

  • Meskipun sulit untuk diakui, tetapi pada sistem saya cl /O2melakukan pekerjaan yang jauh lebih baik pada operasi optimalisasi tingkat rendah untuk sistem saya dan mencapai kinerja puncak mendekati untuk contoh C ++ kecil di atas. Saya mengukur antara 1,85-2,01 jepit / siklus (telah menggunakan jam () pada Windows yang tidak tepat. Saya kira, perlu menggunakan timer yang lebih baik - terima kasih Mackie Messer).

  • Yang terbaik yang saya kelola gccadalah secara manual membuka gulungan dan mengatur penambahan dan perkalian dalam kelompok tiga. Dengan g++ -O2 -march=nocona addmul_unroll.cpp saya mendapatkan yang terbaik 0.207s, 4.825 Gflopsyang sesuai dengan 1,8 jepit / siklus yang saya cukup senang dengan sekarang.

Dalam kode C ++ saya telah mengganti forloop dengan

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

Dan perakitan sekarang terlihat seperti

.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
...
pengguna1059432
sumber
15
Mengandalkan waktu jam dinding mungkin merupakan bagian dari penyebabnya. Dengan asumsi Anda menjalankan ini di dalam OS seperti Linux, itu bebas untuk menjadwalkan proses Anda kapan saja. Kejadian eksternal semacam itu dapat memengaruhi pengukuran kinerja Anda.
tdenniston
Apa versi GCC Anda? Jika Anda menggunakan mac menggunakan default, Anda akan mengalami masalah (ini adalah 4.2 lama).
semisight
2
Ya menjalankan Linux tetapi tidak ada beban pada sistem dan mengulanginya berkali-kali membuat sedikit perbedaan (misalnya berkisar 4.0-4.2 Gflops untuk versi skalar, tetapi sekarang dengan -funroll-loops). Sudah mencoba dengan versi gcc 4.4.1 dan 4.6.2, tetapi output asm terlihat ok?
user1059432
Apakah Anda mencoba -O3untuk gcc, yang memungkinkan -ftree-vectorize? Mungkin dikombinasikan dengan -funroll-loopsmeskipun saya tidak tidak jika itu benar-benar diperlukan. Setelah semua perbandingan memang tampak tidak adil jika salah satu kompiler melakukan vektorisasi / membuka gulungan, sementara yang lain tidak karena itu tidak bisa, tetapi karena itu diberitahu tidak juga.
Grizzly
4
@Grizzly -funroll-loopsmungkin sesuatu untuk dicoba. Tapi saya pikir -ftree-vectorizeitu intinya. OP sedang mencoba hanya untuk mempertahankan 1 mul + 1 menambahkan instruksi / siklus. Instruksi dapat berupa skalar atau vektor - tidak masalah karena latensi dan throughputnya sama. Jadi jika Anda dapat mempertahankan 2 / siklus dengan skalar SSE, maka Anda dapat menggantinya dengan vektor SSE dan Anda akan mencapai 4 jepit / siklus. Dalam jawaban saya, saya melakukan hal itu dari SSE -> AVX. Saya mengganti semua SSE dengan AVX - latensi yang sama, throughput yang sama, 2x jepit.
Mysticial

Jawaban:

517

Saya telah melakukan tugas yang tepat ini sebelumnya. Tapi itu terutama untuk mengukur konsumsi daya dan suhu CPU. Kode berikut (yang cukup panjang) mencapai hampir optimal pada Core i7 2600K saya.

Hal utama yang perlu diperhatikan di sini adalah banyaknya loop-unrolling manual serta interleaving dari multiplies dan menambahkan ...

Proyek lengkap dapat ditemukan di GitHub saya: https://github.com/Mysticial/Flops

Peringatan:

Jika Anda memutuskan untuk mengompilasi dan menjalankan ini, perhatikan suhu CPU Anda !!!
Pastikan Anda tidak kepanasan. Dan pastikan pelambatan CPU tidak memengaruhi hasil Anda!

Selain itu, saya tidak bertanggung jawab atas kerusakan apa pun yang mungkin terjadi akibat menjalankan kode ini.

Catatan:

  • Kode ini dioptimalkan untuk x64. x86 tidak memiliki cukup register untuk dikompilasi dengan baik.
  • Kode ini telah diuji untuk berfungsi dengan baik pada Visual Studio 2010/2012 dan GCC 4.6.
    ICC 11 (Intel Compiler 11) secara mengejutkan mengalami kesulitan menyusunnya dengan baik.
  • Ini untuk prosesor pra-FMA. Untuk mencapai FLOPS puncak pada prosesor Intel Haswell dan AMD Bulldozer (dan yang lebih baru), diperlukan instruksi FMA (Fused Multiply Add). Ini berada di luar cakupan tolok ukur ini.

#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");
}

Output (1 utas, 10000000 iterations) - Dikompilasi dengan Visual Studio 2010 SP1 - x64 Rilis:

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

Mesin tersebut adalah Core i7 2600K @ 4.4 GHz. Puncak SSE teoretis adalah 4 jepit * 4,4 GHz = 17,6 GFlops . Kode ini mencapai 17,3 GFlops - tidak buruk.

Keluaran (8 utas, 10.000.000 iterasi) - Dikompilasi dengan Visual Studio 2010 SP1 - x64 Rilis:

Seconds = 117.202
FP Ops  = 7680000000000
FLOPs   = 6.55279e+010
sum = 17.8122

Puncak SSE teoretis adalah 4 jepit * 4 core * 4,4 GHz = 70,4 GFlops. Sebenarnya adalah 65,5 GFlops .


Mari kita selangkah lebih maju. 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");
}

Output (1 utas, 10000000 iterations) - Dikompilasi dengan Visual Studio 2010 SP1 - x64 Rilis:

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

Puncak AVX teoritis adalah 8 jepit * 4,4 GHz = 35,2 GFlops . Sebenarnya adalah 33,4 GFlops .

Keluaran (8 utas, 10.000.000 iterasi) - Dikompilasi dengan Visual Studio 2010 SP1 - x64 Rilis:

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

Puncak AVX teoritis adalah 8 jepit * 4 core * 4,4 GHz = 140,8 GFlops. Sebenarnya adalah 138,2 GFlops .


Sekarang untuk beberapa penjelasan:

Bagian kritis kinerja jelas adalah 48 instruksi di dalam lingkaran dalam. Anda akan melihat bahwa itu dibagi menjadi 4 blok yang terdiri dari 12 instruksi masing-masing. Masing-masing dari 12 blok instruksi ini sepenuhnya independen satu sama lain - dan mengambil rata-rata 6 siklus untuk dieksekusi.

Jadi ada 12 instruksi dan 6 siklus antara masalah-untuk-digunakan. Latensi multiplikasi adalah 5 siklus, jadi cukup untuk menghindari warung latensi.

Langkah normalisasi diperlukan untuk menjaga data dari over / underflow. Ini diperlukan karena kode tidak melakukan apa pun akan secara perlahan meningkatkan / mengurangi besarnya data.

Jadi sebenarnya mungkin untuk melakukan yang lebih baik dari ini jika Anda hanya menggunakan semua nol dan menyingkirkan langkah normalisasi. Namun, karena saya menulis patokan untuk mengukur konsumsi daya dan suhu, saya harus memastikan jepit berada pada data "nyata", bukan nol - karena unit eksekusi mungkin memiliki penanganan kasus khusus untuk nol yang menggunakan lebih sedikit daya dan menghasilkan lebih sedikit panas.


Hasil lebih:

  • Intel Core i7 920 @ 3.5 GHz
  • Windows 7 Ultimate x64
  • Visual Studio 2010 SP1 - Rilis x64

Thread: 1

Seconds = 72.1116
FP Ops  = 960000000000
FLOPs   = 1.33127e+010
sum = 2.22652

Puncak SSE Teoretis: 4 jepit * 3,5 GHz = 14,0 GFlops . Sebenarnya adalah 13,3 GFlops .

Thread: 8

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

Puncak SSE Teoretis: 4 jepit * 4 core * 3,5 GHz = 56,0 GFlops . Sebenarnya adalah 51,3 GFlops .

Prosesor saya mencapai 76C saat proses multi-ulir! Jika Anda menjalankan ini, pastikan hasilnya tidak terpengaruh oleh pelambatan CPU.


  • 2 x Intel Xeon X5482 Harpertown @ 3.2 GHz
  • Ubuntu Linux 10 x64
  • GCC 4.5.2 x64 - (-O2 -msse3 -fopenmp)

Thread: 1

Seconds = 78.3357
FP Ops  = 960000000000
FLOPs   = 1.22549e+10
sum = 2.22652

Puncak SSE Teoretis: 4 jepit * 3,2 GHz = 12,8 GFlops . Sebenarnya adalah 12,3 GFlops .

Thread: 8

Seconds = 78.4733
FP Ops  = 7680000000000
FLOPs   = 9.78676e+10
sum = 17.8122

Puncak SSE Teoretis: 4 jepit * 8 core * 3,2 GHz = 102,4 GFlops . Sebenarnya adalah 97,9 GFlops .

Mistikal
sumber
13
Hasil Anda sangat mengesankan. Saya telah mengkompilasi kode Anda dengan g ++ pada sistem saya yang lebih lama tetapi tidak mendapatkan hasil yang hampir sama baiknya: iterasi 100k, 1.814s, 5.292 Gflops, sum=0.448883dari puncak 10,68 Gflop atau hanya kurang dari 2,0 jepit per siklus. Tampaknya add/ multidak dieksekusi secara paralel. Ketika saya mengubah kode Anda dan selalu menambah / mengalikan dengan register yang sama, katakan rC, tiba-tiba mencapai puncaknya: 0.953s, 10.068 Gflops, sum=0atau 3,8 flop / cycle. Sangat aneh.
user1059432
11
Ya, karena saya tidak menggunakan perakitan inline, kinerjanya memang sangat sensitif terhadap kompiler. Kode yang saya miliki di sini telah disetel untuk VC2010. Dan jika saya ingat dengan benar, Intel Compiler memberikan hasil yang sama baiknya. Seperti yang telah Anda perhatikan, Anda mungkin harus sedikit men-tweak untuk membuatnya dapat dikompilasi dengan baik.
Mysticial
8
Saya dapat mengkonfirmasi hasil Anda pada Windows 7 menggunakan cl /O2(64-bit dari windows SDK) dan bahkan contoh saya berjalan mendekati puncak untuk operasi skalar (1,9 gagal / siklus) di sana. Compiler loop-unrolls dan reorder tetapi itu mungkin bukan alasan untuk melihat lebih dalam lagi. Mencekik bukan masalah Saya baik-baik saja pada cpu saya dan menjaga iterasi pada 100k. :)
user1059432
6
@Mysticial: Ini muncul di r / coding subreddit hari ini.
greyfade
2
@haylem Ini meleleh atau lepas landas. Tidak pernah keduanya. Jika ada cukup pendinginan, itu akan mendapatkan airtime. Kalau tidak, itu hanya meleleh. :)
Mysticial
33

Ada satu titik dalam arsitektur Intel yang orang sering lupa, port pengiriman dibagi antara Int dan FP / SIMD. Ini berarti bahwa Anda hanya akan mendapatkan jumlah tertentu dari FP / SIMD sebelum logika loop akan membuat gelembung di aliran titik mengambang Anda. Mystical mendapat lebih banyak jepit dari kodenya, karena dia menggunakan langkah yang lebih panjang dalam loop yang tidak terbuka.

Jika Anda melihat arsitektur Nehalem / Sandy Bridge di sini http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 cukup jelas apa yang terjadi.

Sebaliknya, akan lebih mudah untuk mencapai kinerja puncak pada AMD (Bulldozer) karena pipa INT dan FP / SIMD memiliki port masalah terpisah dengan penjadwal mereka sendiri.

Ini hanya teoretis karena saya tidak memiliki satupun dari prosesor ini untuk diuji.

Patrick Schlüter
sumber
2
Hanya ada tiga instruksi loop overhead: inc, cmp, dan jl. Semua ini dapat pergi ke port # 5 dan tidak mengganggu dengan vektor faddatau fmul. Saya lebih suka curiga bahwa decoder (kadang-kadang) masuk ke jalan. Perlu mempertahankan antara dua dan tiga instruksi per siklus. Saya tidak ingat batasan yang pasti, tetapi panjang instruksi, awalan, dan penyelarasan ikut berperan.
Mackie Messer
cmpdan jltentu saja pergi ke port 5, inctidak begitu yakin karena selalu berkelompok dengan 2 lainnya. Tapi Anda benar, sulit untuk mengatakan di mana kemacetan dan decoder juga bisa menjadi bagian dari itu.
Patrick Schlüter
3
Saya bermain-main sedikit dengan loop dasar: pemesanan instruksi tidak masalah. Beberapa pengaturan mengambil 13 siklus, bukan minimal 5 siklus. Saatnya untuk melihat penghitung acara pertunjukan, kurasa ...
Mackie Messer
16

Cabang pasti dapat mencegah Anda mempertahankan kinerja teoretis puncak. Apakah Anda melihat perbedaan jika Anda melakukan loop-unrolling secara manual? Misalnya, jika Anda menempatkan 5 atau 10 kali lebih banyak ops per loop iterasi:

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;
   }
TJD
sumber
4
Saya mungkin salah, tapi saya percaya g ++ dengan -O2 akan mencoba untuk melepaskan loop secara otomatis (saya pikir ini menggunakan Duff's Device).
Weaver
6
Ya, terima kasih memang agak membaik. Saya sekarang mendapatkan sekitar 4,1-4,3 Gflops, atau 1,55 jepit per siklus. Dan tidak, dalam contoh ini -O2 tidak mengulang membuka gulungan.
user1059432
1
Weaver benar tentang loop membuka gulungan, saya percaya. Jadi membuka gulungan secara manual mungkin tidak perlu
jim mcnamara
5
Lihat output perakitan di atas, tidak ada tanda-tanda loop terbuka.
user1059432
14
Buka gulungan otomatis juga meningkatkan rata-rata 4,2 Gflop, tetapi membutuhkan -funroll-loopsopsi yang bahkan tidak termasuk dalam -O3. Lihat g++ -c -Q -O2 --help=optimizers | grep unroll.
user1059432
7

Menggunakan Intels icc Versi 11.1 pada 2.4GHz Intel Core 2 Duo saya dapatkan

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 

Itu sangat dekat dengan 9,6 Gflops ideal.

EDIT:

Ups, sambil melihat kode rakitan, tampaknya icc tidak hanya membuat vektor perkalian, tetapi juga menarik penambahan dari loop. Memaksa semantik yang lebih ketat kode tidak lagi vektor:

Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000
addmul:  0.516 s, 1.938 Gflops, res=1.326463

EDIT2:

Seperti yang diminta:

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

Lingkaran dalam kode dentang terlihat seperti ini:

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

Akhirnya, dua saran: Pertama, jika Anda menyukai jenis tolok ukur ini, pertimbangkan untuk menggunakan rdtscinstruksi istead dari gettimeofday(2). Ini jauh lebih akurat dan memberikan waktu dalam siklus, yang biasanya merupakan hal yang Anda minati. Untuk gcc dan teman, Anda dapat mendefinisikannya seperti ini:

#include <stdint.h>

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

Kedua, Anda harus menjalankan program benchmark beberapa kali dan menggunakan kinerja terbaik saja . Dalam sistem operasi modern banyak hal terjadi secara paralel, cpu mungkin berada dalam mode hemat daya frekuensi rendah, dll. Menjalankan program berulang kali memberi Anda hasil yang lebih dekat ke kasus ideal.

Mackie Messer
sumber
2
dan seperti apa pembongkaran itu?
Bahbar
1
Menarik, itu kurang dari 1 kegagalan / siklus. Apakah kompiler mencampur addsd's dan mulsd' atau apakah mereka dalam kelompok seperti pada output perakitan saya? Saya juga mendapatkan hanya sekitar 1 gagal / siklus ketika kompiler mencampurnya (yang saya dapatkan tanpa -march=native). Bagaimana perubahan kinerja jika Anda menambahkan garis add=mul;di awal fungsi addmul(...)?
user1059432
1
@ user1059432: Instruksi addsddan subsdmemang dicampur dalam versi yang tepat. Saya mencoba dentang 3.0 juga, itu tidak mencampur instruksi dan ia datang sangat dekat dengan 2 jepit / siklus pada core 2 duo. Ketika saya menjalankan kode yang sama pada laptop saya core i5, pencampuran kode tidak ada bedanya. Saya mendapatkan sekitar 3 jepit / siklus dalam kedua kasus.
Mackie Messer
1
@ user1059432: Pada akhirnya ini semua tentang menipu kompiler untuk menghasilkan kode "bermakna" untuk tolok ukur sintetis. Ini lebih sulit daripada yang terlihat pada pandangan pertama. (yaitu icc mengakali benchmark Anda) Jika semua yang Anda inginkan adalah menjalankan beberapa kode pada 4 flops / cycle, hal yang paling mudah adalah menulis loop perakitan kecil. Apalagi sakit kepala. :-)
Mackie Messer
1
Ok, jadi Anda mendekati 2 jepit / siklus dengan kode perakitan yang mirip dengan yang saya kutip di atas? Seberapa dekat dengan 2? Saya hanya mendapat 1,4, jadi itu penting. Saya tidak berpikir Anda mendapatkan 3 jepit / siklus di laptop Anda kecuali jika kompiler melakukan optimisasi seperti yang Anda lihat iccsebelumnya, dapatkah Anda memeriksa perakitannya?
user1059432