मैं प्रति चक्र 4 सैद्धांतिक की अधिकतम कैसे प्राप्त कर सकता हूं?


642

आधुनिक x86-64 इंटेल सीपीयू पर प्रति चक्र 4 फ्लोटिंग पॉइंट ऑपरेशन्स (डबल प्रिसिजन) का सैद्धांतिक शिखर प्रदर्शन कैसे प्राप्त किया जा सकता है?

जहाँ तक मैं समझता हूँ कि यह SSE के लिए तीन चक्र लेती है addऔर mulआधुनिक Intel CPU में से अधिकांश पर पूर्ण करने के लिए पाँच चक्र (उदाहरण के लिए 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 GHz पर निम्न आउटपुट का उत्पादन करता है।

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 फ्लॉप की कमी होगी। क्या एक सरल उदाहरण है जो प्रति चक्र चार फ्लॉप प्राप्त करता है?

मिस्टिकल द्वारा अच्छा सा कार्यक्रम; यहां मेरे परिणाम हैं (हालांकि कुछ सेकंड के लिए चलाएं):

  • gcc -O2 -march=nocona: 5.6 Gflops 10.66 Gflops (2.1 फ़्लॉप / चक्र) में से
  • cl /O2, ओपनम्प निकाल दिया गया: 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, विंडोज 7 के लिए एसडीके से 64-बिट संकलक स्वचालित रूप से लूप-अनरोलिंग करता है और संचालन की कोशिश करने और व्यवस्थित करने के लिए लगता है ताकि तीन के समूह तीन addpdके साथ वैकल्पिक हो mulpd(ठीक है, कम से कम मेरे सिस्टम पर और मेरे सरल कार्यक्रम के लिए) ।

  • My Core i5 750 ( Nehalem आर्किटेक्चर ) को ऐड और मुल का विकल्प पसंद नहीं है और दोनों ऑपरेशन को समानांतर में चलाने में असमर्थ हैं। हालांकि, अगर 3 में समूहीकृत किया जाए तो यह अचानक जादू की तरह काम करता है।

  • अन्य आर्किटेक्चर (संभवतः सैंडी ब्रिज और अन्य) असेंबली कोड में वैकल्पिक होने पर समस्याओं के बिना समानांतर में ऐड / म्यूल को निष्पादित करने में सक्षम होते हैं।

  • यद्यपि स्वीकार करना मुश्किल है, लेकिन मेरे सिस्टम पर मेरे सिस्टम के लिए cl /O2निम्न-स्तरीय अनुकूलन संचालन में बहुत बेहतर काम करता है और ऊपर थोड़ा सी ++ उदाहरण के लिए चोटी के प्रदर्शन के करीब प्राप्त करता है। मैंने विंडोज में 1.85-2.01 फ्लॉप्स / साइकिल (घड़ी का उपयोग किया है) के बीच मापा गया जो कि सटीक नहीं है। मुझे लगता है, एक बेहतर टाइमर का उपयोग करने की आवश्यकता है - धन्यवाद मैकी मेसर)।

  • मेरे साथ सबसे अच्छा मैं 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
वॉलकॉक के समय पर भरोसा शायद इसका कारण है। यह मानकर कि आप इसे लिनक्स जैसे OS के अंदर चला रहे हैं, यह किसी भी समय आपकी प्रक्रिया को रद्द करने के लिए स्वतंत्र है। उस प्रकार की बाहरी घटना आपके प्रदर्शन माप को प्रभावित कर सकती है।
tdenniston 18

आपका GCC संस्करण क्या है? यदि आप डिफ़ॉल्ट का उपयोग करते हुए एक मैक पर हैं, तो आप समस्याओं में भाग लेंगे (यह एक पुरानी 4.2 है)।
19 दिसबंर को अर्धशतक

2
हाँ, लिनक्स चल रहा है, लेकिन सिस्टम पर कोई भार नहीं है और इसे कई बार दोहराने से थोड़ा अंतर पड़ता है (जैसे स्केलर संस्करण के लिए 4.0-4.2 Gflops, लेकिन अब साथ है -funroll-loops)। Gcc संस्करण 4.4.1 और 4.6.2 के साथ प्रयास किया गया, लेकिन asm आउटपुट ठीक है?
user1059432 19

