एक कोड कैसे लिखता है जो प्रदर्शन को बेहतर बनाने के लिए CPU कैश का सबसे अच्छा उपयोग करता है?


159

यह एक व्यक्तिपरक प्रश्न की तरह लग सकता है, लेकिन मैं जो देख रहा हूं वह विशिष्ट उदाहरण हैं, जो आपको इस से संबंधित हो सकते हैं।

  1. कोड कैसे बनाएं, कैश प्रभावी / कैश फ्रेंडली (अधिक कैश हिट, जितना संभव हो उतना कम कैश)? दोनों दृष्टिकोणों से, डेटा कैश और प्रोग्राम कैश (इंस्ट्रक्शन कैश), यानी किसी के कोड में क्या चीजें हैं, डेटा संरचनाओं और कोड निर्माण से संबंधित हैं, किसी को इसे कैश प्रभावी बनाने के लिए ध्यान रखना चाहिए।

  2. क्या कोई विशेष डेटा संरचनाएं हैं जिनका किसी को उपयोग / अवहेलना करना चाहिए, या उस संरचना के सदस्यों तक पहुंचने का एक विशेष तरीका है आदि ... कैश को प्रभावी बनाने के लिए।

  3. क्या कोई भी कार्यक्रम निर्माण (यदि, के लिए, स्विच, ब्रेक, गोटो, ...), कोड-प्रवाह (यदि एक के अंदर के लिए, यदि एक के लिए अंदर है, आदि ...) एक का पालन करना चाहिए / इस मामले में बचना चाहिए?

मैं सामान्य रूप से कैश कुशल कोड बनाने से संबंधित व्यक्तिगत अनुभवों को सुनने के लिए उत्सुक हूं। यह किसी भी प्रोग्रामिंग भाषा (C, C ++, असेंबली, ...), कोई भी हार्डवेयर लक्ष्य (ARM, Intel, PowerPC, ...), कोई भी OS (Windows, Linux, S ymbian, ...), आदि हो सकता है। ।

विविधता इसे गहराई से समझने के लिए बेहतर बनाने में मदद करेगी।


1
इस बात परिचय एक सिंहावलोकन एक अच्छा देता है के रूप में youtu.be/BP6NxVxDQIs
schoetbi

उपरोक्त छोटा URL अब काम नहीं कर रहा है, यह बात करने के लिए पूर्ण URL है: youtube.com/watch?v=BP6NxVxDQIs
अभिनव उपाध्याय

जवाबों:


119

कैश उस समय की संख्या को कम करने के लिए होता है, जब सीपीयू मेमोरी अनुरोध पूरा होने का इंतजार कर रहा होता है (मेमोरी विलंबता से बचते हुए ), और दूसरे प्रभाव के रूप में, संभवतः डेटा की कुल मात्रा को कम करने के लिए जिसे स्थानांतरित करने की आवश्यकता होती है (संरक्षित करना) मेमोरी बैंडविड्थ )।

मेमोरी फ़िंच लेटेंसी से पीड़ित होने से बचने के लिए तकनीक आम तौर पर पहली बात है, और कभी-कभी एक लंबा रास्ता तय करने में मदद करती है। सीमित मेमोरी बैंडविड्थ भी एक सीमित कारक है, खासकर मल्टीकोर और मल्टीथ्रेड एप्लिकेशन के लिए जहां कई थ्रेड्स मेमोरी बस का उपयोग करना चाहते हैं। तकनीकों का एक अलग सेट बाद के मुद्दे को संबोधित करने में मदद करता है।

स्थानिक इलाके में सुधार का मतलब है कि आप यह सुनिश्चित करते हैं कि प्रत्येक कैश लाइन को एक बार कैश करने के लिए मैप किया गया हो। जब हमने विभिन्न मानक बेंचमार्क को देखा है, तो हमने देखा है कि उन लोगों का एक आश्चर्यजनक बड़ा हिस्सा कैश लाइनों को हटाए जाने से पहले 100% प्राप्त कैश लाइनों का उपयोग करने में विफल रहता है।

कैश लाइन उपयोग में सुधार से तीन तरह से मदद मिलती है:

  • यह कैश में अधिक उपयोगी डेटा को फिट करता है, अनिवार्य रूप से प्रभावी कैश आकार को बढ़ाता है।
  • यह एक ही कैश लाइन में अधिक उपयोगी डेटा को फिट करता है, जिससे संभावना है कि डेटा का अनुरोध कैश में पाया जा सकता है।
  • यह मेमोरी बैंडविड्थ की आवश्यकताओं को कम करता है, क्योंकि इसमें कम भ्रूण होंगे।

