"कैश-फ्रेंडली" कोड क्या है?


738

" कैश फ्रेंडली कोड " और " कैश फ्रेंडली " कोड में क्या अंतर है ?

मैं यह कैसे सुनिश्चित कर सकता हूं कि मैं कैश-कुशल कोड लिखूं?


28
यह आपको एक संकेत दे सकता है: stackoverflow.com/questions/9936132/…
रॉबर्ट मार्टिन

4
कैश लाइन के आकार के बारे में भी जानकारी रखें। आधुनिक प्रोसेसर पर, यह अक्सर 64 बाइट्स होता है।
जॉन डिबलिंग

3
यहाँ एक और बहुत अच्छा लेख है। सिद्धांत किसी भी OS (लिनक्स, मैक्सओएस या विंडोज) पर C / C ++ प्रोग्रामों पर लागू होते हैं: lwn.net/Articles/255364
paulsm4

4
संबंधित प्रश्न: stackoverflow.com/questions/8469427/…
मैट

जवाबों:


965

प्रारंभिक

आधुनिक कंप्यूटरों पर, केवल सबसे निचले स्तर की मेमोरी संरचनाएं ( रजिस्टर ) एकल घड़ी चक्रों में डेटा को स्थानांतरित कर सकती हैं। हालांकि, रजिस्टर बहुत महंगे हैं और अधिकांश कंप्यूटर कोर में कुछ दर्जन रजिस्टरों से कम है (कुछ सौ से शायद एक हजार बाइट्स कुल)। मेमोरी स्पेक्ट्रम ( DRAM ) के दूसरे छोर पर, मेमोरी बहुत सस्ती होती है (यानी शाब्दिक रूप से लाखों गुना सस्ती ), लेकिन डेटा प्राप्त करने के अनुरोध के बाद सैकड़ों चक्र लेती है। सुपर फास्ट और महंगी और सुपर धीमी और सस्ती के बीच की खाई को पाटने के लिए कैशे यादें हैं, नाम L1, L2, L3 की गति और लागत में कमी। विचार यह है कि अधिकतर निष्पादित कोड अक्सर चर का एक छोटा सा सेट मार रहे होंगे, और बाकी (चर का एक बड़ा सेट) अक्सर। यदि प्रोसेसर L1 कैश में डेटा नहीं ढूंढ सकता है, तो यह L2 कैश में दिखता है। यदि नहीं, तो L3 कैश, और यदि नहीं, तो मुख्य मेमोरी। इनमें से प्रत्येक "मिस" समय में महंगा है।

(सादृश्य कैश मेमोरी है सिस्टम मेमोरी के लिए, क्योंकि सिस्टम मेमोरी बहुत हार्ड डिस्क स्टोरेज है। हार्ड डिस्क स्टोरेज सुपर सस्ता है लेकिन बहुत धीमा है)।

कैशिंग लेटेंसी के प्रभाव को कम करने के मुख्य तरीकों में से एक है । हर्ब सटर (सीएफआर। नीचे दिए गए लिंक) को समझने के लिए: बैंडविड्थ बढ़ाना आसान है, लेकिन हम विलंबता से अपना रास्ता नहीं खरीद सकते हैं

डेटा को हमेशा मेमोरी पदानुक्रम (छोटी से छोटी == सबसे तेज़ से धीमी) के माध्यम से पुनर्प्राप्त किया जाता है। एक कैश हिट / मिस आमतौर पर सीपीयू में कैश के उच्चतम स्तर में एक हिट / मिस को संदर्भित करता है - उच्चतम स्तर से मेरा मतलब सबसे बड़ा = सबसे धीमा है। कैश हिट दर प्रदर्शन के लिए महत्वपूर्ण है क्योंकि RAM से डेटा प्राप्त करने में हर कैश मिस परिणाम (या बदतर ...) जो बहुत समय लगता है (RAM के लिए सैकड़ों चक्र, HDD के लिए लाखों चक्र)। इसकी तुलना में, (उच्चतम स्तर) कैश से डेटा पढ़ना आमतौर पर केवल एक मुट्ठी भर चक्र होता है।