क्या आपने -O3जीसीसी के लिए प्रयास किया, जो सक्षम बनाता है -ftree-vectorize? शायद -funroll-loopsमेरे साथ संयुक्त है लेकिन अगर ऐसा नहीं है तो वास्तव में आवश्यक है। तुलना के बाद अनुचित एक तरह का प्रतीत होता है यदि संकलक में से कोई एक वेक्टरकरण / अनियंत्रित करता है, जबकि दूसरा ऐसा नहीं करता है क्योंकि यह नहीं कर सकता है, लेकिन क्योंकि यह भी नहीं बताया गया है।
ग्रिजली

4
@ -funroll-loopsअच्छी तरह से शायद कुछ करने की कोशिश है। लेकिन मुझे लगता -ftree-vectorizeहै कि बिंदु के अलावा है। ओपी केवल 1 mul + 1 जोड़ने के निर्देश / चक्र को बनाए रखने की कोशिश कर रहा है। निर्देश अदिश या वेक्टर हो सकते हैं - इससे कोई फर्क नहीं पड़ता क्योंकि विलंबता और प्रवाह एक समान हैं। इसलिए यदि आप स्केलर एसएसई के साथ 2 / चक्र बनाए रख सकते हैं, तो आप उन्हें वेक्टर एसएसई से बदल सकते हैं और आप 4 फ्लॉप / चक्र प्राप्त करेंगे। अपने जवाब में मैंने बस इतना ही किया कि SSE -> AVX से जा रहा हूँ। मैंने एवीएक्स के साथ सभी एसएसई को बदल दिया - समान विलंबता, समान प्रवाह, 2x फ्लॉप।
रहस्यपूर्ण

जवाबों:


517

मैंने यह सटीक कार्य पहले किया है। लेकिन यह मुख्य रूप से बिजली की खपत और सीपीयू तापमान को मापने के लिए था। निम्नलिखित कोड (जो काफी लंबा है) मेरे कोर i7 2600K पर इष्टतम के करीब प्राप्त करता है।

यहां ध्यान देने योग्य बात यह है कि मैनुअल लूप की भारी मात्रा में अनियंत्रित होने के साथ-साथ कई गुणा और जुड़ाव भी होता है ...

पूरा प्रोजेक्ट मेरे GitHub: https://github.com/Mysticial/Flops पर मिल सकता है

चेतावनी:

यदि आप इसे संकलित करने और चलाने का निर्णय लेते हैं, तो अपने सीपीयू तापमान पर ध्यान दें !!!
सुनिश्चित करें कि आप इसे ज़्यादा गरम नहीं करते हैं। और सुनिश्चित करें कि सीपीयू-थ्रॉटलिंग आपके परिणामों को प्रभावित नहीं करता है!

इसके अलावा, इस कोड को चलाने से जो भी नुकसान हो सकता है, उसके लिए मैं कोई जिम्मेदारी नहीं लेता हूं।

टिप्पणियाँ:

  • यह कोड x64 के लिए अनुकूलित है। x86 में अच्छी तरह से संकलित करने के लिए पर्याप्त रजिस्टर नहीं है।
  • इस कोड को Visual Studio 2010/2012 और GCC 4.6 पर अच्छी तरह से काम करने के लिए परीक्षण किया गया है।
    ICC 11 (Intel Compiler 11) को आश्चर्यजनक रूप से इसे अच्छी तरह से समझने में परेशानी होती है।
  • ये प्री-एफएमए प्रोसेसर के लिए हैं। इंटेल हैवेल और एएमडी बुलडोजर प्रोसेसर (और बाद में) पर एफएलओपीएस प्राप्त करने के लिए, एफएमए (फ्यूज्ड मल्टीलीली ऐड) निर्देशों की आवश्यकता होगी। ये इस बेंचमार्क के दायरे से परे हैं।

#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 पुनरावृत्तियों) - विजुअल स्टूडियो 2010 SP1 के साथ संकलित - x64 रिलीज़:

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

मशीन एक कोर i7 2600K @ 4.4 GHz है। सैद्धांतिक SSE शिखर 4 फ्लॉप * 4.4 GHz = 17.6 GFlops है । यह कोड 17.3 GFlops प्राप्त करता है - बुरा नहीं है।

आउटपुट (8 थ्रेड्स, 10000000 पुनरावृत्तियों) - दृश्य स्टूडियो 2010 SP1 के साथ संकलित - x64 रिलीज़:

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