सामान्य तकनीकें हैं:

  • छोटे डेटा प्रकारों का उपयोग करें
  • संरेखण छेद से बचने के लिए अपने डेटा को व्यवस्थित करें (आकार को कम करके अपने सदस्यों को छाँटना एक तरह से है)
  • मानक डायनामिक मेमोरी एलोकेटर से सावधान रहें, जो छिद्रों को पेश कर सकता है और आपके डेटा को मेमोरी में चारों ओर फैला सकता है क्योंकि यह गर्म होता है।
  • सुनिश्चित करें कि सभी आसन्न डेटा वास्तव में गर्म छोरों में उपयोग किया जाता है। अन्यथा, डेटा संरचनाओं को गर्म और ठंडे घटकों में तोड़ने पर विचार करें, ताकि गर्म लूप गर्म डेटा का उपयोग करें।
  • ऐसे एल्गोरिदम और डेटास्ट्रक्टर्स से बचें जो अनियमित एक्सेस पैटर्न प्रदर्शित करते हैं, और रैखिक डेटास्ट्रक्चर का पक्ष लेते हैं।

हमें यह भी ध्यान देना चाहिए कि कैश का उपयोग करने की तुलना में मेमोरी लेटेंसी को छिपाने के अन्य तरीके हैं।

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

निर्देश को इस तरह से एकत्रित करना कि जो हमेशा कैश में छूट जाते हैं वे एक-दूसरे के करीब होते हैं, सीपीयू कभी-कभी इन भ्रूणों को ओवरलैप कर सकता है ताकि आवेदन केवल एक विलंबता हिट ( मेमोरी स्तर समानता ) को बनाए रखे ।

समग्र मेमोरी बस के दबाव को कम करने के लिए, आपको अस्थायी स्थानीयता कहा जाता है । इसका मतलब है कि आपको डेटा का पुन: उपयोग करना होगा जबकि यह अभी भी कैश से निकाला नहीं गया है।

एक ही डेटा ( लूप फ्यूजन ) को छूने वाले छोरों को विलय करना , और उन अतिरिक्त मेमोरी भ्रूणों से बचने के लिए सभी प्रयासों को टाइलिंग या अवरुद्ध करने वाली पुनर्लेखन तकनीकों को नियोजित करना ।

हालांकि इस पुन: लिखने के अभ्यास के लिए अंगूठे के कुछ नियम हैं, आपको आमतौर पर यह सुनिश्चित करना होगा कि आप लूप किए गए डेटा निर्भरता पर ध्यान दें, यह सुनिश्चित करने के लिए कि आप कार्यक्रम के शब्दार्थ को प्रभावित नहीं करते हैं।

ये चीजें वास्तव में मल्टीकोर दुनिया में भुगतान करती हैं, जहां आप आमतौर पर दूसरे धागे को जोड़ने के बाद बहुत अधिक थ्रूपुट सुधार नहीं देखते हैं।


5
जब हमने विभिन्न मानक बेंचमार्क को देखा है, तो हमने देखा है कि उन लोगों का एक आश्चर्यजनक बड़ा हिस्सा कैश लाइनों को हटाए जाने से पहले 100% प्राप्त कैश लाइनों का उपयोग करने में विफल रहता है। क्या मैं पूछ सकता हूं कि किस प्रकार के प्रोफाइलिंग टूल आपको इस तरह की जानकारी देते हैं, और कैसे?
ड्रैगन एनर्जी

"संरेखण छेद से बचने के लिए अपने डेटा को व्यवस्थित करें (आकार को कम करके अपने सदस्यों को छाँटना एक तरह से है)" - क्यों संकलक खुद को अनुकूलित नहीं करता है? क्यों संकलक हमेशा "आकार कम करके सदस्यों को क्रमबद्ध नहीं कर सकता"? सदस्यों को अनसुलझा रखने के लिए क्या लाभ है?
javapowered

मुझे पता है कि मूल नहीं है, लेकिन एक के लिए, सदस्य आदेश नेटवर्क संचार में महत्वपूर्ण है, जहां आप वेब पर बाइट द्वारा संपूर्ण संरचनाएं भेजना चाहते हैं।
कोबरा

1
@javapowered संकलक भाषा के आधार पर ऐसा करने में सक्षम हो सकता है, हालांकि मुझे यकीन नहीं है कि उनमें से कोई भी करता है। इसका कारण यह है कि आप इसे C में नहीं कर सकते हैं, यह आधार पते से सदस्यों को संबोधित करने के लिए पूरी तरह से मान्य है + नाम के बजाय ऑफसेट, जिसका अर्थ है कि सदस्यों को फिर से जोड़ना कार्यक्रम को पूरी तरह से तोड़ देगा।
डेन बेचर

56

मुझे विश्वास नहीं हो रहा है कि इस बारे में अधिक जवाब नहीं हैं। वैसे भी, एक क्लासिक उदाहरण एक बहुआयामी सरणी को "अंदर बाहर" करना है:

pseudocode
for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[j][i]