आधुनिक कंप्यूटर आर्किटेक्चर में, प्रदर्शन टोंटी सीपीयू को मर रहा है (जैसे रैम या उच्चतर तक पहुंच)। यह केवल समय के साथ खराब हो जाएगा। प्रोसेसर की आवृत्ति में वृद्धि वर्तमान में प्रदर्शन को बढ़ाने के लिए प्रासंगिक नहीं है। समस्या मेमोरी एक्सेस है। सीपीयू में हार्डवेयर डिजाइन के प्रयास इसलिए वर्तमान में कैश, प्रीफेटिंग, पाइपलाइन और कंसीडर को अनुकूलित करने पर अधिक ध्यान केंद्रित करते हैं। उदाहरण के लिए, आधुनिक सीपीयू कैश पर लगभग 85% मर जाते हैं और डेटा को संग्रहीत / स्थानांतरित करने के लिए 99% तक खर्च करते हैं!

विषय पर काफी कुछ कहा जा सकता है। यहां कैश, मेमोरी पदानुक्रम और उचित प्रोग्रामिंग के बारे में कुछ महान संदर्भ दिए गए हैं:

कैश-फ्रेंडली कोड के लिए मुख्य अवधारणाएँ

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

कैशिंग का अनुकूलन करने के लिए निम्नलिखित विशेष पहलू उच्च महत्व के हैं:

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

उचित उपयोग करें कंटेनर

कैश-फ्रेंडली बनाम कैश-अनफ्रेंडली का एक सरल उदाहरण है का std::vectorबनाम std::list। किसी तत्व को std::vectorसन्निहित स्मृति में संग्रहीत किया जाता है, और जैसे कि उन तक पहुंचना एक में तत्वों तक पहुंचने की तुलना में बहुत अधिक कैश-अनुकूल है std::list, जो सभी जगह अपनी सामग्री को संग्रहीत करता है। यह स्थानिक इलाके के कारण है।

इसका एक बहुत अच्छा चित्रण इस youtube क्लिप में बज़्ने स्ट्रॉस्ट्रुप द्वारा दिया गया है (लिंक के लिए @ मोहम्मद अली बेयदौन का धन्यवाद!)।

डेटा संरचना और एल्गोरिथम डिज़ाइन में कैश की उपेक्षा न करें

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

डेटा की निहित संरचना को जानें और उसका दोहन करें

एक और सरल उदाहरण, जिसे क्षेत्र के कई लोग कभी-कभी भूल जाते हैं, स्तंभ-प्रधान (पूर्व)। ,) बनाम पंक्ति-प्रमुख क्रम (पूर्व)। ,) दो आयामी सरणियों के भंडारण के लिए। उदाहरण के लिए, निम्नलिखित मैट्रिक्स पर विचार करें:

1 2
3 4

पंक्ति-प्रमुख क्रम में, इसे मेमोरी में संग्रहीत किया जाता है 1 2 3 4; कॉलम-मेजर ऑर्डरिंग में, इसे स्टोर किया जाएगा 1 3 2 4। यह देखना आसान है कि कार्यान्वयन जो इस आदेश का फायदा नहीं उठाते हैं वे जल्दी से (आसानी से परिहार्य!) कैश मुद्दों में चलेंगे। दुर्भाग्य से, मैं अपने डोमेन (मशीन सीखने) में बहुत बार इस तरह से सामान देखता हूं। @MatteoItalia ने अपने जवाब में इस उदाहरण को अधिक विस्तार से दिखाया।

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

सादगी के लिए, मान लें कि कैश में एक एकल कैश लाइन शामिल है जिसमें 2 मैट्रिक्स तत्व शामिल हो सकते हैं और यह कि जब किसी दिए गए तत्व को मेमोरी से लिया जाता है, तो अगला भी होता है। कहें कि हम उपरोक्त 2x2 मैट्रिक्स में सभी तत्वों पर योग लेना चाहते हैं (इसे कॉल करें M):

