c++ - "संकलन समय पर आवंटित स्मृति" वास्तव में क्या मतलब है?




memory memory-management (8)

सी और सी ++ जैसी प्रोग्रामिंग भाषाओं में, लोग अक्सर स्थिर और गतिशील स्मृति आवंटन को संदर्भित करते हैं। मैं अवधारणा को समझता हूं लेकिन वाक्यांश "संकलन समय के दौरान सभी स्मृति आवंटित (आरक्षित)" हमेशा मुझे भ्रमित करता है।

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

एक रनटाइम अवधारणा परिभाषा द्वारा स्मृति आवंटन नहीं है?

यदि मैं अपने सी / सी ++ कोड में 1 केबी स्थिर आवंटित चर बना देता हूं, तो क्या वह उसी राशि से निष्पादन योग्य आकार को बढ़ाएगा?

यह उन पृष्ठों में से एक है जहां वाक्यांश "स्टेटिक आवंटन" शीर्षक के तहत उपयोग किया जाता है।

मूल बातें: स्मृति आवंटन, इतिहास के नीचे चलना


आपके प्रश्न का मूल यह है: "संकलित फ़ाइल में" स्मृति कैसे आवंटित की जाती है "? क्या सभी वर्चुअल मेमोरी प्रबंधन सामग्री के साथ रैम में स्मृति हमेशा आवंटित नहीं होती है? परिभाषा द्वारा स्मृति आवंटन को रनटाइम अवधारणा नहीं है?"

मुझे लगता है कि समस्या यह है कि स्मृति आवंटन में दो अलग-अलग अवधारणाएं शामिल हैं। इसकी मूलभूत पर, स्मृति आवंटन वह प्रक्रिया है जिसके द्वारा हम कहते हैं कि "डेटा का यह आइटम स्मृति के इस विशिष्ट खंड में संग्रहीत है"। एक आधुनिक कंप्यूटर सिस्टम में, इसमें दो चरण की प्रक्रिया शामिल है:

  • कुछ सिस्टम वर्चुअल एड्रेस को तय करने के लिए उपयोग किया जाता है जिस पर आइटम संग्रहीत किया जाएगा
  • वर्चुअल एड्रेस को भौतिक पते पर मैप किया गया है

बाद की प्रक्रिया पूरी तरह से समय चलती है, लेकिन पूर्व संकलन समय पर किया जा सकता है, यदि डेटा का ज्ञात आकार है और उनमें से एक निश्चित संख्या आवश्यक है। मूल रूप से यह कैसे काम करता है:

  • कंपाइलर एक स्रोत फ़ाइल को देखता है जिसमें एक रेखा होती है जो इस तरह दिखती है:

    int c;
    
  • यह असेंबलर के लिए आउटपुट उत्पन्न करता है जो इसे चर 'सी' के लिए स्मृति आरक्षित करने के लिए निर्देश देता है। यह ऐसा दिखाई दे सकता है:

    global _c
    section .bss
    _c: resb 4
    
  • जब असेंबलर चलता है, तो यह एक काउंटर रखता है जो प्रत्येक आइटम के ऑफसेट को स्मृति 'सेगमेंट' (या 'सेक्शन') की शुरुआत से ट्रैक करता है। यह एक बहुत बड़ी 'संरचना' के हिस्सों की तरह है जिसमें पूरी फाइल में सब कुछ शामिल है, इस समय इस पर आवंटित कोई वास्तविक स्मृति नहीं है, और कहीं भी हो सकती है। यह एक तालिका में नोट करता है कि _c पास एक विशेष ऑफ़सेट है (सेगमेंट की शुरुआत से 510 बाइट्स कहें) और फिर उसके काउंटर को 4 से बढ़ाता है, इसलिए अगला ऐसा चर (51) बाइट्स पर होगा। किसी भी कोड के लिए जो _c के पते की आवश्यकता है, यह सिर्फ आउटपुट फ़ाइल में 510 रखता है, और एक नोट जोड़ता है कि आउटपुट को उस सेगमेंट के पते की आवश्यकता होती है जिसमें बाद में _c जोड़ना होता है।

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

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


