आधुनिक 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=noconaC ++ स्रोत में परिभाषित फ्लोटिंग पॉइंट संचालन के क्रम को बनाए रखने के लिए लगता है।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
...
-funroll-loops)। Gcc संस्करण 4.4.1 और 4.6.2 के साथ प्रयास किया गया, लेकिन asm आउटपुट ठीक है?
-O3जीसीसी के लिए प्रयास किया, जो सक्षम बनाता है -ftree-vectorize? शायद -funroll-loopsमेरे साथ संयुक्त है लेकिन अगर ऐसा नहीं है तो वास्तव में आवश्यक है। तुलना के बाद अनुचित एक तरह का प्रतीत होता है यदि संकलक में से कोई एक वेक्टरकरण / अनियंत्रित करता है, जबकि दूसरा ऐसा नहीं करता है क्योंकि यह नहीं कर सकता है, लेकिन क्योंकि यह भी नहीं बताया गया है।
-funroll-loopsअच्छी तरह से शायद कुछ करने की कोशिश है। लेकिन मुझे लगता -ftree-vectorizeहै कि बिंदु के अलावा है। ओपी केवल 1 mul + 1 जोड़ने के निर्देश / चक्र को बनाए रखने की कोशिश कर रहा है। निर्देश अदिश या वेक्टर हो सकते हैं - इससे कोई फर्क नहीं पड़ता क्योंकि विलंबता और प्रवाह एक समान हैं। इसलिए यदि आप स्केलर एसएसई के साथ 2 / चक्र बनाए रख सकते हैं, तो आप उन्हें वेक्टर एसएसई से बदल सकते हैं और आप 4 फ्लॉप / चक्र प्राप्त करेंगे। अपने जवाब में मैंने बस इतना ही किया कि SSE -> AVX से जा रहा हूँ। मैंने एवीएक्स के साथ सभी एसएसई को बदल दिया - समान विलंबता, समान प्रवाह, 2x फ्लॉप।