आदेश का पालन करना (जैसे पहले कॉलम इंडेक्स बदलना ):

M[0][0] (memory) + M[0][1] (cached) + M[1][0] (memory) + M[1][1] (cached)
= 1 + 2 + 3 + 4
--> 2 cache hits, 2 memory accesses

आदेश का शोषण न करना (जैसे पहले पंक्ति सूचकांक बदलना ):

M[0][0] (memory) + M[1][0] (memory) + M[0][1] (memory) + M[1][1] (memory)
= 1 + 3 + 2 + 4
--> 0 cache hits, 4 memory accesses

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

अप्रत्याशित शाखाओं से बचें

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

यह यहां बहुत अच्छी तरह से समझाया गया है (लिंक के लिए @ 0x90 के लिए धन्यवाद): एक अनसोल्ड सरणी को संसाधित करने की तुलना में तेजी से सॉर्ट किए गए सरणी को क्यों संसाधित कर रहा है?

आभासी कार्यों से बचें

के संदर्भ में , virtualतरीके कैश मिस के संबंध में एक विवादास्पद मुद्दे का प्रतिनिधित्व करते हैं (एक आम सहमति मौजूद है कि प्रदर्शन के मामले में जब संभव हो तो उन्हें टाला जाना चाहिए)। वर्चुअल फ़ंक्शंस लुक अप के दौरान कैश मिस को प्रेरित कर सकते हैं, लेकिन यह केवल तभी होता है जब विशिष्ट फ़ंक्शन को अक्सर नहीं बुलाया जाता है (अन्यथा यह संभवतः कैश किया जाएगा), इसलिए इसे कुछ द्वारा गैर-मुद्दा माना जाता है। इस समस्या के संदर्भ के लिए, देखें: C ++ क्लास में वर्चुअल विधि होने की प्रदर्शन लागत क्या है?

सामान्य समस्यायें

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

रैम मेमोरी में खराब कैशिंग का एक चरम लक्षण (जो शायद इस संदर्भ में आपका मतलब नहीं है) तथाकथित थ्रैशिंग है । यह तब होता है जब प्रक्रिया लगातार पृष्ठ दोष उत्पन्न करती है (उदाहरण के लिए स्मृति तक पहुंचती है जो वर्तमान पृष्ठ में नहीं है) जिसे डिस्क पहुंच की आवश्यकता होती है।


27
शायद आप इस उत्तर को थोड़ा विस्तार देकर यह भी बता सकते हैं कि, -multithreaded कोड- डेटा बहुत अधिक स्थानीय हो सकता है (उदाहरण के लिए गलत साझाकरण)
TemplateRex

2
कैश के कई स्तर हो सकते हैं जैसा कि चिप डिजाइनर सोचते हैं कि यह उपयोगी है। आम तौर पर वे गति बनाम आकार को संतुलित कर रहे हैं। यदि आप अपने L1 कैश को L5 जितना बड़ा बना सकते हैं, और जितनी तेजी से, आपको केवल L1 की आवश्यकता होगी।
राफेल बैप्टिस्टा

24
मुझे लगता है कि StackOverflow पर समझौते के खाली पदों को अस्वीकार कर दिया गया है, लेकिन यह ईमानदारी से स्पष्ट, सर्वश्रेष्ठ, उत्तर है जो मैंने अब तक देखा है। बहुत बढ़िया काम, मार्क।
जैक एडले

2
@JackAidley आपकी प्रशंसा के लिए धन्यवाद! जब मैंने इस प्रश्न पर ध्यान देने की मात्रा को देखा, तो मुझे लगा कि बहुत से लोगों को कुछ हद तक व्यापक व्याख्या में दिलचस्पी हो सकती है। मुझे खुशी है कि यह उपयोगी है।
मार्क क्लेसेन

1
आपने जो उल्लेख नहीं किया है वह यह है कि कैश फ्रेंडली डेटा स्ट्रक्चर्स को कैशे लाइन के भीतर फिट करने के लिए डिज़ाइन किया गया है और कैशे लाइनों का इष्टतम उपयोग करने के लिए मेमोरी में संरेखित किया गया है। हालांकि महान जवाब! बहुत बढ़िया।
मैट