यह कैश अक्षम होने का कारण है क्योंकि आधुनिक सीपीयू कैश मेमोरी को मुख्य मेमोरी से "पास" मेमोरी एड्रेस के साथ लोड करेंगे जब आप सिंगल मेमोरी एड्रेस का उपयोग करते हैं। हम आंतरिक लूप में सरणी में "जे" (बाहरी) पंक्तियों के माध्यम से पुनरावृत्ति कर रहे हैं, इसलिए आंतरिक लूप के माध्यम से प्रत्येक यात्रा के लिए, कैश लाइन को फ्लश किया जाएगा और पते की एक पंक्ति के साथ लोड किया जाएगा जो [के पास हैं] j] [i] प्रविष्टि यदि इसे समकक्ष में बदल दिया जाए:

for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[i][j]

यह ज्यादा तेज चलेगी।


9
कॉलेज में वापस हमारे पास मैट्रिक्स गुणा पर एक असाइनमेंट था। यह पता चला है कि पहले "कॉलम" मैट्रिक्स का एक स्थानान्तरण लेना अधिक तेज़ था और उस सटीक कारण के लिए पंक्तियों के बजाय पंक्तियों द्वारा पंक्तियों को गुणा करना।
यकागानोविच

11
वास्तव में, अधिकांश आधुनिक कंपाइलर इसका पता लगा सकते हैं (इसके अनुकूलन के साथ)
रिकार्डो नोल्ड

1
@ykaganovich उलरिच ड्रेपर लेख में भी इसका उदाहरण है: lwn.net/Articles/255364
सिमोन स्टेंडर बोइसन

मुझे यकीन नहीं है कि यह हमेशा सही होता है - अगर पूरी सरणी L1 कैश (अक्सर 32k!) के भीतर फिट होती है, तो दोनों आदेशों में समान संख्या में कैश हिट्स और मिस होंगे। शायद स्मृति पूर्व लाने से मुझे लगता है कि कुछ प्रभाव पड़ सकता है। निश्चित रूप से सही होने की खुशी।
मैट पार्किंस

यदि ऑर्डर में कोई फर्क नहीं पड़ता तो कौन इस कोड का पहला संस्करण चुनेगा?
सिल्वर_क्रिकेट

45

मूल नियम वास्तव में काफी सरल हैं। जहां यह मुश्किल हो जाता है कि वे आपके कोड पर कैसे लागू होते हैं।

कैश दो सिद्धांतों पर काम करता है: अस्थायी स्थानीयता और स्थानिक इलाके। पूर्व का विचार है कि यदि आपने हाल ही में डेटा का एक निश्चित हिस्सा इस्तेमाल किया है, तो आपको शायद जल्द ही फिर से इसकी आवश्यकता होगी। उत्तरार्द्ध का मतलब है कि यदि आपने हाल ही में पता X पर डेटा का उपयोग किया है, तो आपको शायद जल्द ही पता X + 1 की आवश्यकता होगी।

कैश डेटा के सबसे हाल ही में इस्तेमाल किए गए चंक्स को याद करके इसे समायोजित करने की कोशिश करता है। यह कैश लाइनों के साथ काम करता है, आम तौर पर 128 बाइट या तो आकार का होता है, इसलिए भले ही आपको केवल एक बाइट की आवश्यकता हो, पूरी कैशे लाइन जिसमें यह कैश में खींच लिया जाता है। इसलिए यदि आपको निम्नलिखित बाइट की आवश्यकता है, तो यह पहले से ही कैश में होगा।

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

एक सरल उदाहरण 2 डी सरणी ट्रैवर्सल है जो 1800 का उत्तर दिखाता है। यदि आप इसे एक बार में एक पंक्ति में ले जाते हैं, तो आप क्रमिक रूप से मेमोरी पढ़ रहे हैं। यदि आप इसे कॉलम-वार करते हैं, तो आप एक प्रविष्टि पढ़ेंगे, फिर एक पूरी तरह से अलग स्थान पर जा सकते हैं (अगली पंक्ति की शुरुआत), एक प्रविष्टि पढ़ें, और फिर से कूदें। और जब आप अंततः पहली पंक्ति में वापस आते हैं, तो यह अब कैश में नहीं होगा।

कोड पर वही लागू होता है। कूद या शाखाओं का मतलब कम कुशल कैश उपयोग है (क्योंकि आप क्रमिक रूप से निर्देशों को नहीं पढ़ रहे हैं, लेकिन एक अलग पते पर कूद रहे हैं)। बेशक, छोटे अगर बयान कुछ भी नहीं बदलेंगे (आप केवल कुछ बाइट्स छोड़ रहे हैं, तो आप अभी भी कैश्ड क्षेत्र के अंदर समाप्त हो जाएंगे), लेकिन फ़ंक्शन कॉल आमतौर पर इसका मतलब है कि आप पूरी तरह से अलग हो रहे हैं पता जिसे कैश नहीं किया जा सकता है। जब तक कि इसे हाल ही में नहीं बुलाया गया।

अनुदेश कैश का उपयोग आमतौर पर हालांकि एक समस्या से कम है। डेटा कैश के बारे में आपको आमतौर पर चिंता करने की जरूरत है।