सैद्धांतिक SSE शिखर 4 फ्लॉप * 4 कोर * 4.4 GHz = 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 पुनरावृत्तियों) - विजुअल स्टूडियो 2010 SP1 के साथ संकलित - x64 रिलीज़:

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

सैद्धांतिक AVX शिखर 8 फ्लॉप * 4.4 GHz = 35.2 GFlops है । वास्तविक 33.4 GFlops है

आउटपुट (8 थ्रेड्स, 10000000 पुनरावृत्तियों) - दृश्य स्टूडियो 2010 SP1 के साथ संकलित - x64 रिलीज़:

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

सैद्धांतिक AVX शिखर 8 फ्लॉप * 4 कोर * 4.4 GHz = 140.8 GFlops है। वास्तविक 138.2 GFlops है


अब कुछ स्पष्टीकरण के लिए:

प्रदर्शन महत्वपूर्ण हिस्सा स्पष्ट रूप से आंतरिक लूप के अंदर 48 निर्देश हैं। आप देखेंगे कि यह प्रत्येक के 12 निर्देशों के 4 खंडों में टूट गया है। इन 12 निर्देशों में से प्रत्येक ब्लॉक एक दूसरे से पूरी तरह से स्वतंत्र हैं - और निष्पादित करने के लिए औसत 6 चक्र लेते हैं।

इसलिए इश्यू-टू-यूज़ के बीच 12 निर्देश और 6 चक्र हैं। गुणा की विलंबता 5 चक्र है, इसलिए यह विलंबता स्टालों से बचने के लिए पर्याप्त है।

डेटा को ओवर / अंडरफ़्लोइंग से रखने के लिए सामान्यीकरण चरण की आवश्यकता होती है। यह तब से आवश्यक है जब डेटा के परिमाण में कुछ भी नहीं धीरे-धीरे बढ़ेगा / घटेगा।

तो यह वास्तव में इस से बेहतर करना संभव है अगर आप बस सभी शून्य का उपयोग करते हैं और सामान्यीकरण चरण से छुटकारा पाते हैं। हालाँकि, जब से मैंने बिजली की खपत और तापमान को मापने के लिए बेंचमार्क लिखा, मुझे यह सुनिश्चित करना था कि फ्लॉप "वास्तविक" डेटा पर थे, बजाय ज़ीरो - निष्पादन इकाइयों में बहुत अच्छी तरह से शून्य के लिए विशेष केस-हैंडलिंग हो सकता है जो कम बिजली का उपयोग करते हैं और कम गर्मी पैदा करते हैं।


और नतीजे:

  • इंटेल कोर i7 920 @ 3.5 GHz
  • विंडोज 7 अल्टीमेट x64
  • विजुअल स्टूडियो 2010 SP1 - x64 रिलीज़

सूत्र: 1

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

सैद्धांतिक SSE पीक: 4 फ्लॉप * 3.5 GHz = 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 GHz = 56.0 GFlops । वास्तविक 51.3 GFlops है

मेरे प्रोसेसर टेम्पों ने बहु-थ्रेडेड रन पर 76C मारा! यदि आप इन्हें चलाते हैं, तो सुनिश्चित करें कि परिणाम CPU थ्रॉटलिंग से प्रभावित नहीं हैं।


  • 2 x इंटेल Xeon X5482 हार्परटाउन @ 3.2 गीगाहर्ट्ज़
  • उबुन्टु लिनक्स 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 GHz = 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 GHz = 102.4 GFlops । वास्तविक 97.9 GFlops है


13
आपके परिणाम बहुत प्रभावशाली हैं। मैंने आपके कोड को मेरे पुराने सिस्टम पर g ++ के साथ संकलित किया है, लेकिन लगभग अच्छे परिणाम नहीं मिलते हैं: 100k पुनरावृत्तियों, 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 के लिए ट्यून किया गया है। और अगर मैं सही ढंग से याद करता हूं, तो इंटेल कंपाइलर अच्छे परिणाम देता है। जैसा कि आपने देखा है, इसे अच्छी तरह से संकलित करने के लिए आपको इसे थोड़ा मोड़ना पड़ सकता है।
मि।