140

@Marc क्लेसेन के जवाब के अलावा, मुझे लगता है कि कैश-अनफ्रीगेट कोड का एक शिक्षाप्रद क्लासिक उदाहरण कोड है जो कि पंक्ति-वार के बजाय C बिदिम आयामी सरणी (जैसे एक बिटमैप छवि) कॉलम-वार को स्कैन करता है।

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

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

और यह सब प्रदर्शन को बर्बाद करने के लिए होता है

// Cache-friendly version - processes pixels which are adjacent in memory
for(unsigned int y=0; y<height; ++y)
{
    for(unsigned int x=0; x<width; ++x)
    {
        ... image[y][x] ...
    }
}

सेवा

// Cache-unfriendly version - jumps around in memory for no good reason
for(unsigned int x=0; x<width; ++x)
{
    for(unsigned int y=0; y<height; ++y)
    {
        ... image[y][x] ...
    }
}

यह प्रभाव छोटे कैश और / या बड़ी सरणियों के साथ काम करने वाले सिस्टम में गति (गति में परिमाण के कई क्रम) हो सकता है (उदाहरण के लिए मौजूदा मशीनों पर 10 + मेगापिक्सल 24 bpp चित्र); इस कारण से, यदि आपको कई ऊर्ध्वाधर स्कैन करने हैं, तो अक्सर 90 डिग्री की छवि को घुमाने और बाद में विभिन्न विश्लेषण करने के लिए, कैश-अनफ्री कोड को केवल रोटेशन तक सीमित करना बेहतर होता है।


Err, कि x <चौड़ाई होना चाहिए?
मोवाल्कर

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

मैंने अपनी मशीन पर एक समान उदाहरण की कोशिश की, और मैंने पाया कि समय एक ही था। किसी और को यह समय की कोशिश की है?
gsingh2011

@ I3arnon: नहींं, पहला कैश-फ्रेंडली है, क्योंकि सामान्य रूप से C सरणियों को पंक्ति-प्रमुख क्रम में संग्रहीत किया जाता है (यदि किसी कारण से आपकी छवि स्तंभ-प्रमुख क्रम में संग्रहीत होती है तो रिवर्स सही है)।
माटेओ इटालिया

1
@ गौथियर: हाँ, पहला स्निपेट अच्छा है; मुझे लगता है कि जब मैंने यह लिखा था तो मैं "ऑल लेट [वर्किंग एप्लिकेशन के प्रदर्शन को बर्बाद करने के लिए] की तर्ज पर सोच रहा था] ... से जाना है ..."
मैटो इटालिया

88

कैश उपयोग का अनुकूलन काफी हद तक दो कारकों के लिए आता है।

संदर्भ की स्थानीयता

पहला कारक (जिससे दूसरों को पहले से ही पता चला है) संदर्भ की स्थानीयता है। संदर्भ के इलाके में वास्तव में दो आयाम हैं: अंतरिक्ष और समय।

  • स्थानिक

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

दूसरा, हम ऐसी जानकारी चाहते हैं जिसे एक साथ संसाधित किया जाएगा, साथ में स्थित भी। एक विशिष्ट कैश "लाइनों" में काम करता है, जिसका अर्थ है कि जब आप कुछ जानकारी तक पहुंचते हैं, तो पास के पते पर अन्य जानकारी को हमारे द्वारा छुआए गए भाग के साथ कैश में लोड किया जाएगा। उदाहरण के लिए, जब मैं एक बाइट को छूता हूं, तो कैश उस के पास 128 या 256 बाइट लोड कर सकता है। उस का लाभ उठाने के लिए, आप आमतौर पर चाहते हैं कि डेटा की संभावना को अधिकतम करने के लिए व्यवस्था की गई है कि आप उसी समय उपयोग किए गए अन्य डेटा का भी उपयोग करेंगे।

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

  • समय

