Çevrim başına teorik maksimum 4 FLOP'u nasıl elde edebilirim?


642

Modern x86-64 Intel CPU'da çevrim başına 4 kayan nokta işleminin (çift kesinlik) teorik tepe performansı nasıl elde edilebilir?

Anladığım kadarıyla , modern Intel CPU'ların çoğunda bir SSE için üç döngü addve a muliçin beş döngü gerekiyor (örneğin Agner Sis'in 'Talimat Tabloları' na bakın ). Boru hattı nedeniyle add, algoritmanın en az üç bağımsız toplamı varsa, döngü başına bir verim elde edilebilir . Bu addpdhem paketlenmiş hem de skaler addsdversiyonlar ve SSE kayıtları için iki tane içerebildiğinden double, verim, döngü başına iki flop kadar olabilir.

Ayrıca, (bununla ilgili herhangi bir uygun belge görmemiş olsam da) addve muldöngüleri, döngü başına dört flop teorik maksimum verim vererek paralel olarak yürütülebilir.

Ancak, bu performansı basit bir C / C ++ programı ile çoğaltamadım. En iyi denemem yaklaşık 2.7 flop / döngü ile sonuçlandı. Herkes büyük bir takdir en yüksek performansı gösteren basit bir C / C ++ veya montaj programı katkıda bulunabilir.

Girişimim:

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

İle derlendi

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

Intel Core i5-750, 2,66 GHz'de aşağıdaki çıkışı üretir.

addmul:  0.270 s, 3.707 Gflops, res=1.326463

Yani, döngü başına sadece 1.4 flop. Montajcı koduna g++ -S -O2 -march=native -masm=intel addmul.cppana döngü ile bakmak benim için uygun görünüyor:

.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

Paketlenmiş sürümleri ( addpdve mulpd) ile skaler sürümlerini değiştirmek, yürütme süresini değiştirmeden flop sayısını iki katına çıkarır ve böylece döngü başına 2,8 floptan daha kısa olurum. Her döngüde dört flop sağlayan basit bir örnek var mı?

Mysticial tarafından güzel küçük program; benim sonuçlarım (gerçi sadece birkaç saniye çalıştırın):

  • gcc -O2 -march=nocona: 10,66 Gflop'tan 5,6 Gflop (2,1 flop / döngü)
  • cl /O2, openmp çıkarıldı: 10,66 Gflop'tan 10,1 Gflop (3,8 flop / döngü)