एक संरचना या वर्ग में, सभी सदस्यों को सन्निहित रूप से रखा जाता है, जो अच्छा है। एक सरणी में, सभी प्रविष्टियों को संचित रूप से निर्धारित किया गया है। लिंक की गई सूचियों में, प्रत्येक नोड को पूरी तरह से अलग स्थान पर आवंटित किया गया है, जो खराब है। सामान्य तौर पर पॉइंटर्स असंबंधित पते की ओर इशारा करते हैं, जिसके परिणामस्वरूप यदि आप इसे डीरेंस करते हैं तो संभवतः कैश मिस हो जाएगा।

और अगर आप कई कोर का शोषण करना चाहते हैं, तो यह वास्तव में दिलचस्प हो सकता है, जैसा कि आमतौर पर, एक समय में केवल एक सीपीयू का उसके एल 1 कैश में कोई भी पता हो सकता है। इसलिए यदि दोनों कोर लगातार एक ही पते पर पहुंचते हैं, तो इसका परिणाम लगातार कैश मिस होगा, क्योंकि वे पते पर लड़ रहे हैं।


4
+1, अच्छी और व्यावहारिक सलाह। एक जोड़: समय स्थानीयता और अंतरिक्ष इलाके संयुक्त रूप से सुझाव देते हैं कि उदाहरण के लिए मैट्रिक्स ऑप्स के लिए, उन्हें छोटे मेट्रिसेस में विभाजित करने की सलाह दी जा सकती है जो पूरी तरह से कैश लाइन में फिट होते हैं, या जिनकी पंक्तियां / कॉलम कैश लाइनों में फिट होते हैं। मुझे याद है कि मल्टीमिड के विज़ुअलाइज़ेशन के लिए। डेटा। इसने पैंट में कुछ गंभीर किक प्रदान की। यह याद रखना अच्छा है कि कैश एक से अधिक 'लाइन' रखता है;)
एंड्रियास

1
आप कहते हैं कि केवल 1 सीपीयू में एल 1 कैश में एक दिया गया पता आ सकता है - मेरा मानना ​​है कि आप पते के बजाय कैश लाइनों का मतलब है। इसके अलावा मैंने झूठी साझा समस्याओं के बारे में सुना है जब कम से कम एक सीपीयू लिखता है, लेकिन ऐसा नहीं है कि दोनों केवल पढ़ रहे हैं। तो 'पहुँच' से क्या आप वास्तव में लिखते हैं?
जोसेफ गार्विन

2
@ जोसेफगर्विन: हाँ, मेरा मतलब है कि लिखते हैं। आप सही हैं, कई कोर में एक ही समय में उनके L1 कैश में समान कैश लाइनें हो सकती हैं, लेकिन जब कोई कोर इन पते पर लिखता है, तो यह सभी अन्य L1 कैश में अमान्य हो जाता है, और फिर उन्हें इसे लोड करने से पहले इसे फिर से लोड करना होगा इसके साथ कुछ भी। गलत (गलत) शब्दांकन के लिए क्षमा करें। :)
जलेफ

44

मैं 9-भाग के लेख को पढ़ने की सलाह देता हूं कि क्या हर प्रोग्रामर को उलरिक ड्रेपर द्वारा मेमोरी के बारे में पता होना चाहिए, यदि आप रुचि रखते हैं कि मेमोरी और सॉफ्टवेयर कैसे इंटरैक्ट करते हैं। यह 104-पृष्ठ पीडीएफ के रूप में भी उपलब्ध है ।

इस प्रश्न के लिए विशेष रूप से प्रासंगिक अनुभाग भाग 2 (सीपीयू कैश) और भाग 5 (प्रोग्रामर क्या कर सकते हैं - कैश ऑप्टिमाइज़ेशन) हो सकते हैं।


16
आपको लेख से मुख्य बिंदुओं का सारांश जोड़ना चाहिए।
आज़मिसोव

ग्रेट रीड, लेकिन एक अन्य पुस्तक जिसका उल्लेख यहां किया जाना चाहिए वह है हेनेसी, पैटरसन, कंप्यूटर आर्किटेक्चर, ए क्वांटिटेटिव अप्रोच , जो आज तक इसके 5 वें संस्करण में उपलब्ध है।
हयमो कुत्स्चबैक

15

डेटा एक्सेस पैटर्न के अलावा, कैश-फ्रेंडली कोड का एक प्रमुख कारक डेटा आकार है । कम डेटा का मतलब है कि यह अधिक कैश में फिट बैठता है।

यह मुख्य रूप से स्मृति-संरेखित डेटा संरचनाओं के साथ एक कारक है। "परम्परागत" ज्ञान कहता है कि डेटा संरचनाओं को शब्द सीमाओं पर संरेखित किया जाना चाहिए क्योंकि सीपीयू केवल संपूर्ण शब्दों तक पहुंच सकता है, और यदि किसी शब्द में एक से अधिक मूल्य हैं, तो आपको अतिरिक्त काम करना होगा (एक साधारण लेखन के बजाय पढ़ना-संशोधित करना) । लेकिन कैश इस तर्क को पूरी तरह से अमान्य कर सकता है।