समय आयाम का मतलब है कि जब आप कुछ डेटा पर कुछ ऑपरेशन करते हैं, तो आप चाहते हैं (जितना संभव हो) एक ही बार में उस डेटा पर सभी ऑपरेशन करें।

चूँकि आपने इसे C ++ के रूप में टैग किया है, इसलिए मैं अपेक्षाकृत कैश-अनफ़ेयर डिज़ाइन के क्लासिक उदाहरण की ओर इशारा करता हूँ std::valarray:। valarrayभार के सबसे अंकगणितीय ऑपरेटर, तो मैं कर सकते हैं (उदाहरण के लिए) का कहना है a = b + c + d;(जहां a, b, cऔर dसभी valarrays कर रहे हैं) उन सरणियों के तत्व के लिहाज से इसके अलावा करने के लिए।

इसके साथ समस्या यह है कि यह एक जोड़ी इनपुट के माध्यम से चलता है, एक अस्थायी परिणाम देता है, इनपुट की एक और जोड़ी के माध्यम से चलता है, और इसी तरह। बहुत सारे डेटा के साथ, एक संगणना से परिणाम अगले संगणना में उपयोग होने से पहले कैश से गायब हो सकता है, इसलिए हम अपना अंतिम परिणाम प्राप्त करने से पहले डेटा को बार-बार पढ़ना (और लिखना) समाप्त कर देते हैं। अंतिम परिणाम के प्रत्येक तत्व की तरह कुछ हो जाएगा (a[n] + b[n]) * (c[n] + d[n]);, हम आम तौर पर प्रत्येक को पढ़ने के लिए पसंद करते हैं a[n], b[n], c[n]और d[n]एक बार, गणना, कर परिणाम, वेतन वृद्धि लिखने nऔर दोहराने 'टिल हम काम हो गया। 2

लाइन शेयरिंग

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

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

यह समस्या संभवतः काफी हद तक स्पष्ट है: प्रत्यक्ष-मैप किए गए कैश के लिए, एक ही कैश स्थान पर मैप करने के लिए दो ऑपरेंड खराब व्यवहार को जन्म दे सकते हैं। एन-वे सेट-एसोसिएटिव कैश संख्या 2 से एन + 1 तक बढ़ाता है। कैश को अधिक "तरीकों" से व्यवस्थित करना अतिरिक्त सर्किटरी लेता है और आम तौर पर धीमी गति से चलता है, इसलिए (उदाहरण के लिए) 8192-रास्ता सेट साहचर्य कैश शायद ही कभी एक अच्छा समाधान है।

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

  • झूठा शेयर करना

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


  1. जो लोग सी ++ को अच्छी तरह से जानते हैं, वे आश्चर्यचकित हो सकते हैं कि यह अभिव्यक्ति टेम्पलेट्स जैसी किसी चीज के माध्यम से अनुकूलन के लिए खुला है। मुझे पूरा यकीन है कि इसका उत्तर यह है कि हाँ, यह किया जा सकता है और यदि यह होता तो शायद यह एक बहुत बड़ी जीत होती। मैं किसी को भी ऐसा करने के बारे में पता नहीं हूँ, हालाँकि, और यह दिया जाता है कि कैसे कम valarrayउपयोग किया जाता है, मैं कम से कम किसी को भी ऐसा करते हुए देखकर आश्चर्यचकित रहूँगा।

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

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


1
मुझे आपके उत्तर में जानकारी के अतिरिक्त टुकड़े पसंद हैं, विशेष रूप से valarrayउदाहरण।
मार्क क्लेसेन

1
+1 आख़िरी: सेट समरूपता का सादा वर्णन! EDIT आगे: यह SO पर सबसे अधिक जानकारीपूर्ण उत्तरों में से एक है। धन्यवाद।
इंजीनियर

32

डेटा ओरिएंटेड डिज़ाइन की दुनिया में आपका स्वागत है। मूल मंत्र सॉर्ट करना है, शाखाओं को हटा दें, बैच, virtualकॉल हटा दें - बेहतर इलाके की ओर सभी कदम।