Her şey biraz karmaşık gibi görünüyor, ama şu ana kadarki sonuçlarım:

  • gcc -O2Değişken addpdve mulpdmümkünse amacıyla bağımsız kayan nokta işlemlerinin sırasını değiştirir . Aynı şey için de geçerlidir gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona C ++ kaynağında tanımlandığı gibi kayan nokta işlemlerinin sırasını koruyor gibi görünüyor.

  • cl /O2, Windows 7 için SDK'nın 64 bit derleyicisi otomatik olarak döngü açma işlemini gerçekleştirir ve üç grubun üçü addpdile dönüşümlü olarak çalışmasını dener ve düzenler gibi görünüyor mulpd(en azından sistemimde ve basit programımda) .

  • Benim Çekirdek 750 i5 ( Nehalem mimarisini alternatif eklenti en ve mul yıllardan gibi değil) ve paralel olarak her iki işlemleri çalıştırmak mümkün görünüyor. Ancak, 3'lerde gruplandırılmışsa aniden sihir gibi çalışır.

  • Diğer mimariler (muhtemelen Sandy Bridge ve diğerleri) montaj kodunda dönüşümlü olarak eklenti / mul komutunu sorunsuz bir şekilde yürütebilirler.

  • Kabul etmek zor olsa da, sistemimde sistemim cl /O2için düşük seviyeli optimizasyon işlemlerinde çok daha iyi bir iş çıkarıyor ve yukarıdaki küçük C ++ örneği için zirveye yakın performans elde ediyor. 1.85-2.01 flop / döngü arasında ölçtüm (Windows'ta saat () kullandım, bu kesin değil. Sanırım, daha iyi bir zamanlayıcı kullanmanız gerekiyor - teşekkürler Mackie Messer).

  • En iyi yönettiğim gccelle açma ve üç kişilik gruplar halinde eklemeler ve çarpmalar düzenlemek oldu. İle g++ -O2 -march=nocona addmul_unroll.cpp ben en iyi olsun 0.207s, 4.825 Gflops1.8 hangi karşılık flop / Ben şimdi oldukça mutluyum döngüsü.

C ++ kodunda fordöngü ile değiştirdim

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

Ve montaj şimdi benziyor

.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
Wallclock zamanına güvenmek muhtemelen sebebin bir parçasıdır. Bunu Linux gibi bir işletim sisteminin içinde çalıştırdığınızı varsayarsak, işleminizi istediğiniz zaman planlamak ücretsizdir. Bu tür bir dış etkinlik performans ölçümlerinizi etkileyebilir.
tdenniston

GCC sürümünüz nedir? Varsayılanı kullanan bir mac'taysanız, sorun yaşarsınız (eski bir 4.2).
yarı gece

2
Evet Linux çalışıyor ancak sistemde yük yok ve birçok kez tekrarlamak çok az fark yaratıyor (örneğin skaler sürüm için 4.0-4.2 Gflops aralıkları, ancak şimdi ile -funroll-loops). Gcc 4.4.1 ve 4.6.2 sürümleriyle denediniz, ancak asm çıkışı iyi görünüyor mu?
user1059432

Denediniz mi -O3sağlayan, gcc için -ftree-vectorize? Belki de -funroll-loopsgerçekten gerekli olup olmadığını yapmama rağmen birlikte . Derleyicilerden biri vektörleştirme / açma yapıyorsa, diğeri yapamadığı için değil, aynı zamanda söylenmediği için karşılaştırma biraz haksız görünüyor.
Grizzly

4
@Kızıltıcı -funroll-loopsmuhtemelen denemek için bir şey. Ama bence -ftree-vectorizeönemli değil. OP sadece 1 mul + 1 ekleme talimatını / döngüsünü sürdürmeye çalışıyor. Talimatlar skaler veya vektör olabilir - gecikme ve verim aynı olduğundan önemli değildir. Eğer skaler SSE ile 2 / döngüyü sürdürebilirseniz, bunları vektör SSE ile değiştirebilirsiniz ve 4 flop / döngü elde edersiniz. Cevabımda sadece SSE -> AVX'ten bunu yaptım. Tüm SSE'yi AVX ile değiştirdim - aynı gecikmeler, aynı verim, flopların 2 katı.
Gizemli

Yanıtlar:


517

Bu görevi daha önce de yapmıştım. Ancak esas olarak güç tüketimini ve CPU sıcaklıklarını ölçmekti. Aşağıdaki kod (oldukça uzun) Core i7 2600K'mda optimum seviyeye yakın.

Burada dikkat edilmesi gereken en önemli şey, manüel döngü açma işleminin yanı sıra çarpmaların ve eklemelerin araya eklenmesi ...

Projenin tamamı GitHub'ımda bulunabilir: https://github.com/Mysticial/Flops

Uyarı:

Bunu derlemeye ve çalıştırmaya karar verirseniz, CPU sıcaklıklarınıza dikkat edin!
Aşırı ısınmadığınızdan emin olun. Ve CPU kısıtlamanın sonuçlarınızı etkilemediğinden emin olun!

Ayrıca, bu kodun çalıştırılmasından kaynaklanabilecek herhangi bir zarar için sorumluluk kabul etmiyorum.

Notlar:

  • Bu kod x64 için optimize edilmiştir. x86, bunun iyi bir şekilde derlenmesi için yeterli sicile sahip değildir.
  • Bu kod, Visual Studio 2010/2012 ve GCC 4.6 üzerinde iyi çalıştığı test edilmiştir.
    ICC 11 (Intel Compiler 11) şaşırtıcı bir şekilde iyi derleme konusunda sorun yaşıyor.
  • Bunlar FMA öncesi işlemciler içindir. Intel Haswell ve AMD Bulldozer işlemcilerinde (ve daha sonra) en üst FLOPS'a ulaşmak için FMA (Fused Multiply Add) talimatları gerekli olacaktır. Bunlar bu karşılaştırmanın kapsamı dışındadır.

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

Çıktı (1 iş parçacığı, 10000000 yineleme) - Visual Studio 2010 SP1 - x64 Sürüm ile derlenmiştir:

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

Makine, Core i7 2600K @ 4.4 GHz'dir. Teorik SSE piki 4 flop * 4.4 GHz = 17.6 GFlop'tur . Bu kod 17.3 GFlop'a ulaşır - fena değil.

Çıktı (8 iş parçacığı, 10000000 yineleme) - Visual Studio 2010 SP1 - x64 Sürüm ile derlenmiştir:

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

Teorik SSE piki 4 flop * 4 çekirdek * 4.4 GHz = 70.4 GFlop'tur. Gerçek 65.5 GFlop'dur .


Bunu bir adım daha ileri götürelim. 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");
}