8
मैं विंडोज 7 पर अपने परिणामों की पुष्टि कर सकता हूं cl /O2(विंडोज़ एसडीके से 64-बिट) और यहां तक ​​कि मेरा उदाहरण स्केलर ऑपरेशन (1.9 फ्लॉप / चक्र) के लिए चरम के करीब चलता है। कंपाइलर लूप-अनरोल और रीऑर्डर्स लेकिन वह कारण हो सकता है कि इसे थोड़ा और देखने की जरूरत न हो। एक समस्या नहीं है, मैं अपने सीपीयू के लिए अच्छा हूं और 100k पर पुनरावृत्तियों को रखना। :)
उपयोगकर्ता

6
@ मूल: यह आज r / कोडिंग सब्रेडिट पर दिखाई दिया
ग्रेफेड

2
@ हाइलम यह या तो पिघल जाता है या बंद हो जाता है। दोनों कभी नहीं। अगर वहाँ पर्याप्त शीतलन है, तो इसे एयरटाइम मिलेगा। अन्यथा, यह सिर्फ पिघला देता है। :)
रहस्यपूर्ण

33

इंटेल आर्किटेक्चर में एक बिंदु है जिसे लोग अक्सर भूल जाते हैं, प्रेषण बंदरगाहों को इंट और एफपी / सिमडी के बीच साझा किया जाता है। इसका मतलब यह है कि लूप लॉजिक से आपके फ्लोटिंग पॉइंट स्ट्रीम में बुलबुले बनने से पहले आपको केवल एफपी / सिमडी के फटने की एक निश्चित मात्रा मिलेगी। मिस्टिकल को अपने कोड से अधिक फ्लॉप मिला, क्योंकि वह अपने अनियंत्रित लूप में लंबे समय तक घूमता था।

यदि आप यहाँ Nehalem / Sandy Bridge वास्तुकला को देखते हैं तो http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 यह काफी स्पष्ट है कि क्या होता है।

इसके विपरीत, एएमडी (बुलडोजर) पर चरम प्रदर्शन तक पहुंचना आसान होना चाहिए क्योंकि INT और FP / SIMD पाइप के पास अपने स्वयं के शेड्यूलर के साथ अलग-अलग समस्या पोर्ट हैं।

यह केवल सैद्धांतिक है क्योंकि मेरे पास परीक्षण करने के लिए इनमें से कोई भी प्रोसेसर नहीं है।


2
पाश भूमि के ऊपर के केवल तीन निर्देश हैं: inc, cmp, और jl। ये सभी पोर्ट # 5 पर जा सकते हैं और वेक्टर faddया के साथ हस्तक्षेप नहीं करते हैं fmul। मुझे लगता है कि डिकोडर (कभी-कभी) रास्ते में हो जाता है। इसे प्रति चक्र दो और तीन निर्देशों के बीच बनाए रखने की आवश्यकता है। मुझे सटीक सीमाएँ याद नहीं हैं लेकिन निर्देश की लंबाई, उपसर्ग और संरेखण सभी खेल में आते हैं।
मैकी मेसर

cmpऔर jlनिश्चित रूप से पोर्ट 5 पर जाएं, incइतना निश्चित नहीं है क्योंकि यह हमेशा 2 अन्य लोगों के साथ समूह में आता है। लेकिन आप सही हैं, यह बताना कठिन है कि अड़चन कहां है और डिकोडर्स भी इसका हिस्सा हो सकते हैं।
पैट्रिक श्ल्टर

3
मैंने बुनियादी लूप के साथ थोड़ा सा खेला: निर्देशों का क्रम मायने रखता है। कुछ व्यवस्थाएँ न्यूनतम 5 चक्रों के बजाय 13 चक्र लेती हैं। प्रदर्शन इवेंट काउंटर्स को देखने का समय मुझे लगता है ...
मैकी मेसर

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
मुझसे गलती हो सकती है, लेकिन मेरा मानना ​​है कि जी ++ के साथ -O2 स्वचालित रूप से लूप को खोलने का प्रयास करेगा (मुझे लगता है कि यह डफ डिवाइस का उपयोग करता है)।
बुनकर

6
हाँ, धन्यवाद वास्तव में यह कुछ हद तक सुधार करता है। अब मुझे लगभग 4.1-4.3 Gflops, या 1.55 फ्लॉप प्रति चक्र मिलता है। और नहीं, इस उदाहरण में -O2 लूप को अनियंत्रित नहीं करता है।
user1059432