इसी प्रकार, जावा बूलियन सरणी प्रत्येक मान के लिए एक संपूर्ण बाइट का उपयोग करती है ताकि व्यक्तिगत मूल्यों पर सीधे संचालन की अनुमति मिल सके। यदि आप वास्तविक बिट्स का उपयोग करते हैं, तो आप 8 के कारक द्वारा डेटा आकार को कम कर सकते हैं, लेकिन फिर व्यक्तिगत मूल्यों तक पहुंच बहुत अधिक जटिल हो जाती है, बिट शिफ्ट और मास्क संचालन की आवश्यकता होती है ( BitSetवर्ग आपके लिए ऐसा करता है)। हालांकि, कैश प्रभाव के कारण, यह अभी भी बूलियन [] का उपयोग करने से काफी तेज हो सकता है जब सरणी बड़ी हो। IIRC I ने एक बार 2 या 3 के कारक इस तरह से एक गति प्राप्त की।


9

कैश के लिए सबसे प्रभावी डेटा संरचना एक सरणी है। कैश सबसे अच्छा काम करते हैं, यदि आपके डेटा संरचना को क्रमिक रूप से निर्धारित किया जाता है, क्योंकि सीपीयू मुख्य मेमोरी से एक बार में पूरी कैश लाइनें (आमतौर पर 32 बाइट्स या अधिक) पढ़ते हैं।

कोई भी एल्गोरिथ्म जो रैंडम ऑर्डर में मेमोरी एक्सेस करता है, कैश को चकरा देता है क्योंकि इसे हमेशा रैंडमली एक्सेस की गई मेमोरी को आॅन करने के लिए नई कैशे लाइनों की जरूरत होती है। दूसरी ओर एक एल्गोरिथ्म, जो एक सरणी के माध्यम से क्रमिक रूप से चलता है क्योंकि सबसे अच्छा है:

  1. यह सीपीयू को आगे पढ़ने का मौका देता है, उदाहरण के लिए सट्टा को अधिक मेमोरी को कैश में डाल दिया जाता है, जिसे बाद में एक्सेस किया जाएगा। यह पढ़ने-आगे एक विशाल प्रदर्शन को बढ़ावा देता है।

  2. एक बड़े सरणी पर एक तंग लूप चलाने से सीपीयू को लूप में निष्पादित कोड को कैश करने की अनुमति मिलती है और ज्यादातर मामलों में आप बाहरी मेमोरी एक्सेस के लिए ब्लॉक किए बिना कैश मेमोरी से एल्गोरिथ्म को पूरी तरह से निष्पादित करने की अनुमति देता है।


@Grover: अपनी बात के बारे में 2. तो कोई यह कह सकता है कि यदि insidea तंग लूप, प्रत्येक लूप काउंट के लिए एक फंक्शन बुलाया जा रहा है, तो यह पूरी तरह से नया कोड लाएगा और कैशे मिस हो जाएगा, इसके बजाय यदि आप फ़ंक्शन को एक के रूप में रख सकते हैं लूप के लिए कोड ही, कोई फ़ंक्शन कॉल नहीं, कम कैश मिस के कारण यह तेजी से होगा?
सुनहरा

1
हां और ना। नया फ़ंक्शन कैश में लोड किया जाएगा। यदि पर्याप्त कैश स्थान है, तो दूसरे पुनरावृत्ति पर यह पहले से ही कैश में कार्य करेगा, इसलिए इसे फिर से लोड करने का कोई कारण नहीं है। तो यह पहली कॉल पर हिट है। C / C ++ में आप कंपाइलर को उपयुक्त सेगमेंट का उपयोग करके एक दूसरे के बगल में फंक्शन लगाने के लिए कह सकते हैं।
ग्रोवर

एक और ध्यान दें: यदि आप लूप से बाहर जाते हैं और पर्याप्त कैश स्थान नहीं है, तो नए फ़ंक्शन को कैश में लोड किया जाएगा। यह भी हो सकता है कि मूल लूप को कैश से बाहर निकाल दिया जाएगा। इस मामले में कॉल प्रत्येक पुनरावृत्ति के लिए तीन दंड तक का होगा: एक कॉल लक्ष्य को लोड करने के लिए और दूसरा लूप को पुनः लोड करने के लिए। और तीसरा यदि लूप हेड कॉल रिटर्न एड्रेस के समान कैश लाइन में नहीं है। उस मामले में लूप सिर पर कूदने के लिए भी एक नई मेमोरी एक्सेस की आवश्यकता होती है।
ग्रोवर

8

एक उदाहरण जो मैंने एक गेम इंजन में उपयोग किया था, वह वस्तुओं से डेटा को अपने स्वयं के सरणियों में स्थानांतरित करना था। एक गेम ऑब्जेक्ट जो भौतिकी के अधीन था, उसमें बहुत सारे अन्य डेटा भी संलग्न हो सकते हैं। लेकिन भौतिकी अपडेट लूप के दौरान सभी इंजन की स्थिति, गति, द्रव्यमान, बाउंडिंग बॉक्स इत्यादि के बारे में डेटा था, इसलिए उन सभी को अपने स्वयं के सरणियों में रखा गया था और एसएसई के लिए जितना संभव हो अनुकूलित किया गया था।