Çıktı (1 iş parçacığı, 10000000 yineleme) - Visual Studio 2010 SP1 - x64 Sürüm ile derlenmiştir:

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

Teorik AVX pik değeri 8 flop * 4,4 GHz = 35,2 GFlop'dur . Gerçek 33.4 GFlop'dur .

Çıktı (8 iş parçacığı, 10000000 yineleme) - Visual Studio 2010 SP1 - x64 Sürüm ile derlenmiştir:

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

Teorik AVX zirvesi 8 flop * 4 çekirdek * 4.4 GHz = 140.8 GFlop'dur. Gerçek 138.2 GFlop'dur .


Şimdi bazı açıklamalar için:

Performans açısından kritik olan kısım, iç döngü içindeki 48 talimattır. Her biri 12 komuttan oluşan 4 blok halinde ayrıldığını göreceksiniz. Bu 12 komut bloğunun her biri birbirinden tamamen bağımsızdır ve yürütmek için ortalama 6 döngü sürer.

Bu nedenle, kullanım sorunu arasında 12 talimat ve 6 döngü var. Çarpma gecikmesi 5 döngüdür, bu nedenle gecikme duraklarından kaçınmak yeterlidir.

Verilerin aşırı / düşük akmasını önlemek için normalleştirme adımı gereklidir. Hiçbir şey yapma kodu verilerin büyüklüğünü yavaşça artıracağı / azaltacağı için bu gereklidir.

Yani eğer bütün sıfırları kullanırsanız ve normalleştirme adımından kurtulursanız bundan daha iyisini yapmak mümkündür. Ancak, güç tüketimini ve sıcaklığı ölçmek için bir ölçüt yazdığım için , flopların sıfırlardan ziyade "gerçek" verilerde olduğundan emin olmalıydım - çünkü yürütme birimleri daha az güç kullanan sıfırlar için özel durum yönetimi yapabilir ve daha az ısı üretir.


Daha fazla sonuç:

  • 3,5 GHz'de Intel Core i7 920
  • Windows 7 Ultimate x64
  • Visual Studio 2010 SP1 - x64 Sürümü

Konular: 1

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

Teorik SSE Zirvesi: 4 flop * 3,5 GHz = 14,0 GFlop . Gerçek 13.3 GFlop'dur .

Konular: 8

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

Teorik SSE Zirvesi: 4 flop * 4 çekirdek * 3.5 GHz = 56.0 GFloplar . Gerçek 51.3 GFlop'dur .

İşlemci sıcaklıklarım, çok iş parçacıklı çalışma sırasında 76C'ye ulaştı! Bunları çalıştırırsanız, sonuçların CPU kısıtlamasından etkilenmediğinden emin olun.


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

Konular: 1

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

Teorik SSE Zirvesi: 4 flop * 3,2 GHz = 12,8 GFlop . Gerçek 12.3 GFlop'dur .

Konular: 8

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

Teorik SSE Zirvesi: 4 flop * 8 çekirdek * 3,2 GHz = 102,4 GFlop . Gerçek 97.9 GFlop'dur .


