c++ - मैं प्रति चक्र सैद्धांतिक अधिकतम 4 एफएलओपी कैसे प्राप्त करूं?




optimization architecture (3)

2.4 गीगाहर्ट्ज इंटेल कोर 2 डुओ पर इंटेल आईसीसी संस्करण 11.1 का उपयोग करना

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 के बहुत करीब है।

संपादित करें:

ओह, असेंबली कोड को देखते हुए ऐसा लगता है कि आईसीसी न केवल गुणा को सदिशित करता है, बल्कि लूप से जोड़ों को भी खींचता है। एक कठोर एफपी अर्थशास्त्र को मजबूर करना कोड अब वेक्टरकृत नहीं है:

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

#include <stdint.h>

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

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

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

जहां तक ​​मैं समझता हूं कि यह एक SSE add लिए तीन चक्र लेता है और अधिकांश इंटेल सीपीयू पर पूरा करने के लिए एक mul लिए पांच चक्र (उदाहरण के लिए एग्नेर फोग के 'निर्देश टेबल्स' ) देखें। पाइपलाइनिंग के कारण एल्गोरिदम में कम से कम तीन स्वतंत्र सम्मेलन होने पर एक चक्र प्रति एक चक्र का एक थ्रूपुट प्राप्त कर सकता है। चूंकि यह पैक किए गए addpd साथ-साथ स्केलर addsd संस्करणों के लिए भी सच है और एसएसई रजिस्टरों में दो double का थ्रूपुट हो सकता है जितना प्रति चक्र दो फ्लॉप हो सकता है।

इसके अलावा, ऐसा लगता है (हालांकि मैंने इस पर कोई उचित दस्तावेज नहीं देखा है) mul और mul प्रति चक्र चार फ्लॉप के सैद्धांतिक अधिकतम थ्रूपुट को समानांतर में निष्पादित किया जा सकता है।

हालांकि, मैं उस प्रदर्शन को एक साधारण सी / सी ++ प्रोग्राम के साथ दोहराने में सक्षम नहीं हूं। मेरा सबसे अच्छा प्रयास लगभग 2.7 फ्लॉप / चक्र में हुआ। यदि कोई साधारण सी / सी ++ या असेंबलर प्रोग्राम का योगदान कर सकता है जो चरम प्रदर्शन को प्रदर्शित करता है जिसे बहुत सराहना की जाएगी।

मेरा प्रयास:

#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

इंटेल कोर i5-750, 2.66 गीगाहर्ट्ज पर निम्न आउटपुट उत्पन्न करता है।

addmul:  0.270 s, 3.707 Gflops, res=1.326463

यही है, प्रति चक्र लगभग 1.4 फ्लॉप। g++ -S -O2 -march=native -masm=intel addmul.cpp के साथ असेंबलर कोड को 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.16 ग्रफ्लॉप्स में से 10.1 ग्रफॉप्स (3.8 फ्लॉप / चक्र)

यह सब थोड़ा जटिल लगता है, लेकिन मेरे निष्कर्ष अब तक:

  • gcc -O2 mulpd यदि संभव हो तो addpd और mulpd को वैकल्पिक रूप से addpd के उद्देश्य से स्वतंत्र फ़्लोटिंग पॉइंट ऑपरेशंस के क्रम को बदलता है। gcc-4.6.2 -O2 -march=core2 पर भी लागू होता है।

  • gcc -O2 -march=nocona सी ++ स्रोत में परिभाषित फ्लोटिंग पॉइंट ऑपरेशंस का क्रम रखने लगता है।

  • cl /O2 , विंडोज 7 के लिए एसडीके से 64-बिट कंपाइलर स्वचालित रूप से लूप-अनोलरिंग करता है और ऑपरेशन को आजमाने और व्यवस्थित करने लगता है ताकि तीन addpd के वैकल्पिक तीन समूहों के साथ वैकल्पिक (ठीक है, कम से कम मेरे सिस्टम पर और मेरे सरल कार्यक्रम के लिए)।

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

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

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

  • gcc साथ प्रबंधित सबसे अच्छा था मैन्युअल रूप से लूप अनलोल करना और तीन समूहों में जोड़ों और गुणाओं की व्यवस्था करना। g++ -O2 -march=nocona addmul_unroll.cpp मुझे सबसे अच्छे 0.207s, 4.825 Gflops जो 1.8 फ्लॉप / चक्र से मेल 0.207s, 4.825 Gflops हैं जो मैं अब से बहुत खुश हूं।

सी ++ कोड में मैंने लूप के साथ प्रतिस्थापित किया है

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

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

यदि आप नेहलेम / सैंडी ब्रिज आर्किटेक्चर को यहां http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 यह स्पष्ट है कि क्या होता है।

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

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


शाखाएं आपको निश्चित रूप से शीर्ष सैद्धांतिक प्रदर्शन को बनाए रखने से रोक सकती हैं। यदि आप मैन्युअल रूप से कुछ लूप-अनोलिंग करते हैं तो क्या आप एक अंतर देखते हैं? उदाहरण के लिए, यदि आप 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;
   }




assembly