इसलिए भौतिकी लूप के दौरान भौतिकी डेटा वेक्टर गणित का उपयोग करके सरणी क्रम में संसाधित किया गया था। गेम ऑब्जेक्ट्स ने अपनी ऑब्जेक्ट आईडी को विभिन्न सरणियों में सूचकांक के रूप में उपयोग किया। यह एक पॉइंटर नहीं था क्योंकि यदि सरणियों को स्थानांतरित करना पड़ता था तो पॉइंटर्स अवैध हो सकते थे।

कई मायनों में यह ऑब्जेक्ट-ओरिएंटेड डिज़ाइन पैटर्न का उल्लंघन करता है लेकिन इसने डेटा को एक साथ रखकर कोड को बहुत तेज़ कर दिया है जिसे एक ही छोर पर संचालित करने की आवश्यकता है।

यह उदाहरण संभवत: पुराना है, क्योंकि मुझे उम्मीद है कि अधिकांश आधुनिक खेल हॉक की तरह एक पूर्वनिर्मित भौतिकी इंजन का उपयोग करते हैं।


2
+1 पुराना नहीं है। खेल इंजनों के लिए डेटा व्यवस्थित करने का यह सबसे अच्छा तरीका है - डेटा ब्लॉक को सन्निहित बनाएं, और कैश निकटता / इलाके का लाभ उठाने के लिए अगली (कहें भौतिकी) पर जाने से पहले सभी दिए गए प्रकार के ऑपरेशन (एआई) कहें। संदर्भ।
इंजीनियर

मैंने कुछ हफ़्ते पहले एक वीडियो में इसका सटीक उदाहरण देखा था, लेकिन जब से यह लिंक खो गया है / यह याद नहीं है कि इसे कैसे पाया जाए। क्या आप इस उदाहरण को देखते हैं?
होगा

@will: नहीं, मुझे ठीक से याद नहीं है कि यह कहाँ था।
ज़ेन लिंक्स

यह एक इकाई घटक प्रणाली (ECS: en.wikipedia.org/wiki/Entity_component_system ) का बहुत विचार है । OOP प्रथाओं को प्रोत्साहित करने वाले अधिक पारंपरिक सरणी-संरचना की बजाय संरचना-ऑफ़-सरणियों के रूप में डेटा संग्रहीत करें।
BuschnicK

7

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


7

1800 क्लासिक उपयोगकर्ता द्वारा "क्लासिक उदाहरण" के लिए एक टिप्पणी (एक टिप्पणी के लिए बहुत लंबा)

मैं दो पुनरावृत्ति आदेशों ("आउट्टर" और "इनर") के समय के अंतर की जांच करना चाहता था, इसलिए मैंने एक बड़े 2D सरणी के साथ एक सरल प्रयोग किया:

measure::start();
for ( int y = 0; y < N; ++y )
for ( int x = 0; x < N; ++x )
    sum += A[ x + y*N ];
measure::stop();

और दूसरा मामला for छोरों के अदला-बदली।

धीमी संस्करण ("x पहला") 0.88sec था और सबसे तेज़ वाला, 0.06sec था। यह कैशिंग की शक्ति है :)

मैंने इस्तेमाल किया gcc -O2और अभी भी छोरों को अनुकूलित नहीं किया गया था। रिकार्डो की टिप्पणी है कि "अधिकांश आधुनिक संकलक इसके द्वारा इसका पता लगा सकते हैं" पकड़ नहीं है


यकीन नहीं होता कि मुझे यह मिल गया है। दोनों उदाहरणों में आप लूप के लिए प्रत्येक वैरिएबल तक पहुंच रहे हैं। एक रास्ता दूसरे की तुलना में तेज क्यों है?
संस्करण- ४

अंततः मेरे लिए यह समझना कि यह कैसे प्रभावित करता है :)
Laie

@EdwardCorlew यह उस क्रम के कारण है जिसमें वे एक्सेस किए जाते हैं। Y-पहला क्रम तेज़ है क्योंकि यह डेटा को क्रमिक रूप से एक्सेस करता है। जब पहली प्रविष्टि का अनुरोध किया जाता है तो L1 कैश एक संपूर्ण कैश-लाइन को लोड करता है, जिसमें अगले 15 के साथ-साथ अगले अनुरोध को शामिल करना (64-बाइट कैश-लाइन मानकर) शामिल है, इसलिए अगले 15 की प्रतीक्षा में कोई सीपीयू स्टाल नहीं है। x -फर्स्ट ऑर्डर धीमा है क्योंकि एक्सेस किया गया तत्व अनुक्रमिक नहीं है, और संभवतः एन इतना बड़ा है कि एक्सेस की जा रही मेमोरी हमेशा L1 कैश के बाहर होती है और इसलिए हर ऑपरेशन स्टॉल होता है।
मैट पार्किन