13
Sonuçlarınız çok etkileyici. Kodumu eski sistemimde g ++ ile derledim, ancak neredeyse iyi sonuçlar elde edemiyorum: 100k iterasyon, 1.814s, 5.292 Gflops, sum=0.448883en fazla 10.68 Gflop'tan veya döngü başına 2.0 floptan kısa. Paralel görünüyor add/ mulyürütülmüyor. Kodunuzu değiştirdiğimde ve her zaman aynı kayıt ile eklediğimde / çarptığımda rC, aniden neredeyse zirveye ulaşır: 0.953s, 10.068 Gflops, sum=0veya 3.8 flop / döngü. Çok ilginç.
user1059432

11
Evet, satır içi montaj kullanmadığımdan, performans gerçekten derleyiciye çok duyarlıdır . Burada kod VC2010 için ayarlanmıştır. Ve doğru hatırlıyorsam, Intel Derleyici de aynı şekilde iyi sonuçlar verir. Fark ettiğiniz gibi, iyi derlemek için biraz ince ayar yapmanız gerekebilir.
Mistik

8
Windows 7'de cl /O2(windows sdk'den 64 bit) kullanarak sonuçlarınızı doğrulayabilirim ve örneğim bile orada skaler işlemler (1.9 flop / döngü) için zirveye yakın çalışır. Derleyici loop-unrolls ve yeniden sıralar ama bu biraz daha bakmak için nedeni olmayabilir. Sorun değil, benim cpu güzel ve 100k yineleme tutmak. :)
user1059432

6
@Mysticial: Bugün r / kodlama altdiziliminde ortaya çıktı .
greyfade

2
@haylem Erir ya da çıkar. Asla ikisi de. Yeterli soğutma varsa, normal görüşme alır. Aksi takdirde erir. :)
Gizemli

33

Intel mimarisinde insanların sık sık unuttukları bir nokta var, dağıtım portları Int ve FP / SIMD arasında paylaşılıyor. Bu, döngü mantığı kayan nokta akışınızda kabarcıklar oluşturmadan önce yalnızca belirli miktarda FP / SIMD patlaması alacağınız anlamına gelir. Mystical kodundan daha fazla flop aldı, çünkü açılmamış döngüsünde daha uzun adımlar kullandı.

Nehalem / Sandy Bridge mimarisine buradan bakarsanız http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 ne olduğu oldukça açık.

Buna karşılık, INT ve FP / SIMD boruları kendi zamanlayıcılarıyla ayrı sorun bağlantı noktalarına sahip olduğundan AMD (Buldozer) üzerinde en yüksek performansa ulaşmak daha kolay olmalıdır.

Bu sadece teoriktir çünkü test edecek bu işlemcilerden hiçbirine sahip değilim.


2
Orada döngü yükü sadece üç talimatlar şunlardır: inc, cmpve jl. Tüm bunlar # 5 numaralı bağlantı noktasına gidebilir ve vectorized faddya da fmul. Kod çözücünün (bazen) yoluna girdiğinden şüphelenmeyi tercih ederim. Her döngüde iki ila üç talimat sürdürmesi gerekir. Kesin sınırlamaları hatırlamıyorum, ancak talimat uzunluğu, önekler ve hizalama hepsi devreye giriyor.
Mackie Messer

cmpve jlkesinlikle bağlantı noktası 5 gidin, inco kadar emin değilim her zaman diğer 2 grupla birlikte gelir. Ama haklısın, darboğazın nerede olduğunu söylemek zor ve kod çözücüler de bunun bir parçası olabilir.
Patrick Schlüter

3
Temel döngü ile biraz oynadım: talimatların sıralaması önemli. Bazı düzenlemeler minimum 5 döngü yerine 13 döngü alır. Sanırım performans etkinliği sayaçlarına bakma zamanı ...
Mackie Messer

16