1
वीवर लूप अनरोलिंग के बारे में सही है, मुझे विश्वास है। इसलिए मैन्युअल रूप से अनियंत्रित करना आवश्यक नहीं है
जिम् एमकनामारा

5
ऊपर विधानसभा आउटपुट देखें, लूप के अनियंत्रित होने के कोई संकेत नहीं हैं।
user1059432

14
स्वचालित अनरोलिंग भी 4.2 Gflops के औसत में सुधार करता है, लेकिन इसके लिए -funroll-loopsविकल्प की आवश्यकता होती है जो इसमें शामिल नहीं है -O3। देखते हैं g++ -c -Q -O2 --help=optimizers | grep unroll
user1059432

7

Intels icc वर्जन 11.1 का इस्तेमाल 2.4GHz इंटेल कोर 2 डुओ पर मिलता है

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निर्देश के उपयोग से विचार करें gettimeofday(2)। यह बहुत अधिक सटीक है और समय को चक्रों में वितरित करता है, जो आमतौर पर वैसे भी आपकी रुचि है। जीसीसी और दोस्तों के लिए आप इसे इस तरह परिभाषित कर सकते हैं:

#include <stdint.h>

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

दूसरा, आपको अपने बेंचमार्क प्रोग्राम को कई बार चलाना चाहिए और केवल सर्वश्रेष्ठ प्रदर्शन का उपयोग करना चाहिए । आधुनिक ऑपरेटिंग सिस्टम में कई चीजें समानांतर में होती हैं, सीपीयू कम आवृत्ति बिजली की बचत मोड में हो सकता है, आदि। प्रोग्राम को बार-बार चलाने से आपको एक परिणाम मिलता है जो आदर्श मामले के करीब है।


2
और डिसआर्डर को कैसा दिखता है?
०11:४६ में बाहबर

1
दिलचस्प है, यह 1 फ्लॉप / चक्र से कम है। क्या संकलक addsd's' और mulsd's मिक्स करता है या वे मेरी असेंबली आउटपुट के समूह में हैं? मैं भी लगभग 1 फ्लॉप / चक्र प्राप्त करता हूं जब कंपाइलर उन्हें मिलाता है (जो मुझे बिना मिलता है -march=native)। यदि आप add=mul;फ़ंक्शन की शुरुआत में एक पंक्ति जोड़ते हैं तो प्रदर्शन कैसे बदलता है addmul(...)?
user1059432

1
@ user1059432: वास्तव में सटीक संस्करण में निर्देश addsdऔर subsdनिर्देश मिश्रित हैं। मैंने 3.0 क्लैग की कोशिश की, यह निर्देश नहीं मिलाता है और यह कोर 2 जोड़ी पर 2 फ्लॉप / चक्र के बहुत करीब आता है। जब मैं अपने लैपटॉप कोर i5 पर समान कोड चलाता हूं, तो कोड को मिलाकर कोई फर्क नहीं पड़ता। मुझे किसी भी स्थिति में लगभग 3 फ्लॉप / चक्र मिलते हैं।
मैकी मेसर

1
@ user1059432: अंत में यह संकलक को सिंथेटिक बेंचमार्क के लिए "सार्थक" कोड जनरेट करने में बरगला रहा है। यह पहली नज़र में लगता है की तुलना में कठिन है। (यानी icc आपके बेंचमार्क को आउटसोर्स करता है) यदि आप चाहते हैं कि कुछ कोड 4 फ्लॉप / साइकिल पर चलें तो सबसे आसान बात यह है कि एक छोटा असेंबली लूप लिखना है। बहुत कम हेडेक। :-)
मैकी मेसर

1
ठीक है, तो आप 2 फ्लॉप / चक्र के करीब एक विधानसभा कोड के साथ मिलते हैं जो मैंने ऊपर उद्धृत किया है? 2 के करीब कैसे? मुझे केवल 1.4 मिलता है इसलिए यह महत्वपूर्ण है। मुझे नहीं लगता कि आपको अपने लैपटॉप पर 3 फ्लॉप / चक्र मिलते हैं जब तक कि कंपाइलर अनुकूलन नहीं करता है जैसा कि आपने पहले देखा है icc, क्या आप असेंबली की दोबारा जांच कर सकते हैं?
14:105 पर user1059432
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.