4

मैं (2) यह कहकर जवाब दे सकता हूं कि सी ++ दुनिया में, लिंक्ड सूची आसानी से सीपीयू कैश को मार सकती है। जहां संभव हो, एरियर्स एक बेहतर उपाय है। इस पर कोई अनुभव नहीं कि क्या अन्य भाषाओं पर भी लागू होता है, लेकिन यह कल्पना करना आसान है कि समान मुद्दे उत्पन्न होंगे।


@ और: कैसे संरचनाओं के बारे में। क्या वे कैश कुशल हैं? क्या उनके पास कैश के कुशल होने के लिए कोई आकार की कमी है?
सुनहरा

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

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

यकीन है, लेकिन यह बहुत काम हो रहा है std :: सूची <> एट अल। अपने कस्टम मेमोरी ब्लॉक्स का उपयोग करने के लिए। जब मैं एक युवा व्हिपस्नर था, तो मैं बिल्कुल उस रास्ते पर जाऊंगा, लेकिन इन दिनों ... बहुत सी अन्य चीजों से निपटने के लिए।
एंड्रयू


4

कैश को "कैश लाइनों" में व्यवस्थित किया जाता है और (वास्तविक) मेमोरी को इस आकार के भाग से पढ़ा और लिखा जाता है।

डेटा संरचनाएं जो एकल कैश-लाइन के भीतर निहित हैं, इसलिए अधिक कुशल हैं।

इसी तरह, एल्गोरिदम जो सन्निहित मेमोरी ब्लॉक तक पहुंचते हैं, वे एल्गोरिदम की तुलना में अधिक कुशल होंगे जो एक यादृच्छिक क्रम में मेमोरी के माध्यम से कूदते हैं।

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


जरुरी नहीं। बस झूठे बंटवारे को लेकर सावधान रहें। कभी-कभी आपको डेटा को अलग-अलग कैश लाइनों में विभाजित करना पड़ता है। कितना प्रभावी है कैश हमेशा इस पर निर्भर करता है कि आप इसका उपयोग कैसे करते हैं।
DAG

4

यह पूछने के लिए कि एक कोड कैसे बनाया जाए, कैश प्रभावी-कैश फ्रेंडली और अधिकांश अन्य प्रश्न, आमतौर पर यह पूछा जाता है कि किसी प्रोग्राम को कैसे ऑप्टिमाइज़ किया जाए, ऐसा इसलिए है क्योंकि कैश का प्रदर्शन पर इतना बड़ा प्रभाव पड़ता है कि कोई भी अनुकूलित प्रोग्राम कैश है। प्रभावी-कैश फ्रेंडली।

मैं अनुकूलन के बारे में पढ़ने का सुझाव देता हूं, इस साइट पर कुछ अच्छे उत्तर हैं। पुस्तकों के संदर्भ में, मैं कंप्यूटर सिस्टम पर सलाह देता हूं : एक प्रोग्रामर का परिप्रेक्ष्य जिसमें कैश के उचित उपयोग के बारे में कुछ ठीक पाठ है।

(btw - कैश-मिस जितना बुरा हो सकता है, उतना ही बुरा है - अगर कोई प्रोग्राम हार्ड-ड्राइव से पेजिंग कर रहा है ...)


4

सामान्य सलाह जैसे डेटा संरचना चयन, एक्सेस पैटर्न, आदि पर बहुत सारे उत्तर दिए गए हैं। यहां मैं एक अन्य कोड डिज़ाइन पैटर्न जोड़ना चाहूंगा जिसे सॉफ्टवेयर पाइपलाइन कहा जाता है जो सक्रिय कैश प्रबंधन का उपयोग करता है।

विचार अन्य पाइपलाइनिंग तकनीकों से उधार लिया गया है, जैसे सीपीयू अनुदेश पाइपलाइनिंग।

इस प्रकार का पैटर्न उन प्रक्रियाओं पर सबसे अच्छा लागू होता है जो

  1. उचित कई उप-चरणों को तोड़ा जा सकता है, एस [1], एस [2], एस [3], ... जिसका निष्पादन समय लगभग रैम एक्सेस समय (~ 60-70ns) के साथ तुलनीय है।
  2. इनपुट का एक बैच लेता है और परिणाम प्राप्त करने के लिए उन पर कई कदम उठाता है।

चलो एक साधारण मामला लेते हैं जहां केवल एक उप-प्रक्रिया है। आम तौर पर कोड चाहेंगे:

def proc(input):
    return sub-step(input))

बेहतर प्रदर्शन करने के लिए, आप एक बैच में फ़ंक्शन के लिए कई इनपुट पास करना चाह सकते हैं ताकि आप फ़ंक्शन कॉल को ओवरहेड कर सकें और कोड कैश लोकेलिटी भी बढ़ा सके।

def batch_proc(inputs):
    results = []
    for i in inputs:
        // avoids code cache miss, but still suffer data(inputs) miss
        results.append(sub-step(i))
    return res

