तो क्या यह मेमोरी ओवरहेड नहीं है?
हां नहीं शायद?
यह एक अजीब सवाल है क्योंकि मशीन पर मेमोरी एड्रेसिंग रेंज की कल्पना करें, और एक सॉफ्टवेयर जिसे लगातार चीजों पर नज़र रखने की ज़रूरत होती है, जहां चीजें एक तरह से मेमोरी में होती हैं जिन्हें स्टैक से नहीं जोड़ा जा सकता है।
उदाहरण के लिए, एक संगीत खिलाड़ी की कल्पना करें जहां उपयोगकर्ता द्वारा संगीत फ़ाइल को एक बटन पुश पर लोड किया गया हो और जब उपयोगकर्ता किसी अन्य संगीत फ़ाइल को लोड करने का प्रयास करता है, तो इसे अस्थिर मेमोरी से अनलोड किया जाता है।
ऑडियो डेटा कहाँ संग्रहीत किया जाता है, इस पर हम कैसे नज़र रखते हैं? हमें इसके लिए एक मेमोरी एड्रेस चाहिए। कार्यक्रम को न केवल स्मृति में ऑडियो डेटा चंक का ट्रैक रखने की आवश्यकता है, बल्कि यह स्मृति में भी है । इस प्रकार हमें एक मेमोरी एड्रेस (यानी एक पॉइंटर) रखना होगा। और मेमोरी एड्रेस के लिए आवश्यक स्टोरेज का आकार मशीन की एड्रेसिंग रेंज (पूर्व: 64-बिट एड्रेसिंग रेंज के लिए 64-बिट पॉइंटर) से मेल खाने वाला है।
तो यह "हाँ" की तरह है, इसे मेमोरी एड्रेस का ट्रैक रखने के लिए स्टोरेज की आवश्यकता होती है, लेकिन ऐसा नहीं है कि हम इसे इस तरह की डायनामिक-एलिमेंट मेमोरी के लिए टाल सकते हैं।
इसकी भरपाई कैसे होती है?
केवल एक पॉइंटर के आकार के बारे में बात करते हुए, आप कुछ मामलों में स्टैक का उपयोग करके लागत से बच सकते हैं, जैसे कि मामले में, कंपाइलर निर्देश उत्पन्न कर सकते हैं जो एक पॉइंटर की लागत से बचने के लिए सापेक्ष मेमोरी एड्रेस को प्रभावी ढंग से हार्ड-कोड करते हैं। यदि आप बड़े, परिवर्तनशील आकार के आवंटन के लिए ऐसा करते हैं, और उपयोगकर्ता इनपुट (ऑडियो उदाहरण के अनुसार) द्वारा संचालित शाखाओं की एक जटिल श्रृंखला के लिए ऐसा करना अव्यावहारिक है (और यदि असंभव नहीं है) तो यह आपको अत्यधिक प्रभावित करता है। ऊपर)।
एक अन्य तरीका अधिक सन्निहित डेटा संरचनाओं का उपयोग करना है। उदाहरण के लिए, एक डबल-लिंक्ड सूची के बजाय एक सरणी-आधारित अनुक्रम का उपयोग किया जा सकता है, जिसमें प्रति नोड दो पॉइंटर्स की आवश्यकता होती है। हम इन दोनों के हाइब्रिड का उपयोग एक अनियंत्रित सूची की तरह भी कर सकते हैं, जो एन तत्वों के प्रत्येक सन्निहित समूह के बीच केवल संकेत देता है।
समय महत्वपूर्ण कम मेमोरी अनुप्रयोगों में पॉइंटर्स का उपयोग किया जाता है?
हां, बहुत आम तौर पर, चूंकि कई प्रदर्शन-महत्वपूर्ण एप्लिकेशन C या C ++ में लिखे गए हैं जो पॉइंटर उपयोग के प्रभुत्व हैं (वे एक स्मार्ट पॉइंटर के पीछे हो सकते हैं std::vector
या जैसे कंटेनर हो सकता है std::string
, लेकिन अंतर्निहित मैकेनिक एक पॉइंटर को उबालते हैं जो उपयोग किया जाता है डायनेमिक मेमोरी ब्लॉक में पते का ट्रैक रखने के लिए)।
अब वापस इस सवाल पर:
इसकी भरपाई कैसे होती है? (भाग दो)
जब तक आप उनमें से एक लाख (जो अभी भी एक 64-बिट मशीन पर 8 मेगाबाइट है) की तरह भंडारण कर रहे हैं, तब तक पॉइंटर्स आमतौर पर सस्ते होते हैं।
* नोट के रूप में बेन ने बताया कि एक "औसत दर्जे का" 8 megs अभी भी L3 कैश का आकार है। यहाँ मैंने कुल घूंट के उपयोग के अर्थ में "औसत रूप से" अधिक उपयोग किया और मेमोरी को विशिष्ट सापेक्ष आकार बिंदुओं का एक स्वस्थ उपयोग इंगित करेगा।
जहां पॉइंटर्स महंगे मिलते हैं वे खुद पॉइंटर्स नहीं होते हैं:
गतिशील स्मृति आवंटन। डायनामिक मेमोरी आवंटन महंगा हो जाता है क्योंकि इसे एक अंतर्निहित डेटा संरचना (पूर्व: मित्र या स्लैब आवंटनकर्ता) से गुजरना पड़ता है। भले ही ये अक्सर मौत के लिए अनुकूलित होते हैं, वे सामान्य उद्देश्य वाले होते हैं और चर-आकार के ब्लॉकों को संभालने के लिए डिज़ाइन किए जाते हैं, जिन्हें आवश्यकता होती है कि वे कम से कम एक "खोज" जैसा दिखने वाला काम करें (यद्यपि हल्के और संभवतः निरंतर-समय के लिए) स्मृति में सन्निहित पृष्ठों का एक निःशुल्क सेट खोजें।
मेमोरी एक्सेस। यह चिंता करने के लिए बड़ा उपरि हो जाता है। जब भी हम पहली बार डायनामिक रूप से आवंटित मेमोरी का उपयोग करते हैं, तो एक अनिवार्य पृष्ठ दोष होता है और साथ ही कैश मेमोरी पदानुक्रम और नीचे एक रजिस्टर में मेमोरी को स्थानांतरित करने से चूक जाता है।
मेमोरी एक्सेस
मेमोरी एक्सेस एल्गोरिदम से परे प्रदर्शन के सबसे महत्वपूर्ण पहलुओं में से एक है। एएए गेम इंजन जैसे कई प्रदर्शन-महत्वपूर्ण क्षेत्र डेटा-उन्मुख अनुकूलन के प्रति अपनी ऊर्जा का एक बड़ा हिस्सा केंद्रित करते हैं जो अधिक कुशल मेमोरी एक्सेस पैटर्न और लेआउट के लिए उबालते हैं।
उच्च-स्तरीय भाषाओं की सबसे बड़ी प्रदर्शन कठिनाइयों में से एक, जो प्रत्येक उपयोगकर्ता-परिभाषित प्रकार को अलग-अलग कचरा कलेक्टर के माध्यम से आवंटित करना चाहते हैं, उदाहरण के लिए, वे स्मृति को बहुत कम कर सकते हैं। यह विशेष रूप से सच हो सकता है अगर सभी वस्तुओं को एक बार में आवंटित नहीं किया जाता है।
उन मामलों में, यदि आप उपयोगकर्ता-परिभाषित ऑब्जेक्ट प्रकार के लाख उदाहरणों की सूची संग्रहीत करते हैं, तो उन उदाहरणों को क्रमिक रूप से एक लूप में एक्सेस करना काफी धीमा हो सकता है क्योंकि यह एक मिलियन पॉइंटर्स की सूची के अनुरूप है जो स्मृति के क्षेत्रों को अलग करने की ओर इशारा करता है। उन मामलों में, आर्किटेक्चर मेमोरी फॉर्म को ऊपरी, धीमा, पदानुक्रम के बड़े स्तरों को बड़े पैमाने पर जोड़ना चाहता है, इस उम्मीद के साथ संरेखित विखंडन करता है कि उन विखंडों में आस-पास के डेटा को निष्कासन से पहले एक्सेस किया जाएगा। जब इस तरह की सूची में प्रत्येक ऑब्जेक्ट को अलग से आवंटित किया जाता है, तो अक्सर हम कैश मिस के साथ इसके लिए भुगतान करते हैं जब प्रत्येक बाद के पुनरावृत्ति को मेमोरी में एक पूरी तरह से अलग क्षेत्र से लोड करना पड़ सकता है जिसमें कोई भी आसन्न वस्तुओं को बेदखली से पहले एक्सेस नहीं किया जा सकता है।
ऐसी भाषाओं के लिए बहुत सारे कंपाइलर इन दिनों निर्देश चयन और रजिस्टर आवंटन में एक बहुत अच्छा काम कर रहे हैं, लेकिन स्मृति प्रबंधन पर अधिक प्रत्यक्ष नियंत्रण की कमी हत्यारा हो सकती है (हालांकि अक्सर कम त्रुटि-प्रवण) और फिर भी जैसी भाषाएं बनाते हैं C और C ++ काफी लोकप्रिय है।
अप्रत्यक्ष रूप से पॉइंटर एक्सेस का अनुकूलन
अधिकांश प्रदर्शन-महत्वपूर्ण परिदृश्यों में, अनुप्रयोग अक्सर मेमोरी पूल का उपयोग करते हैं जो संदर्भ की स्थानीयता में सुधार करने के लिए सन्निहित खंडों से पूल मेमोरी का उपयोग करते हैं। ऐसे मामलों में, यहां तक कि एक पेड़ या एक लिंक्ड सूची जैसी लिंक की गई संरचना को कैश-फ्रेंडली बनाया जा सकता है, बशर्ते कि इसके नोड्स का मेमोरी लेआउट प्रकृति में सन्निहित हो। यह प्रभावी रूप से सूचक डेरेफ्रेंसिंग को सस्ता बना रहा है, यद्यपि अप्रत्यक्ष रूप से संदर्भ की स्थानीयता में सुधार करते हुए शामिल किया गया है जब उन्हें डीरेफरेंसिंग कर रहा है।
चारों ओर पॉइंटर्स का पीछा करते हुए
मान लें कि हमारे पास एक एकल-लिंक्ड सूची है:
Foo->Bar->Baz->null
समस्या यह है कि अगर हम इन सभी नोड्स को अलग-अलग एक सामान्य-उद्देश्य आवंटन (और संभवतः सभी एक बार में) के खिलाफ आवंटित करते हैं, तो वास्तविक मेमोरी कुछ इस तरह से सरलीकृत हो सकती है (सरलीकृत आरेख):
जब हम चारों ओर बिंदुओं का पीछा करना शुरू करते हैं और Foo
नोड तक पहुंचते हैं, तो हम एक अनिवार्य याद (और संभवतः एक पृष्ठ दोष) के साथ शुरू करते हैं, जो स्मृति क्षेत्र से अपने मेमोरी क्षेत्र से स्मृति के तेज क्षेत्रों से स्मृति के तेजी से क्षेत्रों तक बढ़ रहा है, जैसे:
यह हमें केवल एक मेमोरी क्षेत्र को कैश करने (संभवतः पृष्ठ) को इसके एक हिस्से तक पहुंचने और बाकी को बेदखल करने का कारण बनता है क्योंकि हम इस सूची में पॉइंटर्स का पीछा करते हैं। हालाँकि, मेमोरी एलोकेटर पर नियंत्रण रखने से, हम इस तरह की सूची को इस तरह आवंटित कर सकते हैं:
... और इस तरह से उस गति में काफी सुधार होता है जिस पर हम इन बिंदुओं को कम कर सकते हैं और उनके पॉइंटर की प्रक्रिया कर सकते हैं। इसलिए, बहुत अप्रत्यक्ष रूप से, हम इस तरह से पॉइंटर एक्सेस को तेज कर सकते हैं। बेशक अगर हम इन सन्दर्भों को एक सरणी में संग्रहीत करते हैं, तो हमारे पास यह समस्या पहली जगह में नहीं होगी, लेकिन यहाँ मेमोरी एलोकेटर हमें मेमोरी लेआउट पर स्पष्ट नियंत्रण देता है, जिस दिन एक लिंक किए गए ढांचे की आवश्यकता होती है।
* नोट: यह स्मृति पदानुक्रम और संदर्भ की स्थानीयता के बारे में एक बहुत ही सुस्पष्ट आरेख और चर्चा है, लेकिन उम्मीद है कि यह प्रश्न के स्तर के लिए उपयुक्त है।