चूँकि आपने C ++ के साथ प्रश्न को टैग किया है, यहाँ अनिवार्य विशिष्ट C ++ Bullshit है । टोनी अल्ब्रेक्ट के ऑब्जेक्ट्स ऑफ़ ऑब्जेक्ट ओरिएंटेड प्रोग्रामिंग भी विषय में एक महान परिचय है।


1
बैच से आपका क्या मतलब है, कोई समझ नहीं सकता है।
0x90

5
बैचिंग: किसी एकल ऑब्जेक्ट पर कार्य की इकाई को निष्पादित करने के बजाय, इसे ऑब्जेक्ट के बैच पर निष्पादित करें।
अरुल

AKA ब्लॉक करना, रजिस्टर करना बंद करना, कैश ब्लॉक करना।
0x90

1
ब्लॉकिंग / नॉन-ब्लॉकिंग आमतौर पर संदर्भित करता है कि वस्तुएं समवर्ती वातावरण में कैसे व्यवहार करती हैं।
अरुल

2
बैचिंग == वैश्वीकरण
अमरो

23

बस जमा करने पर: कैश-अनफ्रेंडली बनाम कैश-फ्रेंडली कोड का क्लासिक उदाहरण मैट्रिक्स का "कैश ब्लॉकिंग" है।

भोले मैट्रिक्स के समान दिखता है:

for(i=0;i<N;i++) {
   for(j=0;j<N;j++) {
      dest[i][j] = 0;
      for( k==;k<N;i++) {
         dest[i][j] += src1[i][k] * src2[k][j];
      }
   }
}

यदि Nबड़ा है, उदाहरण के लिए, यदि N * sizeof(elemType)कैश आकार से अधिक है, तो हर एक का उपयोग src2[k][j]कैश मिस होगा।

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

int itemsPerCacheLine = CacheLineSize / sizeof(elemType);

for(i=0;i<N;i++) {
   for(j=0;j<N;j += itemsPerCacheLine ) {
      for(jj=0;jj<itemsPerCacheLine; jj+) {
         dest[i][j+jj] = 0;
      }
      for( k=0;k<N;k++) {
         for(jj=0;jj<itemsPerCacheLine; jj+) {
            dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
         }
      }
   }
}

यदि कैश लाइन का आकार 64 बाइट्स है, और हम 32 बिट (4 बाइट) पर चल रहे हैं, तो कैश लाइन पर 16 आइटम हैं। और कैश की संख्या बस इस साधारण परिवर्तन के माध्यम से लगभग 16 गुना कम हो जाती है।

2 डी टाइलों पर फैनसीयर ट्रांसफॉर्मेशन संचालित होते हैं, कई कैश (एल 1, एल 2, टीएलबी) के लिए अनुकूलन करते हैं, और इसी तरह।

"कैश ब्लॉकिंग" को देखने के कुछ परिणाम:

http://stumptown.cc.gt.atl.ga.us/cse6230-hpcta-fa11/slides/11a-matmul-goto.pdf

http://software.intel.com/en-us/articles/cache-blocking-techniques

एक अनुकूलित कैश अवरुद्ध एल्गोरिथ्म का एक अच्छा वीडियो एनीमेशन।

http://www.youtube.com/watch?v=IFWgwGMMrh0

लूप टाइलिंग बहुत बारीकी से संबंधित है:

http://en.wikipedia.org/wiki/Loop_tiling


7
जो लोग इसे पढ़ते हैं, वे मैट्रिक्स गुणा के बारे में मेरे लेख में दिलचस्पी ले सकते हैं, जहां मैंने "कैश-फ्रेंडली" ikj-एल्गोरिथ्म का परीक्षण किया और दो 2000x2000 मैट्रिसेस को गुणा करके अमित्र ijk-एल्गोरिथ्म।
मार्टिन थोमा

3
k==;मुझे उम्मीद है कि यह एक टाइपो है?
ट्रेब्ल्डज

13

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

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