एक निष्पादन योग्य वर्णन करता है कि स्थिर चर के लिए आवंटित करने के लिए कौन सी जगह आवंटित की जाती है। जब आप निष्पादन योग्य चलाते हैं, तो यह आवंटन सिस्टम द्वारा किया जाता है। तो आपका 1 केबी स्थिर चर 1kb के साथ निष्पादन योग्य के आकार में वृद्धि नहीं करेगा:

static char[1024];

बेशक आप एक प्रारंभकर्ता निर्दिष्ट नहीं करते हैं:

static char[1024] = { 1, 2, 3, 4, ... };

इसलिए, 'मशीन भाषा' (यानी सीपीयू निर्देश) के अलावा, एक निष्पादन योग्य में आवश्यक स्मृति लेआउट का विवरण होता है।


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

int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;

यह लिंकर को बताएगा कि इसे बीएसएस के लिए 208 बाइट, "डेटा" के लिए 16 बाइट और "कॉन्स" के लिए 28 बाइट्स की आवश्यकता है। इसके अलावा, एक चर के किसी भी संदर्भ को एक क्षेत्र चयनकर्ता और ऑफ़सेट के साथ प्रतिस्थापित किया जाएगा, इसलिए ए, बी, सी, डी, और ई, बीएसएस + 0, कॉन्स + 0, बीएसएस + 4, कॉन्स + 24, डेटा द्वारा प्रतिस्थापित किया जाएगा क्रमश: +0, या बीएसएस + 204।

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

जब कोई प्रोग्राम लोड होता है, तो प्लेटफॉर्म के आधार पर चार चीजों में से एक आम तौर पर होता है:

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

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

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

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

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


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


मैं कुछ आरेखों की मदद से इन अवधारणाओं को समझाना चाहता हूं।

यह सच है कि निश्चित रूप से संकलन समय पर स्मृति आवंटित नहीं की जा सकती है। लेकिन, फिर संकलन समय पर वास्तव में क्या होता है।

यहां स्पष्टीकरण आता है। कहें, उदाहरण के लिए एक प्रोग्राम में चार चर x, y, z और k हैं। अब, संकलन समय पर यह केवल एक मेमोरी मैप बनाता है, जहां एक दूसरे के संबंध में इन चर के स्थान का पता लगाया जाता है। यह चित्र इसे बेहतर समझाएगा।

अब कल्पना करें, स्मृति में कोई प्रोग्राम नहीं चल रहा है। यह मैं एक बड़े खाली आयत से दिखाता हूं।

इसके बाद, इस कार्यक्रम का पहला उदाहरण निष्पादित किया गया है। आप इसे निम्नानुसार कल्पना कर सकते हैं। यह वह समय है जब वास्तव में स्मृति आवंटित की जाती है।

जब इस प्रोग्राम का दूसरा उदाहरण चल रहा है, तो स्मृति निम्नानुसार दिखाई देगी।

और तीसरा ..

इसी तरह आगे भी।

मुझे आशा है कि यह विज़ुअलाइजेशन इस अवधारणा को अच्छी तरह से समझाएगा


यदि आप असेंबली प्रोग्रामिंग सीखते हैं, तो आप देखेंगे कि आपको डेटा, स्टैक और कोड इत्यादि के लिए सेगमेंट बनाना होगा। डेटा सेगमेंट है जहां आपके स्ट्रिंग्स और नंबर रहते हैं। कोड सेगमेंट वह जगह है जहां आपका कोड रहता है। ये खंड निष्पादन योग्य कार्यक्रम में बनाए गए हैं। बेशक ढेर का आकार भी महत्वपूर्ण है ... आप एक ढेर अतिप्रवाह नहीं चाहते हैं!

तो यदि आपका डेटा सेगमेंट 500 बाइट्स है, तो आपके प्रोग्राम में 500 बाइट क्षेत्र है। यदि आप डेटा सेगमेंट को 1500 बाइट्स में बदलते हैं, तो प्रोग्राम का आकार 1000 बाइट बड़ा होगा। डेटा वास्तविक कार्यक्रम में इकट्ठा किया जाता है।

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


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