Şubeler kesinlikle en yüksek teorik performansı sürdürmenizi engelleyebilir. Manuel olarak bazı loop-unrolling işlemleri yaparsanız bir fark görüyor musunuz? Örneğin, döngü yinelemesi başına 5 veya 10 kat daha fazla işlem yaparsanız:

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
Yanılıyor olabilir, ama g ++ ile -O2 otomatik olarak döngü çözmeye çalışacağına inanıyorum (Ben Duff'ın Cihaz kullanır düşünüyorum).
Weaver

6
Evet, teşekkürler gerçekten biraz gelişiyor. Şimdi döngü başına yaklaşık 4.1-4.3 Gflop veya 1.55 flop alıyorum. Ve hayır, bu örnekte -O2 çözmeyi açmadı.
user1059432

1
Weaver, döngü çözme konusunda haklı. Yani elle açma muhtemelen gerekli değildir
jim mcnamara

5
Yukarıdaki montaj çıkışına bakın, döngü açma belirtisi yoktur.
user1059432

14
Otomatik açma da ortalama 4,2 Gflop'a yükselir, ancak -funroll-loopsdahil olmayan bir seçenek gerektirir -O3. Bkz g++ -c -Q -O2 --help=optimizers | grep unroll.
user1059432

7

2.4GHz Intel Core 2 Duo'da Intels icc Sürüm 11.1'i kullanma

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 

Bu ideal 9,6 Gflop'lara çok yakın.

DÜZENLE:

Hata, montaj koduna bakıldığında, icc'nin sadece çarpımı vektörleştirmekle kalmayıp, eklemeleri döngüden çıkardığı anlaşılıyor. Daha katı bir fp anlambilimini zorlamak kodu artık vektörlenmez:

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

EDIT2:

İstendiği gibi:

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 kodunun iç döngüsü şöyle görünür:

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

Son olarak, iki öneri: İlk olarak, bu tür kıyaslamalardan hoşlanıyorsanız, rdtscistead komutunu kullanmayı düşünün gettimeofday(2). Çok daha doğrudur ve zamanı döngüsel olarak verir, bu da genellikle ilgilendiğiniz şeydir. Gcc ve arkadaşları için bunu şu şekilde tanımlayabilirsiniz:

#include <stdint.h>

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

İkinci olarak, karşılaştırma programınızı birkaç kez çalıştırmalı ve yalnızca en iyi performansı kullanmalısınız . Modern işletim sistemlerinde birçok şey paralel olarak gerçekleşir, cpu düşük frekanslı güç tasarrufu modunda olabilir, vb. Programı tekrar tekrar çalıştırmak size ideal duruma daha yakın bir sonuç verir.


2
ve demontaj neye benziyor?
Bahbar

1
İlginç, bu 1 flop / döngüden daha az. Derleyici addsd've mulsd' leri karıştırıyor mu yoksa montaj çıktımdaki gibi gruplar halinde mi? Derleyici onları karıştırdığında da (yaklaşık 1 olsun) sadece 1 flop / döngüsü olsun -march=native. add=mul;Fonksiyonun başına bir satır eklerseniz performans nasıl değişir addmul(...)?
user1059432

1
@ user1059432: addsdve subsdtalimatları gerçekten de kesin sürümde karıştırılır. Clang 3.0'ı da denedim, talimatları karıştırmıyor ve çekirdek 2 duo'da 2 flop / döngüye çok yakın geliyor. Aynı kodu dizüstü bilgisayarım çekirdek i5 üzerinde çalıştırdığımda, kodun karıştırılması hiçbir fark yaratmaz. Her iki durumda da yaklaşık 3 flop / döngü elde ederim.
Mackie Messer

1
@ user1059432: Sonuçta, derleyiciyi sentetik bir kıyaslama için "anlamlı" kod üretmeye kandırmakla ilgili. Bu ilk bakışta göründüğünden daha zor. (yani icc karşılaştırmanızı aşar) İstediğiniz tek şey 4 flop / döngüde bir kod çalıştırmaksa en kolay şey küçük bir montaj döngüsü yazmaktır. Çok daha az baş ağrısı. :-)
Mackie Messer

1
Tamam, böylece yukarıda belirttiğim gibi bir montaj kodu ile 2 flop / döngüye yakın olsun? 2'ye ne kadar yakın? Sadece 1.4 elde ediyorum, bu yüzden bu önemli. Derleyici daha önce gördüğünüz gibi optimizasyon yapmazsa, dizüstü bilgisayarınızda 3 flop / döngü elde ettiğinizi sanmıyorum icc, montajı iki kez kontrol edebilir misiniz?
user1059432
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.