हालाँकि, जैसा कि पहले कहा गया था, यदि चरण का निष्पादन लगभग राम की पहुंच के समय के समान है, तो आप कोड को कुछ इस तरह से सुधार सकते हैं:

def batch_pipelined_proc(inputs):
    for i in range(0, len(inputs)-1):
        prefetch(inputs[i+1])
        # work on current item while [i+1] is flying back from RAM
        results.append(sub-step(inputs[i-1]))

    results.append(sub-step(inputs[-1]))

निष्पादन प्रवाह इस तरह दिखेगा:

  1. प्रीफ़ेच (1) सीपीयू से इनपुट [1] को कैश में प्रीफ़ैच करने के लिए कहता है, जहाँ प्रीफ़ैच इंस्ट्रक्शन पी चक्रों को खुद लेता है और वापस लौटता है, और पृष्ठभूमि इनपुट [1] में आर साइकिल के बाद कैश में आ जाएगा।
  2. works_on (0) को 0 पर कोल्ड मिस करता है और उस पर काम करता है, जो M लेता है
  3. प्रीफ़ेच (2) एक और भ्रूण जारी करता है
  4. works_on (1) यदि P + R <= M है, तो इस कदम से पहले इनपुट [1] कैश में होना चाहिए, इस प्रकार डेटा कैश मिस से बचें
  5. works_on (2) ...

इसमें और भी कई चरण शामिल हो सकते हैं, तब आप एक मल्टी-स्टेज पाइपलाइन डिज़ाइन कर सकते हैं जब तक कि स्टेप्स और मैमोरी एक्सेस लेटेंसी मैच की टाइमिंग नहीं हो जाती, आपको कम कोड / डेटा कैशे मिस करना होगा। हालाँकि, इस प्रक्रिया को कई प्रयोगों के साथ पूरा करने की आवश्यकता है ताकि चरणों और प्रीफ़ैच समय के सही समूह का पता लगाया जा सके। अपने आवश्यक प्रयास के कारण, यह उच्च प्रदर्शन डेटा / पैकेट स्ट्रीम प्रसंस्करण में अधिक गोद लेने को देखता है। डीपीडीके क्यूओएस एनकेयू पाइपलाइन डिजाइन में एक अच्छा उत्पादन कोड उदाहरण पाया जा सकता है: http://dpdk.org/doc/guides/prog_guide/qos_framework.html अध्याय 21.2.4.3। एनक्यूपी पाइपलाइन।

अधिक जानकारी मिल सकती है:

https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and

http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf


1

न्यूनतम आकार लेने के लिए अपना कार्यक्रम लिखें। यही कारण है कि जीसीसी के लिए -O3 अनुकूलन का उपयोग करना हमेशा एक अच्छा विचार नहीं है। यह एक बड़ा आकार लेता है। अक्सर, -OO उतना ही अच्छा है जितना -O2। यह सब हालांकि उपयोग किए गए प्रोसेसर पर निर्भर करता है। YMMV।

एक समय में डेटा की छोटी मात्रा के साथ काम करें। यही कारण है कि यदि डेटा सेट बड़ा है, तो कम कुशल छंटाई एल्गोरिदम क्विकॉर्ट की तुलना में तेजी से चल सकता है। अपने बड़े डेटा सेट को छोटे में सेट करने के तरीके खोजें। दूसरों ने यह सुझाव दिया है।

अस्थायी अनुदेश / स्थानिक इलाके का बेहतर उपयोग करने में आपकी मदद करने के लिए, आप यह अध्ययन कर सकते हैं कि आपका कोड असेंबली में कैसे परिवर्तित हो जाता है। उदाहरण के लिए:

for(i = 0; i < MAX; ++i)
for(i = MAX; i > 0; --i)

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


दिलचस्प बिंदु। क्या लुक-फॉरवर्ड कैश एक लूप की दिशा के आधार पर धारणा बनाते हैं / स्मृति से गुजरते हैं?
एंड्रयू

1
सट्टा डेटा कैश डिज़ाइन करने के कई तरीके हैं। स्ट्राइड आधारित लोग डेटा एक्सेस की 'दूरी' और 'दिशा' को मापते हैं। सामग्री आधारित सूचक श्रृंखला का पीछा करते हैं। उन्हें डिजाइन करने के अन्य तरीके हैं।
सिब्रॉन

1

अपनी संरचना और फ़ील्ड्स को संरेखित करने के अलावा, यदि आपकी संरचना यदि ढेर आवंटित की जाती है, तो आप आवंटित आवंटन का समर्थन करने वाले आवंटनकर्ताओं का उपयोग करना चाह सकते हैं; जैसे _aligned_malloc (sizeof (DATA), SYSTEM_CACHE_LINE_SIZE); अन्यथा आपके पास यादृच्छिक गलत साझाकरण हो सकता है; याद रखें कि विंडोज में, डिफ़ॉल्ट हीप में 16 बाइट्स संरेखण है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.