char a[32];
char b;
char c;

उन 3 चर "संकलन समय पर आवंटित" हैं, इसका मतलब है कि संकलक संकलन समय पर उनके आकार (जो तय है) की गणना करता है। वैरिएबल a मेमोरी में ऑफ़सेट होगा, मान लीजिए, 0 को इंगित करने के लिए इंगित करते हुए b 34 पर पता करेगा और 34 पर c (कोई संरेखण अनुकूलन का अनुमान लगाएगा)। इसलिए, स्थिर डेटा के 1 केबी आवंटित करने से आपके कोड के आकार में वृद्धि नहीं होगी , क्योंकि यह केवल इसके अंदर ऑफसेट बदल देगा। लोड समय पर वास्तविक स्थान आवंटित किया जाएगा

वास्तविक स्मृति आवंटन हमेशा रन टाइम में होता है, क्योंकि कर्नेल को इसका ट्रैक रखने और इसकी आंतरिक डेटा संरचनाओं को अद्यतन करने की आवश्यकता होती है (प्रत्येक प्रक्रिया, पृष्ठों और इसी तरह के लिए कितनी मेमोरी आवंटित की जाती है)। अंतर यह है कि संकलक पहले से ही आपके द्वारा उपयोग किए जाने वाले प्रत्येक डेटा के आकार को जानता है और जैसे ही आपका प्रोग्राम निष्पादित किया जाता है, इसे आवंटित किया जाता है।

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


संकलन-समय पर आवंटित स्मृति का मतलब है कि संकलक संकलन-समय पर हल करता है जहां प्रक्रिया मेमोरी मैप के अंदर कुछ चीजें आवंटित की जाएंगी।

उदाहरण के लिए, वैश्विक सरणी पर विचार करें:

int array[100];

संकलक संकलन-समय को सरणी के आकार और int के आकार में जानता है, इसलिए यह संकलन-समय पर सरणी के पूरे आकार को जानता है। इसके अलावा एक वैश्विक चर में डिफ़ॉल्ट रूप से स्थिर भंडारण अवधि होती है: इसे प्रक्रिया मेमोरी स्पेस (.डेटा / .bs अनुभाग) के स्थिर स्मृति क्षेत्र में आवंटित किया जाता है। उस जानकारी को देखते हुए, संकलक संकलन के दौरान तय करता है कि उस स्थिर स्मृति क्षेत्र के पते में सरणी क्या होगी

बेशक कि स्मृति पते आभासी पते हैं। कार्यक्रम मानता है कि इसकी अपनी संपूर्ण मेमोरी स्पेस है (उदाहरण के लिए 0x00000000 से 0xFFFFFFFF तक)। यही कारण है कि संकलक धारणाएं कर सकता है जैसे "ठीक है, सरणी 0x00A33211 पते पर होगी"। रनटाइम पर उन पते को एमएमयू और ओएस द्वारा वास्तविक / हार्डवेयर पते पर अनुवादित किया जाता है।

मूल्य प्रारंभिक स्थिर भंडारण चीजें थोड़ा अलग हैं। उदाहरण के लिए:

int array[] = { 1 , 2 , 3 , 4 };

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

कंपाइलर द्वारा उत्पन्न असेंबली के दो उदाहरण यहां दिए गए हैं (x864.8.1 x86 लक्ष्य के साथ):

सी ++ कोड:

int a[4];
int b[] = { 1 , 2 , 3 , 4 };

int main()
{}

आउटपुट असेंबली:

a:
    .zero   16
b:
    .long   1
    .long   2
    .long   3
    .long   4
main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, %eax
    popq    %rbp
    ret

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

8.5.9 (प्रारंभकर्ता) [नोट]:
स्थैतिक भंडारण अवधि की प्रत्येक वस्तु किसी अन्य प्रारंभिक -करण होने से पहले कार्यक्रम स्टार्टअप पर शून्य-प्रारंभिक होती है। कुछ मामलों में, अतिरिक्त प्रारंभिक बाद में किया जाता है।

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







terminology