महत्वपूर्ण पृष्ठभूमि का पठन: एग्नर फॉग का माइक्रोआर्च पीडीएफ , और संभवत: उलरिच ड्रेपर की मेमोरी के बारे में हर प्रोग्रामर को भी जानना चाहिए । अन्य लिंक भी देखें86टैग विकी, विशेष रूप से इंटेल के अनुकूलन मैनुअल, और डेविड कनेटर के डायग्राम के साथ हैसवेल माइक्रोआर्किटेक्चर का विश्लेषण ।
बहुत शांत असाइनमेंट; मैंने उन लोगों की तुलना में बहुत बेहतर देखा है जहाँ छात्रों को कुछ कोड को ऑप्टिमाइज़ करने के लिए कहा गया थाgcc -O0
, जो कि ट्रिक्स का एक गुच्छा सीखते हैं जो वास्तविक कोड में मायने नहीं रखते हैं। इस स्थिति में, आपको सीपीयू पाइपलाइन के बारे में जानने के लिए कहा जा सकता है और इसका उपयोग करके अपने डी-ऑप्टिमाइज़ेशन प्रयासों को निर्देशित किया जा सकता है, न कि केवल अंधा अनुमान लगाने के लिए। इस का सबसे मजेदार हिस्सा "निराशात्मक अक्षमता" के साथ प्रत्येक निराशाकरण को सही ठहरा रहा है, जानबूझकर द्वेष नहीं।
असाइनमेंट शब्दांकन और कोड के साथ समस्याएं :
इस कोड के लिए विशिष्ट-विशिष्ट विकल्प सीमित हैं। यह किसी भी सरणियों का उपयोग नहीं करता है, और लागत का बहुत exp
/ log
पुस्तकालय कार्यों के लिए कॉल है । अधिक या कम निर्देश-स्तरीय समानता का स्पष्ट तरीका नहीं है, और लूप-आधारित निर्भरता श्रृंखला बहुत कम है।
मैं एक जवाब देखना पसंद करूंगा, जिसने निर्भरता को बदलने के लिए अभिव्यक्ति को फिर से व्यवस्थित करने से मंदी लाने का प्रयास किया, आईएलपी को केवल निर्भरता (खतरों) से कम करने के लिए । मैंने इसका प्रयास नहीं किया है।
इंटेल सैंडीब्रिज-परिवार सीपीयू आक्रामक आउट-ऑफ-ऑर्डर डिज़ाइन हैं जो समानांतरवाद को खोजने और खतरों (निर्भरता) से बचने के लिए बहुत सारे ट्रांजिस्टर और शक्ति खर्च करते हैं जो एक क्लासिक आरआईएससी-इन-ऑर्डर पाइपलाइन को परेशान करेगा । आमतौर पर एकमात्र पारंपरिक खतरे जो इसे धीमा करते हैं, रॉ "सच" निर्भरताएं हैं जो थ्रूपुट द्वारा सीमित होने का कारण बनती हैं।
रजिस्टर के लिए WAR और WAW खतरे बहुत ज्यादा एक मुद्दा नहीं है, नाम बदलने के लिए धन्यवाद । (popcnt
/lzcnt
/को छोड़करtzcnt
, जिनके पास Intel CPUs पर एक झूठी निर्भरता है , भले ही यह केवल लिखने योग्य हो। अर्थात WAW को रॉ के खतरे + एक लेखन के रूप में संभाला जा रहा है)। मेमोरी ऑर्डर करने के लिए, आधुनिक सीपीयू सेवानिवृत्ति तक कैश में देरी करने के लिए स्टोर कतारों का उपयोग करते हैं, साथ ही WAR और WAW खतरों से भी बचते हैं ।
एगनर के निर्देश तालिकाओं से अलग, हवेलवेल पर केवल 3 चक्र क्यों लगते हैं? एफपी डॉट उत्पाद लूप में एफएमए विलंबता का नाम बदलने और छिपाने के बारे में अधिक है।
"आई 7" ब्रांड-नाम नेहेलम ( कोर 2 के उत्तराधिकारी) के साथ पेश किया गया था , और कुछ इंटेल मैनुअल भी "कोर आई 7" कहते हैं, जब वे नेहेलम का मतलब समझते हैं, लेकिन उन्होंने सैंडब्रिज और बाद में माइक्रोआर्किटेक्चर्स के लिए "i7" ब्रांडिंग रखी । SnB है जब P6- परिवार एक नई प्रजाति, SnB- परिवार में विकसित हुआ । कई मायनों में, नेहल में सैंडियम के साथ पेंटियम III की तुलना में अधिक आम है (उदाहरण के लिए रजिस्टर स्टॉल और आरओबी-रीड स्टॉल एसएनबी पर नहीं होते हैं, क्योंकि यह एक भौतिक रजिस्टर फ़ाइल का उपयोग करने के लिए बदल गया है। इसके अलावा एक यूओपी कैश और एक अलग आंतरिक कोड भी है। यूओपी प्रारूप)। "I7 आर्किटेक्चर" शब्द उपयोगी नहीं है, क्योंकि यह SnB- परिवार को Nehalem के साथ समूहित करने के लिए बहुत कम मायने रखता है, लेकिन Core2 नहीं। (नेहेलम ने कई कोर को एक साथ जोड़ने के लिए साझा समावेशी L3 कैश आर्किटेक्चर का परिचय दिया, हालांकि। और एकीकृत GPU भी। इसलिए चिप-स्तर, नामकरण अधिक समझ में आता है।)
अच्छे विचारों का सारांश जो शैतानी अक्षमता को सही ठहरा सकते हैं
यहां तक कि शैतानी अक्षमता भी स्पष्ट रूप से बेकार काम या एक अनंत लूप को जोड़ने की संभावना नहीं है, और सी ++ / बूस्ट कक्षाओं के साथ गड़बड़ करना असाइनमेंट के दायरे से परे है।
- एकल साझा
std::atomic<uint64_t>
लूप काउंटर के साथ बहु-धागा , इसलिए सही पुनरावृत्तियों की कुल संख्या होती है। परमाणु uint64_t के साथ विशेष रूप से खराब है -m32 -march=i586
। बोनस बिंदुओं के लिए, इसे गलत बताए जाने की व्यवस्था करें, और एक असमान विभाजन (4: 4 नहीं) के साथ एक पृष्ठ सीमा पार करें।
- कुछ अन्य गैर-परमाणु चर के लिए गलत साझाकरण -> मेमोरी-ऑर्डर मिस-स्पेकुलेशन पाइपलाइन को साफ करता है, साथ ही अतिरिक्त कैश मिस भी करता है।
-
एफपी चर पर उपयोग करने के बजाय , XOR उच्च बाइट 0x80 के साथ साइन बिट को फ्लिप करने के लिए, जिससे स्टोर-फ़ॉरवर्डिंग स्टॉल होता है ।
- स्वतंत्र रूप से प्रत्येक पुनरावृत्ति का समय, इससे भी भारी कुछ के साथ
RDTSC
। जैसे CPUID
/ RDTSC
या एक समय फ़ंक्शन जो एक सिस्टम कॉल करता है। सीरियलाइज़िंग निर्देश स्वाभाविक रूप से पाइपलाइन-अनफ्रेंडली हैं।
- स्थिरांक द्वारा गुणकों को उनके पारस्परिक ("पढ़ने में आसानी के लिए") से विभाजित करने के लिए बदलें। div धीमा है और पूरी तरह से पाइपलाइन नहीं है।
- AVX (SIMD) के साथ गुणा / sqrt को सदिश करें, लेकिन
vzeroupper
स्केलर मैथ-लाइब्रेरी exp()
और log()
फ़ंक्शंस पर कॉल करने से पहले उपयोग करने में विफल रहें , जिससे AVX <-> SSE ट्रांस्फ़ॉर्म स्टॉल ।
- किसी लिंक की गई सूची में, या सरणियों में RNG आउटपुट को स्टोर करें जिसे आप ऑर्डर से बाहर निकालते हैं। प्रत्येक पुनरावृत्ति के परिणाम के लिए समान, और अंत में योग।
इस उत्तर में भी कवर किया गया है, लेकिन सारांश से बाहर रखा गया है: सुझाव जो कि एक गैर-पिपेलिनेटेड सीपीयू पर धीमे होंगे, या यह शैतानी अक्षमता के साथ भी उचित नहीं लगता है। उदाहरण के लिए कई जिम्प-इन-कंपाइलर विचार जो स्पष्ट रूप से अलग / बदतर एस्मा का उत्पादन करते हैं।
बहु-धागा बुरी तरह से
शायद बहुत कम पुनरावृत्तियों के साथ मल्टी-थ्रेड लूप्स के लिए ओपनएमपी का उपयोग करें, जिस तरह से गति से अधिक ओवरहेड होता है। आपके मोंटे-कार्लो कोड में वास्तव में स्पीडअप प्राप्त करने के लिए पर्याप्त समानता है, हालांकि, एस्प। यदि हम प्रत्येक पुनरावृत्ति को धीमा करने में सफल होते हैं। (प्रत्येक थ्रेड एक आंशिक गणना करता है payoff_sum
, अंत में जोड़ा जाता है)। #omp parallel
उस पाश पर शायद एक अनुकूलन होगा, निराशावाद नहीं।
बहु-धागा लेकिन दोनों थ्रेड्स को एक ही लूप काउंटर को साझा करने के लिए मजबूर करते हैं ( atomic
वेतन वृद्धि के साथ इसलिए पुनरावृत्तियों की कुल संख्या सही है)। यह शैतानी तार्किक लगता है। इसका अर्थ static
है लूप काउंटर के रूप में एक चर का उपयोग करना । यह atomic
लूप काउंटर के लिए उपयोग को सही ठहराता है , और वास्तविक कैश-लाइन पिंग-पॉन्गिंग बनाता है (जब तक कि थ्रेड हाइपरथ्रेडिंग के साथ एक ही भौतिक कोर पर नहीं चलते हैं; वह उतना धीमा नहीं हो सकता है )। वैसे भी, यह के लिए संयुक्त राष्ट्र के मामले से बहुत धीमी है lock inc
। और lock cmpxchg8b
एटमॉली इन्क्रीमेंट के लिए uint64_t
32 बिट सिस्टम पर एक कंटेस्टेंट को हार्डवेयर एटम होने की बजाय लूप में रिट्रीट करना होगा inc
।
झूठी साझाकरण भी बनाएं , जहां कई धागे एक ही कैश लाइन के अलग-अलग बाइट्स में अपने निजी डेटा (जैसे आरएनजी राज्य) को रखते हैं। (इसके बारे में इंटेल ट्यूटोरियल, देखने के लिए परफेक्ट काउंटर सहित) । इसके लिए एक माइक्रोआर्किटेक्चर-विशिष्ट पहलू है : इंटेल सीपीयू मेमोरी मिस-ऑर्डरिंग नहीं होने का अनुमान लगाता है, और कम से कम पी 4 पर यह पता लगाने के लिए एक मेमोरी-ऑर्डर मशीन-स्पष्ट संपूर्ण घटना है । हसवेल पर जुर्माना उतना बड़ा नहीं हो सकता। जैसा कि लिंक बताता है, एक lock
एड अनुदेश मानता है कि यह गलत अनुमानों से बचने के लिए होगा। एक सामान्य लोड अनुमान लगाता है कि जब लोड निष्पादित होता है और जब वह प्रोग्राम-ऑर्डर में रिटायर होता है, तो अन्य कोर कैश लाइन को अमान्य नहीं करेंगेजब तक आप उपयोग नहpause
) ं करते । lock
एड निर्देशों के बिना सच साझा करना आमतौर पर एक बग है। परमाणु मामले के साथ गैर-परमाणु साझा लूप काउंटर की तुलना करना दिलचस्प होगा। वास्तव में pessimize करने के लिए, साझा परमाणु लूप काउंटर को रखें, और किसी अन्य चर के लिए समान या अलग कैश लाइन में गलत साझाकरण का कारण बनें।
रैंडम यार्क-विशिष्ट विचार:
यदि आप किसी भी अप्रत्याशित शाखाओं को पेश कर सकते हैं , तो यह कोड को काफी हद तक कम कर देगा। आधुनिक x86 सीपीयू में काफी लंबी पाइपलाइनें होती हैं, इसलिए एक गलत लागत की लागत ~ 15 चक्र (यूओपी कैश से चलने पर) होती है।
निर्भरता श्रृंखला:
मुझे लगता है कि यह असाइनमेंट के इच्छित भागों में से एक था।
सीपीयू की क्षमता को कई छोटी निर्भरता श्रृंखलाओं के बजाय एक लंबी निर्भरता श्रृंखला के संचालन के एक आदेश को चुनकर अनुदेश-स्तरीय समानता का दोहन करने की क्षमता को हराएं। जब तक आप उपयोग नहीं करते -ffast-math
, तब तक कंपाइलर्स को FP गणनाओं के संचालन के क्रम को बदलने की अनुमति नहीं है , क्योंकि इससे परिणाम बदल सकते हैं (जैसा कि नीचे चर्चा की गई है)।
वास्तव में इसे प्रभावी बनाने के लिए, लूप-आधारित निर्भरता श्रृंखला की लंबाई बढ़ाएं। कुछ भी स्पष्ट नहीं है, यद्यपि: लिखे गए लूप में बहुत कम लूप-निर्भर निर्भरता श्रृंखला होती है: बस एक एफपी ऐड। (3 चक्र)। एकाधिक पुनरावृत्तियों में एक बार में उनकी गणना हो सकती है, क्योंकि वे payoff_sum +=
पिछले पुनरावृत्ति के अंत से पहले अच्छी तरह से शुरू कर सकते हैं । ( log()
और exp
कई निर्देश लेते हैं, लेकिन समानता खोजने के लिए हसवेल के आउट-ऑफ-ऑर्डर विंडो से बहुत अधिक नहीं है : आरओबी आकार = 192 फ्यूज्ड-डोमेन यूओपी, और शेड्यूलर आकार = 60 अप्रयुक्त-डोमेन यूपीएस। जैसे ही वर्तमान पुनरावृत्ति का निष्पादन आगे बढ़ने के लिए अगले पुनरावृत्ति से निर्देश के लिए जगह बनाने के लिए पर्याप्त होता है, इसके किसी भी हिस्से में जो उनके इनपुट तैयार होते हैं (यानी स्वतंत्र / अलग डिप चेन) जब पुराने निर्देश निष्पादन इकाइयों को छोड़ देते हैं तो निष्पादन शुरू हो सकता है। मुक्त (उदाहरण के लिए क्योंकि वे विलंबता पर अड़चन हैं, न कि थ्रूपुट।)।
RNG राज्य लगभग निश्चित रूप से लूप-आधारित निर्भरता श्रृंखला होगा addps
।
धीमी / अधिक एफपी संचालन (esp। अधिक डिवीजन) का उपयोग करें:
0.5 से गुणा करने के बजाय 2.0 से विभाजित करें, और इसी तरह। एफपी मल्टी इंटेल के डिजाइनों में बड़े पैमाने पर पाइपलाइज्ड है, और हसवेल और बाद में प्रति 0.5c थ्रूपुट है। एफपी divsd
/ divpd
केवल आंशिक रूप से पाइपलाइज्ड है । (हालांकि divpd xmm
, स्काईलेक के पास 4c थ्रूपुट के लिए एक प्रभावशाली है , 13-14c विलंबता के साथ, बनाम नेहेल्म (7-22c) पर बिल्कुल भी पाइपलाइन नहीं किया गया है)।
do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);
स्पष्ट रूप से एक दूरी के लिए परीक्षण कर रहा है, तो स्पष्ट रूप से यह करने के लिए उचित होगा sqrt()
यह। : पी ( sqrt
से भी धीमी है div
)।
जैसा कि @Paul क्लेटन सुझाव देते हैं, साहचर्य / वितरण समकक्षों के साथ अभिव्यक्ति को फिर से लिखना अधिक काम का परिचय दे सकता है (जब तक आप -ffast-math
संकलक को फिर से अनुकूलित करने की अनुमति देने के लिए उपयोग नहीं करते हैं )। (exp(T*(r-0.5*v*v))
बन सकता है exp(T*r - T*v*v/2.0)
। ध्यान दें कि जबकि वास्तविक संख्या पर गणित साहचर्य है, चल बिन्दु गणित है नहीं , यहां तक कि अतिप्रवाह पर विचार किए बिना / NaN (जिसके कारण -ffast-math
डिफ़ॉल्ट रूप से चालू नहीं है)। बहुत बालों वाले नेस्टेड सुझाव के लिए पॉल की टिप्पणी देखें pow()
।
यदि आप गणनाओं को बहुत छोटी संख्याओं तक सीमित कर सकते हैं, तो दो सामान्य संख्याओं पर एक ऑपरेशन असामान्य होने पर FP गणित ऑप्स को माइक्रोकोड में फंसाने के लिए ~ 120 अतिरिक्त चक्र लेते हैं । सटीक संख्याओं और विवरणों के लिए Agner Fog का माइक्रोप्रिंट पीडीएफ देखें। यह बहुत अधिक होने के कारण संभावना नहीं है, इसलिए स्केल फैक्टर को चुकता किया जाएगा और 0.0 के सभी तरह से कम किया जाएगा। मैं अक्षमता (यहां तक कि शैतानी) के साथ आवश्यक स्केलिंग को सही ठहराने का कोई तरीका नहीं देखता, केवल जानबूझकर द्वेष करता हूं।
यदि आप आंतरिक ( <immintrin.h>
) का उपयोग कर सकते हैं
movnti
अपने डेटा को कैश से निकालने के लिए उपयोग करें । शैतानी: यह नया और कमजोर-क्रम वाला है, ताकि CPU को इसे और तेज चलाने दें, है ना? या उस मामले के लिए उस जुड़े हुए प्रश्न को देखें जहां किसी को ऐसा करने का खतरा था (बिखरे हुए लेखन के लिए जहां केवल कुछ स्थान गर्म थे)। clflush
शायद द्वेष के बिना असंभव है।
बायपास देरी का कारण एफपी गणित संचालन के बीच पूर्णांक फेरबदल का उपयोग करें।
vzeroupper
पूर्व-स्काईलेक (और स्काईलेक में एक अलग दंड) में बड़े स्टालों के कारणों के उचित उपयोग के बिना एसएसई और एवीएक्स निर्देशों का मिश्रण । इसके बिना भी, वेक्टरिंग बुरी तरह से स्केलर से भी बदतर हो सकती है (एक बार में 4 मोंटे-कार्लो पुनरावृत्तियों के लिए जोड़ / उप / mul / div / sqrt संचालन करने से बचाए गए डेटा की तुलना में वैक्टर में / से अधिक डेटा खर्च किए गए, 256b वैक्टर के साथ) । add / sub / mul निष्पादन इकाइयाँ पूरी तरह से पाइपलाइनयुक्त और पूर्ण-चौड़ाई वाली हैं, लेकिन 256b वैक्टर पर div और sqrt 128b वैक्टर (या स्केलर) पर उतने तेज़ नहीं हैं, इसलिए स्पीडअप के लिए नाटकीय नहीं हैdouble
।
exp()
और log()
हार्डवेयर समर्थन नहीं है, इसलिए उस हिस्से को वेक्टर तत्वों को स्केलर पर वापस लाने और लाइब्रेरी फ़ंक्शन को अलग से कॉल करने की आवश्यकता होगी, फिर परिणामों को एक वेक्टर में फेरबदल करेंगे। libm को आम तौर पर केवल SSE2 का उपयोग करने के लिए संकलित किया जाता है, इसलिए स्केलर गणित निर्देशों के विरासत-SSE एन्कोडिंग का उपयोग करेगा। यदि आपका कोड 256b वैक्टर का उपयोग करता है और पहले exp
किए बिना कॉल करता है vzeroupper
, तो आप स्टाल करते हैं। लौटने के बाद, एक AVX-128 निर्देश vmovsd
अगले वेक्टर तत्व को एक arg के रूप में सेट करने के लिए exp
भी स्टाल करेगा। और तब फिर exp()
से स्टाल होगा जब यह एक एसएसई निर्देश चलाता है। इस सवाल में ठीक ऐसा ही हुआ , जिससे 10 गुना मंदी आ गई। (साभार @ZBoson)।
इस कोड के लिए इंटेल के गणित के साथ नाथन कुर्ज़ के प्रयोगों को भी देखें । भविष्य glibc के साथ आ जाएगा की vectorized कार्यान्वयन exp()
इतने पर और।
यदि पूर्व-IvB, या esp को लक्षित किया गया है। नेहलेम, 16bit या 8bit संचालन के साथ आंशिक रूप से पंजीकृत स्टालों का कारण बनने के लिए gcc प्राप्त करने का प्रयास करें और उसके बाद 32bit या 64bit संचालन करें। ज्यादातर मामलों में, gcc movzx
8 या 16bit ऑपरेशन के बाद उपयोग करेगा , लेकिन यहाँ एक मामला है जहाँ gcc संशोधित होता है ah
और फिर पढ़ता हैax
इनलाइन (इनलाइन) के साथ:
इनलाइन (इनलाइन) एसम के साथ, आप यूओपी कैश को तोड़ सकते हैं: 32B कोड ऑफ कोड जो तीन 6uop कैश लाइनों में फिट नहीं होता है, यूओपी कैश से डिकोडर्स पर स्विच करने के लिए बाध्य करता है। इनर लूप के अंदर एक शाखा लक्ष्य पर एक जोड़े लंबे एस के बजाय ALIGN
कई सिंगल-बाइट का उपयोग करते हुए एक अक्षमता चाल हो सकती है। या पहले के बजाय लेबल के बाद संरेखण पैडिंग डालें। : P यह केवल तभी मायने रखता है जब फ्रंटएंड एक अड़चन है, जो कि ऐसा नहीं होगा यदि हम बाकी कोड को रोकने में सफल रहे।nop
nop
पाइपलाइन क्लीयर (उर्फ मशीन-नुक्स) को ट्रिगर करने के लिए स्व-संशोधित कोड का उपयोग करें।
8 बिट्स में फिट होने के लिए तत्काल बड़े के साथ 16bit निर्देशों के LCP स्टालों उपयोगी होने की संभावना नहीं है। SnB पर uop cache और बाद में इसका मतलब है कि आप केवल एक बार decode पेनल्टी का भुगतान करते हैं। Nehalem (पहला i7) पर, यह एक लूप के लिए काम कर सकता है जो 28 uop लूप बफर में फिट नहीं होता है। gcc कभी-कभी इस तरह के निर्देश उत्पन्न करेगा, तब भी -mtune=intel
और जब वह 32bit निर्देश का उपयोग कर सकता था।
समय के लिए एक सामान्य मुहावरा है CPUID
(क्रमबद्ध करने के लिए)RDTSC
। यह सुनिश्चित करने के लिए अलग से प्रत्येक पुनरावृत्ति का समय CPUID
/ पहले के निर्देशों के साथ पुन: व्यवस्थित नहीं किया गया है, जो चीजों को बहुत धीमा कर देगा । (वास्तविक जीवन में, समय का स्मार्ट तरीका यह है कि सभी पुनरावृत्तियों को एक साथ जोड़ा जाए, बजाय एक-दूसरे को अलग-अलग समय देने और उन्हें जोड़ने के)।RDTSC
RDTSC
बहुत सारी कैश मिस और अन्य मेमोरी स्लोडाउन का कारण
union { double d; char a[8]; }
अपने कुछ चरों के लिए उपयोग करें । केवल एक बाइट्स के लिए एक संकीर्ण स्टोर (या रीड-संशोधित-लिखें) करके स्टोर-फ़ॉरवर्डिंग स्टाल का कारण बनें। (उस विकी लेख में लोड / स्टोर कतारों के लिए बहुत से अन्य सूक्ष्मजैविकृत सामान शामिल हैं)। उदाहरण के लिए एक ऑपरेटर के बजाय सिर्फ उच्च बाइट पर XOR 0x80 का उपयोग करने के संकेत को फ्लिप करेंdouble
-
। शैतानी अक्षम डेवलपर ने सुना हो सकता है कि FP पूर्णांक की तुलना में धीमा है, और इस प्रकार पूर्णांक ऑप्स का उपयोग करके यथासंभव करने की कोशिश करते हैं। (SSE रजिस्टरों में FP गणित को लक्षित करने वाला एक बहुत अच्छा संकलक संभवतः इसको संकलित कर सकता हैxorps
एक और xmm रजिस्टर में एक निरंतरता के साथ, लेकिन x87 के लिए यह एकमात्र तरीका भयानक नहीं है यदि कंपाइलर को पता चलता है कि यह मूल्य को नकार रहा है और अगले जोड़ को घटाव के साथ बदल देता है।)
का प्रयोग करें volatile
अगर आप के साथ संकलित कर रहे हैं -O3
और नहीं का उपयोग कर std::atomic
, वास्तव में दुकान करने के लिए मजबूर करने के लिए संकलक / जगह भर से लोड करें। ग्लोबल वैरिएबल (स्थानीय लोगों के बजाय) कुछ स्टोर्स / रीलोड्स को भी बाध्य करेंगे, लेकिन C ++ मेमोरी मॉडल के कमजोर ऑर्डर के लिए कंपाइलर को हर समय मेमोरी को लोड / रीलोड करने की आवश्यकता नहीं होती है।
एक बड़ी संरचना के सदस्यों के साथ स्थानीय संस्करण बदलें, ताकि आप मेमोरी लेआउट को नियंत्रित कर सकें।
पैडिंग के लिए संरचना में सरणियों का उपयोग करें (और उनके अस्तित्व को सही ठहराने के लिए यादृच्छिक संख्याओं को संग्रहीत करना)।
अपना मेमोरी लेआउट चुनें ताकि L1 कैश में एक ही "सेट" में सब कुछ एक अलग लाइन में चला जाए । यह केवल 8-तरफा साहचर्य है, अर्थात प्रत्येक सेट में 8 "तरीके" हैं। कैश लाइनें 64B हैं।
इससे भी बेहतर है, चीजों को बिल्कुल 4096B अलग रखें, क्योंकि भार विभिन्न स्टोरों पर झूठी निर्भरता रखता है, लेकिन एक पृष्ठ के भीतर समान ऑफसेट के साथ । आक्रामक आउट-ऑफ-ऑर्डर सीपीयू मेमोरी डिसैम्बिगेशन का उपयोग यह पता लगाने के लिए करते हैं कि लोड और स्टोर को परिणामों को बदलने के बिना फिर से व्यवस्थित किया जा सकता है , और इंटेल के कार्यान्वयन में गलत-सकारात्मक हैं जो लोड को जल्दी शुरू होने से रोकते हैं। संभवतः वे केवल पृष्ठ ऑफसेट के नीचे बिट्स की जांच करते हैं, इसलिए टीएलबी द्वारा आभासी पृष्ठ से भौतिक पृष्ठ तक उच्च बिट्स का अनुवाद करने से पहले चेक शुरू हो सकता है। Agner के गाइड के साथ-साथ स्टीफन कैनन का उत्तर भी देखें, और इसी प्रश्न पर @Krazy Glew के उत्तर के अंत में एक अनुभाग भी देखें। (एंडी ग्लीव इंटेल के मूल पी 6 माइक्रोआर्किटेक्चर के आर्किटेक्ट में से एक थे।)
का प्रयोग करें __attribute__((packed))
ताकि वे कैश लाइन या यहाँ तक कि पेज सीमाओं अवधि आप गलत संरेखित चर जाने के लिए। (इसलिए एक लोड double
दो कैश-लाइनों से डेटा की जरूरत है)। कैश लाइनों और पृष्ठ रेखाओं को पार करने के अलावा किसी भी Intel i7 uarch में गलत लोड का कोई दंड नहीं है। कैश-लाइन स्प्लिट अभी भी अतिरिक्त चक्र लेते हैं । स्काइलेक नाटकीय रूप से पृष्ठ विभाजन भार के लिए दंड को 100 से 5 चक्र तक कम कर देता है। (धारा 2.1.3) । शायद समानांतर में दो पृष्ठ चलने में सक्षम होने से संबंधित है।
एक पृष्ठ पर विभाजित atomic<uint64_t>
होना चाहिए सबसे खराब स्थिति के बारे में , esp। यदि यह एक पृष्ठ में 5 बाइट्स और दूसरे पृष्ठ में 3 बाइट्स, या 4: 4 के अलावा कुछ भी है। यहां तक कि बीच में विभाजन भी कैश-लाइन विभाजन के लिए अधिक कुशल होते हैं कुछ यूरेश, IIRC पर 16B वैक्टर के साथ। alignas(4096) struct __attribute((packed))
आरएनजी परिणामों के लिए भंडारण के लिए एक सरणी सहित, सब कुछ (अंतरिक्ष को बचाने के लिए) डालें। काउंटर से पहले uint8_t
या uint16_t
किसी चीज का उपयोग करके मिसलिग्न्मेंट प्राप्त करें ।
यदि आप संकलित पते मोड का उपयोग करने के लिए कंपाइलर प्राप्त कर सकते हैं, तो यह यूओपी माइक्रो-फ्यूजन को हरा देगा । शायद #define
सरल स्केलर चर को बदलने के लिए एस का उपयोग करके my_data[constant]
।
यदि आप एक अतिरिक्त स्तर का अप्रत्यक्ष परिचय दे सकते हैं, तो लोड / स्टोर एड्रेस को जल्दी नहीं जाना जा सकता है, जो आगे चलकर कम कर सकता है।
गैर-सन्निहित आदेश में अनुप्रस्थ सरणियाँ
मुझे लगता है कि हम पहली जगह में एक सरणी शुरू करने के लिए अक्षम औचित्य के साथ आ सकते हैं: यह हमें यादृच्छिक संख्या के उपयोग को यादृच्छिक संख्या के उपयोग से अलग करता है। प्रत्येक पुनरावृत्ति के परिणाम को एक सरणी में संग्रहीत किया जा सकता है, बाद में अभिव्यक्त किया जा सकता है (अधिक शैतानी अक्षमता के साथ)।
"अधिकतम यादृच्छिकता" के लिए, हम यादृच्छिक सरणी को उस पर नए यादृच्छिक संख्या लिखने पर थ्रेड लूपिंग कर सकते हैं। यादृच्छिक संख्या का उपभोग करने वाला धागा एक यादृच्छिक संख्या को लोड करने के लिए एक यादृच्छिक सूचकांक उत्पन्न कर सकता है। (यहां कुछ मेक-वर्क है, लेकिन माइक्रोऑरेक्टेक्टुरली यह लोड-एड्रेस को जल्दी ज्ञात करने में मदद करता है ताकि लोड किए गए डेटा की आवश्यकता होने से पहले किसी भी संभावित लोड विलंबता को हल किया जा सके।) अलग-अलग कोर पर एक रीडर और लेखक होने से मेमोरी-ऑर्डर गलत हो जाएगा। -पीसुलेशन पाइपलाइन को साफ करता है (जैसा कि पहले झूठे बंटवारे के मामले के लिए चर्चा की गई थी)।
अधिकतम निराशावाद के लिए, 4096 बाइट्स (यानी 512 डबल्स) के स्ट्राइड के साथ अपने एरे पर लूप करें। जैसे
for (int i=0 ; i<512; i++)
for (int j=i ; j<UPPER_BOUND ; j+=512)
monte_carlo_step(rng_array[j]);
तो एक्सेस पैटर्न 0, 4096, 8192, ...,
8, 4104, 8200, ...
16, 4112, 820%, ...
यह वह है जो आपको एक 2 डी सरणी तक पहुँचने के लिए मिलेगा जैसे double rng_array[MAX_ROWS][512]
कि गलत क्रम में (पंक्तियों पर लूपिंग करना, आंतरिक लूप में एक पंक्ति के भीतर कॉलम के बजाय, जैसा कि @JesperJuhl द्वारा सुझाया गया है)। अगर शैतानी अक्षमता उस तरह के आयामों के साथ 2 डी सरणी को सही ठहरा सकती है, तो बगीचे की विविधता वास्तविक दुनिया की अक्षमता आसानी से गलत पहुंच पैटर्न के साथ लूपिंग को सही ठहराती है। यह वास्तविक जीवन में वास्तविक कोड में होता है।
लूप सीमा को समायोजित करें यदि आवश्यक हो तो कुछ ही पृष्ठों का पुन: उपयोग करने के बजाय कई अलग-अलग पृष्ठों का उपयोग करें, यदि सरणी इतनी बड़ी नहीं है। हार्डवेयर प्रीफ़ेचिंग पृष्ठों के दौरान (साथ ही साथ / बिल्कुल भी) काम नहीं करता है। प्रीफ़ेचर प्रत्येक पृष्ठ के भीतर एक आगे और एक पिछड़ी धारा को ट्रैक कर सकता है (जो कि यहां होता है), लेकिन यह केवल उस पर कार्य करेगा यदि मेमोरी बैंडविड्थ पहले से ही गैर-प्रीफैच के साथ संतृप्त नहीं है।
यह तब तक बहुत सारी TLB मिस भी उत्पन्न करेगा, जब तक कि पेज एक विशाल पृष्ठ में विलय नहीं हो जाते ( लिनक्स यह अवसरवादी रूप से अनाम (फ़ाइल-समर्थित नहीं) आवंटन जैसे malloc
/ new
उस उपयोग के लिए करता हैmmap(MAP_ANONYMOUS)
)।
परिणामों की सूची संग्रहीत करने के लिए एक सरणी के बजाय, आप एक लिंक की गई सूची का उपयोग कर सकते हैं । तब प्रत्येक पुनरावृत्ति को एक पॉइंटर-चेज़िंग लोड (अगले लोड के लोड-पता के लिए एक रॉ निर्भरता खतरा) की आवश्यकता होगी। खराब एलोकेटर के साथ, आप कैश को हराकर, सूची नोड्स को मेमोरी में चारों ओर बिखेरने का प्रबंधन कर सकते हैं। शैतानी रूप से अक्षम आवंटनकर्ता के साथ, यह प्रत्येक नोड को अपने स्वयं के पृष्ठ की शुरुआत में रख सकता है। (उदाहरण के लिए, mmap(MAP_ANONYMOUS)
सीधे पेजों को तोड़ने या ऑब्जेक्ट साइज़ को ठीक से सपोर्ट करने के बिना, आवंटित करें free
)।
ये वास्तव में सूक्ष्म-विशिष्ट नहीं हैं, और इनका पाइपलाइन से कोई लेना-देना नहीं है (इनमें से अधिकांश गैर-पाइपलाइड सीपीयू पर एक मंदी भी होगी)।
कुछ हद तक विषय: संकलक को बदतर कोड बनाते हैं / अधिक काम करते हैं:
C ++ 11 का उपयोग करें std::atomic<int>
और std::atomic<double>
सबसे अधिक बार कोड के लिए। एमएफईएनसीई और lock
एड निर्देश किसी अन्य धागे से विवाद के बिना भी काफी धीमा हैं।
-m32
धीमी कोड बना देगा, क्योंकि x87 कोड SSE2 कोड से भी बदतर होगा। स्टैक-आधारित 32 बिट कॉलिंग कन्वेंशन अधिक निर्देश लेता है, और स्टैक पर भी एफपी की तरह कार्य करने के लिए गुजरता है exp()
। atomic<uint64_t>::operator++
पर -m32
एक की आवश्यकता है lock cmpxchg8B
पाश (i586)। (ताकि लूप काउंटर के लिए उपयोग करें! [बुराई हंसी])।
-march=i386
को भी कम करेगा (धन्यवाद @ जैस्पर)। एफपी की तुलना fcom
686 से धीमी है fcomi
। पूर्व -586 एक परमाणु 64 बिट स्टोर प्रदान नहीं करता है, (अकेले एक cmpxchg चलो), इसलिए सभी 64 बिट atomic
ऑप्स libgcc फ़ंक्शन कॉल के लिए संकलित करते हैं (जो कि वास्तव में i686 के लिए संकलित किया जाता है, वास्तव में लॉक का उपयोग करने के बजाय)। अंतिम पैराग्राफ में Godbolt Compiler Explorer लिंक पर इसे आज़माएँ।
ABI में अतिरिक्त सटीकता और अतिरिक्त सुस्ती के लिए long double
/ sqrtl
/ का उपयोग करें expl
जहां आकार ( long double
) 10 या 16 है (संरेखण के लिए पैडिंग के साथ)। (IIRC, 64 बिट विंडोज के long double
बराबर 8byte का उपयोग करता है double
। (वैसे भी, 10byte (80bit) का लोड / स्टोर) FP ऑपरेंड्स 4/7 uops है, बनाम float
या double
केवल 1 के लिए प्रत्येक यूओपी ले रहा है fld m64/m32
/ fst
) फोर्स को long double
ऑटो- वैरिफिकेशन के लिए भी x87 मजबूर करना। जीसीसी -m64 -march=haswell -O3
।
यदि atomic<uint64_t>
लूप काउंटर का उपयोग नहीं कर long double
रहे हैं, तो लूप काउंटर सहित सभी चीजों के लिए उपयोग करें ।
atomic<double>
संकलन, लेकिन पढ़ने-संशोधित-लिखने के संचालन जैसे +=
इसके लिए समर्थित नहीं हैं (64 बिट पर भी)। atomic<long double>
सिर्फ परमाणु भार / भंडार के लिए एक पुस्तकालय समारोह को कॉल करना है। यह शायद वास्तव में अक्षम है, क्योंकि x86 ISA स्वाभाविक रूप से परमाणु 10byte भार / स्टोर का समर्थन नहीं करता है , और जिस तरह से मैं लॉक किए बिना सोच सकता हूं ( cmpxchg16b
64 बिट मोड की आवश्यकता होती है)।
पर -O0
, अस्थायी vars को भागों को असाइन करके एक बड़ी अभिव्यक्ति को तोड़ने से अधिक स्टोर / रीलोड हो जाएगा। बिना volatile
या कुछ और, यह अनुकूलन सेटिंग्स के साथ कोई फर्क नहीं पड़ेगा जो वास्तविक कोड का एक वास्तविक निर्माण उपयोग करेगा।
सी अलियासिंग नियमों में किसी char
भी चीज को उर्फ करने की अनुमति होती है , इसलिए char*
बाइट-स्टोर से पहले / बाद में सब कुछ स्टोर करने / फिर से लोड करने के लिए कंपाइलर के माध्यम से भंडारण करना चाहिए -O3
। (यह ऑटो-वेक्टरिंग कोड केuint8_t
लिए एक समस्या है जो उदाहरण के लिए, सरणी पर संचालित होता है ।)
uint16_t
लूप काउंटर्स को आज़माएं , 16bit पर ट्रंकेशन को मजबूर करने के लिए, संभवतः 16bit ऑपरेंड-साइज़ (संभावित स्टॉल) और / या अतिरिक्त movzx
निर्देशों (सुरक्षित) का उपयोग करके। हस्ताक्षरित अतिप्रवाह अपरिभाषित व्यवहार है , इसलिए जब तक आप उपयोग नहीं करते हैं -fwrapv
या कम से कम -fno-strict-overflow
, हस्ताक्षरित लूप काउंटरों को हर पुनरावृत्ति को पुन: साइन-विस्तारित नहीं करना पड़ता है , भले ही वह 64 बिट पॉइंट्स के लिए ऑफ़सेट के रूप में उपयोग किया जाए।
पूर्णांक से बल परिवर्तन float
और फिर से वापस। और / या double
<=> float
रूपांतरण। निर्देशों में अधिक से अधिक एक विलंबता है, और स्केलर int-> फ्लोट ( cvtsi2ss
) बुरी तरह से xmm रजिस्टर के बाकी को शून्य नहीं करने के लिए डिज़ाइन किया गया है। ( pxor
इस कारण से निर्भरताएं तोड़ने के लिए gcc एक अतिरिक्त सम्मिलित करता है ।)
बार-बार अपने CPU की आत्मीयता को एक अलग CPU (@Egwor द्वारा सुझाया गया) पर सेट करें । शैतानी तर्क: आप नहीं चाहते कि एक कोर आपके धागे को लंबे समय तक चलाने से गर्म हो जाए, क्या आप? हो सकता है कि किसी अन्य कोर को स्वैप करने से वह कोर टर्बो एक उच्चतर घड़ी की गति से हो। (वास्तव में: वे एक-दूसरे के इतने करीब हैं कि बहु-सॉकेट प्रणाली को छोड़कर यह अत्यधिक संभावना नहीं है)। अब बस ट्यूनिंग को गलत करें और इसे अक्सर करें। थ्रेड स्टेट को बचाने / बहाल करने में बिताए गए समय के अलावा, नए कोर में ठंडे एल 2 / एल 1 कैश, यूओपी कैश, और शाखा भविष्यवक्ता हैं।
बार-बार अनावश्यक सिस्टम कॉल का परिचय आपको धीमा कर सकता है, चाहे वे कुछ भी हों। यद्यपि gettimeofday
कर्नेल मोड में कोई संक्रमण नहीं होने के साथ कुछ महत्वपूर्ण लेकिन सरल जैसे उपयोगकर्ता-अंतरिक्ष में लागू किए जा सकते हैं। (लिनक्स पर glibc कर्नेल की मदद से ऐसा करता है, क्योंकि कर्नेल निर्यात कोड में होता है vdso
)।
सिस्टम कॉल ओवरहेड के लिए (कैश / टीएलबी सहित उपयोगकर्ता-अंतरिक्ष में लौटने के बाद छूट जाती है, न कि केवल संदर्भ स्विच ही), फ्लेक्सएससी पेपर में वर्तमान स्थिति के कुछ महान पूर्ण-काउंटर विश्लेषण हैं, साथ ही बैचिंग सिस्टम के लिए एक प्रस्ताव है। बड़े पैमाने पर बहु-थ्रेडेड सर्वर प्रक्रियाओं से कॉल।
while(true){}