इसलिए आज जब आप एक पता प्राप्त करते हैं तो यह कैश के पहले स्तर की जांच करता है यह देखने के लिए कि क्या यह पहले से ही उस पते को कैश में पढ़ता है, अगर यह नहीं मिलता है, तो यह कैश मिस है और इसे अगले स्तर तक जाना है इसे खोजने के लिए कैश करें, जब तक कि अंततः इसे मुख्य मेमोरी में नहीं जाना पड़े।

कैश फ्रेंडली कोड एक्सेस को मेमोरी में एक साथ बंद रखने की कोशिश करता है ताकि आप कैश मिस को कम कर सकें।

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

यदि आपने तत्वों को एक पंक्ति में बाईं से दाईं ओर कॉपी किया है - जो कैश फ्रेंडली होगा। यदि आपने एक बार में तालिका एक कॉलम को कॉपी करने का निर्णय लिया है, तो आप ठीक उसी मात्रा में मेमोरी कॉपी करेंगे - लेकिन यह कैफ़े से कैश होगी।


4

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

आमतौर पर कोड को सघन करने के लिए, इसे संग्रहीत करने के लिए कम कैश लाइनों की आवश्यकता होगी। इससे डेटा के लिए अधिक कैश लाइनें उपलब्ध हो रही हैं।

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

एक फ़ंक्शन कैश लाइन-संरेखण-अनुकूल पते पर शुरू होना चाहिए। हालाँकि इसके लिए (gcc) संकलक स्विच होते हैं जो जानते हैं कि यदि फ़ंक्शन बहुत कम हैं, तो प्रत्येक के लिए पूरी कैश लाइन पर कब्जा करना व्यर्थ हो सकता है। उदाहरण के लिए, यदि तीन सबसे अधिक बार उपयोग किए जाने वाले फ़ंक्शन एक 64 बाइट कैश लाइन के अंदर फिट होते हैं, तो यह कम बेकार है यदि प्रत्येक की अपनी लाइन है और दो कैश लाइनों में परिणाम अन्य उपयोग के लिए कम उपलब्ध हैं। एक विशिष्ट संरेखण मूल्य 32 या 16 हो सकता है।

इसलिए कोड को घना बनाने के लिए कुछ अतिरिक्त समय व्यतीत करें। विभिन्न निर्माणों का परीक्षण करें, उत्पन्न कोड आकार और प्रोफ़ाइल की समीक्षा करें और उनकी समीक्षा करें।


2

जैसा कि @Marc Claesen ने उल्लेख किया है कि कैश फ्रेंडली कोड लिखने का एक तरीका उस संरचना का शोषण करना है जिसमें हमारा डेटा संग्रहीत है। इसके अलावा कैश फ्रेंडली कोड लिखने का एक और तरीका है: हमारे डेटा को संग्रहीत करने के तरीके को बदलना; फिर इस नई संरचना में संग्रहीत डेटा तक पहुंचने के लिए नया कोड लिखें।

यह इस मामले में समझ में आता है कि कैसे डेटाबेस सिस्टम एक टेबल के टुपल्स को रैखिक करते हैं और उन्हें स्टोर करते हैं। टेबल के रस्सियों को स्टोर करने के दो मूल तरीके हैं अर्थात पंक्ति स्टोर और कॉलम स्टोर। पंक्ति की दुकान में जैसा कि नाम से पता चलता है कि ट्यूपल्स पंक्तिबद्ध हैं। मान लीजिए कि Productसंग्रहित की जा रही तालिका में 3 विशेषताएँ हैं अर्थात int32_t key, char name[56]और int32_t price, इसलिए टपल का कुल आकार 64बाइट्स है।

हम Productआकार एन के साथ संरचनाओं की एक सरणी बनाकर मुख्य मेमोरी में एक बहुत ही मूल पंक्ति स्टोर क्वेरी निष्पादन का अनुकरण कर सकते हैं , जहां एन तालिका में पंक्तियों की संख्या है। इस तरह के मेमोरी लेआउट को अरैम्प ऑफ स्ट्रक्सेस भी कहा जाता है। तो उत्पाद के लिए संरचना निम्न प्रकार हो सकती है:

struct Product
{
   int32_t key;
   char name[56];
   int32_t price'
}

/* create an array of structs */
Product* table = new Product[N];
/* now load this array of structs, from a file etc. */

इसी प्रकार हम Productतालिका की प्रत्येक विशेषता के लिए आकार एन के 3 सरणियों का निर्माण करके मुख्य मेमोरी में एक बहुत ही बुनियादी कॉलम स्टोर क्वेरी निष्पादन का अनुकरण कर सकते हैं । इस तरह के मेमोरी लेआउट को एरेज़ की संरचना भी कहा जाता है। इसलिए उत्पाद की प्रत्येक विशेषता के लिए 3 सरणियाँ निम्न हो सकती हैं:

/* create separate arrays for each attribute */
int32_t* key = new int32_t[N];
char* name = new char[56*N];
int32_t* price = new int32_t[N];
/* now load these arrays, from a file etc. */

अब दोनों सरणी के पैटर्न (पंक्ति लेआउट) और 3 अलग-अलग सरणियों (कॉलम लेआउट) को लोड करने के बाद, हमारे पास Productहमारी मेमोरी में मौजूद टेबल पर पंक्ति स्टोर और कॉलम स्टोर है ।

अब हम कैश फ्रेंडली कोड पार्ट पर चलते हैं। मान लीजिए कि हमारी मेज पर कार्यभार ऐसा है कि हमारे पास मूल्य विशेषता पर एक एकत्रीकरण क्वेरी है। जैसे कि

SELECT SUM(price)
FROM PRODUCT

रो स्टोर के लिए हम उपरोक्त SQL क्वेरी को में बदल सकते हैं

int sum = 0;
for (int i=0; i<N; i++)
   sum = sum + table[i].price;

कॉलम स्टोर के लिए हम उपरोक्त SQL क्वेरी को में बदल सकते हैं

int sum = 0;
for (int i=0; i<N; i++)
   sum = sum + price[i];

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

मान लीजिए कि कैश लाइन का आकार 64बाइट्स है।

पंक्ति लेआउट के मामले में जब कोई कैश लाइन पढ़ी जाती है, तो केवल 1 ( cacheline_size/product_struct_size = 64/64 = 1) टपल का मूल्य मूल्य पढ़ा जाता है, क्योंकि हमारे 64 बाइट्स का संरचनात्मक आकार और यह हमारी पूरी कैशे लाइन को भर देता है, इसलिए प्रत्येक ट्यूपल के लिए कैशे मिस हो जाता है। एक पंक्ति लेआउट का।

कॉलम लेआउट के मामले में जब एक कैश लाइन पढ़ी जाती है, तो 16 ( cacheline_size/price_int_size = 64/4 = 16) टुपल्स का मूल्य मूल्य पढ़ा जाता है, क्योंकि मेमोरी में संग्रहीत 16 सन्निहित मूल्य मूल्यों को कैश में लाया जाता है, इसलिए प्रत्येक सोलहवें ट्यूपल के लिए कैश मिस ओवर्स के मामले में। कॉलम लेआउट।

तो कॉलम लेआउट दिए गए क्वेरी के मामले में तेज होगा, और तालिका के स्तंभों के सबसेट पर ऐसे एकत्रीकरण प्रश्नों में तेज है। आप TPC-H बेंचमार्क के डेटा का उपयोग करके अपने लिए इस तरह के प्रयोग को आजमा सकते हैं , और दोनों लेआउट के रन समय की तुलना कर सकते हैं। विकिपीडिया स्तंभ उन्मुख डाटाबेस सिस्टम पर लेख भी अच्छा है।

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


1

ध्यान रखें कि कैश निरंतर मेमोरी को कैश नहीं करता है। उनके पास कई पंक्तियाँ हैं (कम से कम 4) इसलिए बंद और अतिव्यापी मेमोरी को अक्सर कुशलता से संग्रहीत किया जा सकता है।

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

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