कॉलॉक की तुलना में मॉलोक + मेमसेट धीमा क्यों है?


256

यह ज्ञात है कि इस callocसे अलग है mallocकि यह आवंटित स्मृति को इनिशियलाइज़ करता है। इसके साथ calloc, मेमोरी शून्य पर सेट है। के साथ malloc, स्मृति साफ़ नहीं है।

इसलिए रोजमर्रा के काम में, मैं + callocको मानता हूं । संयोग से, मनोरंजन के लिए, मैंने एक बेंचमार्क के लिए निम्नलिखित कोड लिखा था।mallocmemset

परिणाम भ्रामक है।

कोड 1:

#include<stdio.h>
#include<stdlib.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)calloc(1,BLOCK_SIZE);
                i++;
        }
}

कोड 1 का आउटपुट:

time ./a.out  
**real 0m0.287s**  
user 0m0.095s  
sys 0m0.192s  

कोड 2:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)malloc(BLOCK_SIZE);
                memset(buf[i],'\0',BLOCK_SIZE);
                i++;
        }
}

कोड 2 का आउटपुट:

time ./a.out   
**real 0m2.693s**  
user 0m0.973s  
sys 0m1.721s  

कोड 2 में प्रतिस्थापित memsetकरने bzero(buf[i],BLOCK_SIZE)से समान परिणाम प्राप्त होता है।

मेरा सवाल है: क्यों malloc+ memsetइतना धीमा है calloc? वह कैसे कर callocसकता है?

जवाबों:


455

लघु संस्करण: calloc()इसके बजाय हमेशा उपयोग करें malloc()+memset()। ज्यादातर मामलों में, वे एक ही होंगे। कुछ मामलों में, calloc()कम काम करेगा क्योंकि यह memset()पूरी तरह से छोड़ सकता है । अन्य मामलों में, calloc()धोखा भी दे सकते हैं और किसी भी स्मृति को आवंटित नहीं कर सकते हैं! हालांकि, malloc()+memset()काम की पूरी राशि हमेशा करेंगे।

इसे समझने के लिए मेमोरी सिस्टम के छोटे दौरे की आवश्यकता होती है।

स्मृति का त्वरित दौरा

यहाँ चार मुख्य भाग हैं: आपका प्रोग्राम, मानक पुस्तकालय, कर्नेल और पेज टेबल। आप पहले से ही अपना कार्यक्रम जानते हैं, इसलिए ...

स्मृति आवंटनकर्ता पसंद करते हैं malloc()और calloc()अधिकतर छोटे आवंटन (कुछ भी 1 बाइट से 100 केबी तक) लेते हैं और उन्हें मेमोरी के बड़े पूल में समूहित करते हैं। उदाहरण के लिए, यदि आप 16 बाइट्स आवंटित करते हैं, तो malloc()पहले अपने एक पूल से 16 बाइट्स प्राप्त करने का प्रयास करेंगे, और फिर पूल के सूखने पर कर्नेल से अधिक मेमोरी के लिए कहें। हालाँकि, चूंकि आप जिस प्रोग्राम के बारे में पूछ रहे हैं, वह एक बार में बड़ी मात्रा में मेमोरी के लिए आवंटित हो रहा है, malloc()और calloc()कर्नेल से सीधे उस मेमोरी के लिए पूछेगा। इस व्यवहार के लिए सीमा आपके सिस्टम पर निर्भर करती है, लेकिन मैंने 1 MiB को सीमा के रूप में उपयोग किया है।

कर्नेल प्रत्येक प्रक्रिया को वास्तविक रैम आवंटित करने और यह सुनिश्चित करने के लिए ज़िम्मेदार है कि प्रक्रियाएं अन्य प्रक्रियाओं की स्मृति में हस्तक्षेप नहीं करती हैं। इसे मेमोरी प्रोटेक्शन कहा जाता है , यह 1990 के दशक से आम गंदगी है, और यही कारण है कि एक कार्यक्रम पूरे सिस्टम को नीचे लाए बिना क्रैश कर सकता है। इसलिए जब किसी प्रोग्राम को अधिक मेमोरी की आवश्यकता होती है, तो यह सिर्फ मेमोरी नहीं ले सकता है, बल्कि यह कर्नेल से सिस्टम कॉल mmap()या जैसे मेमोरी का उपयोग करता है sbrk()। कर्नेल पृष्ठ तालिका को संशोधित करके प्रत्येक प्रक्रिया को RAM देगा।

पृष्ठ तालिका वास्तविक भौतिक RAM में मेमोरी पते को मैप करती है। आपकी प्रक्रिया के पते, 0x00000000 से 0xFFFFFFFF एक 32-बिट सिस्टम पर, वास्तविक मेमोरी नहीं हैं, बल्कि वर्चुअल मेमोरी में पते हैं प्रोसेसर इन पतों को 4 KiB पेजों में विभाजित करता है, और प्रत्येक पेज को पेज टेबल को संशोधित करके भौतिक RAM के एक अलग टुकड़े को सौंपा जा सकता है। पृष्ठ तालिका को संशोधित करने के लिए केवल कर्नेल की अनुमति है।

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

यहाँ कैसे आवंटन 256 MiB करता है नहीं काम करते हैं:

  1. आपका प्रोसेस कॉल करता है calloc()और 256 MiB मांगता है।

  2. मानक पुस्तकालय कॉल करता है mmap()और 256 MiB के लिए पूछता है।

  3. कर्नेल अनुपयोगी RAM के 256 MiB पाता है और पृष्ठ तालिका को संशोधित करके आपकी प्रक्रिया को देता है।

  4. मानक पुस्तकालय रैम को शून्य करता है memset()और इससे लौटता है calloc()

  5. आपकी प्रक्रिया अंततः बाहर निकल जाती है, और कर्नेल रैम को पुनः प्राप्त करता है ताकि इसे किसी अन्य प्रक्रिया द्वारा उपयोग किया जा सके।

यह वास्तव में कैसे काम करता है

उपरोक्त प्रक्रिया काम करेगी, लेकिन यह इस तरह से नहीं होता है। तीन प्रमुख अंतर हैं।

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

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

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

अंतिम प्रक्रिया इस तरह दिखाई देती है:

  1. आपका प्रोसेस कॉल करता है calloc()और 256 MiB मांगता है।

  2. मानक पुस्तकालय कॉल करता है mmap()और 256 MiB के लिए पूछता है।

  3. कर्नेल अप्रयुक्त पता स्थान के 256 MiB पाता है, इस बारे में एक नोट बनाता है कि अब उस पता स्थान का उपयोग किस लिए किया जाता है, और रिटर्न।

  4. मानक पुस्तकालय जानता है कि का परिणाम mmap()हमेशा शून्य से भर जाता है (या हो जाएगा एक बार यह वास्तव में कुछ रैम हो जाता है), तो यह स्मृति स्पर्श नहीं करता है, इसलिए कोई पृष्ठ दोष है, और राम अपनी प्रक्रिया को कभी नहीं दिया गया है ।

  5. आपकी प्रक्रिया अंततः बाहर निकल जाती है, और कर्नेल को रैम को पुनः प्राप्त करने की आवश्यकता नहीं होती है क्योंकि यह पहली बार में आवंटित नहीं किया गया था।

यदि आप memset()पृष्ठ को शून्य करने के लिए उपयोग करते हैं , memset()तो पृष्ठ दोष को ट्रिगर किया जाएगा, रैम को आवंटित करने का कारण होगा, और फिर इसे शून्य होगा भले ही यह पहले से ही शून्य से भरा हो। यह अतिरिक्त काम की एक बड़ी मात्रा है, और बताते हैं कि क्यों और calloc()से तेज है । तो अंत वैसे भी स्मृति का उपयोग कर, की तुलना में तेजी अब भी है और लेकिन अंतर काफी इतना हास्यास्पद नहीं है।malloc()memset()calloc()malloc()memset()


यह हमेशा काम नहीं करता है

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

यह भी हमेशा छोटे आवंटन के साथ काम नहीं करेगा। छोटे आवंटन के साथ, calloc()कर्नेल पर सीधे जाने के बजाय एक साझा पूल से मेमोरी प्राप्त करता है। सामान्य तौर पर, साझा किए गए पूल में पुरानी मेमोरी से इसमें जमा किया गया डेटा हो सकता है जिसे इस्तेमाल किया गया था और इसके साथ मुक्त किया गया था free(), इसलिए calloc()उस मेमोरी को ले सकते हैं और memset()इसे खाली करने के लिए कॉल कर सकते हैं। सामान्य कार्यान्वयन ट्रैक करेंगे कि साझा पूल के कौन से हिस्से प्राचीन हैं और फिर भी जीरो से भरे हुए हैं, लेकिन सभी कार्यान्वयन ऐसा नहीं करते हैं।

कुछ गलत उत्तरों को खारिज करना

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

calloc()समारोह की कुछ विशेष स्मृति गठबंधन संस्करण का उपयोग नहीं कर रहा है memset(), और यह है कि यह बहुत तेजी से वैसे भी नहीं होगा। memset()आधुनिक प्रोसेसर के लिए अधिकांश कार्यान्वयन इस तरह दिखते हैं:

function memset(dest, c, len)
    // one byte at a time, until the dest is aligned...
    while (len > 0 && ((unsigned int)dest & 15))
        *dest++ = c
        len -= 1
    // now write big chunks at a time (processor-specific)...
    // block size might not be 16, it's just pseudocode
    while (len >= 16)
        // some optimized vector code goes here
        // glibc uses SSE2 when available
        dest += 16
        len -= 16
    // the end is not aligned, so one byte at a time
    while (len > 0)
        *dest++ = c
        len -= 1

तो आप देख सकते हैं, memset()बहुत तेज़ है और आप वास्तव में मेमोरी के बड़े ब्लॉक के लिए कुछ भी बेहतर नहीं कर सकते हैं ।

तथ्य यह memset()है कि शून्य मेमोरी जो पहले से ही शून्य है, इसका मतलब यह है कि मेमोरी दो बार शून्य हो जाती है, लेकिन यह केवल 2x प्रदर्शन अंतर की व्याख्या करता है। यहां प्रदर्शन का अंतर बहुत बड़ा है (मैंने अपनी प्रणाली के बीच परिमाण के तीन से अधिक क्रमों को मापा malloc()+memset()और calloc())।

पार्टी की चाल

10 बार लूपिंग करने के बजाय, एक प्रोग्राम लिखें जो मेमोरी को तब तक आवंटित करता है जब तक कि malloc()या calloc()NULL वापस नहीं करता है।

यदि आप जोड़ते हैं तो क्या होता है memset()?


7
@ डिट्रिच: ओएसच के बारे में डायट्रीच के आभासी स्मृति स्पष्टीकरण ने कॉलोक के लिए एक ही शून्य भरे हुए पृष्ठ को कई बार आवंटित किया है, यह जांचना आसान है। बस कुछ लूप जोड़ें जो हर आवंटित मेमोरी पेज में जंक डेटा लिखते हैं (प्रत्येक 500 बाइट्स में एक बाइट लिखना पर्याप्त होना चाहिए)। समग्र परिणाम तब बहुत करीब हो जाना चाहिए क्योंकि दोनों मामलों में सिस्टम को वास्तव में विभिन्न पृष्ठों को आवंटित करने के लिए मजबूर किया जाएगा।
क्रिस्स

1
@kriss: वास्तव में, हालांकि हर 4096 बाइट एक सिस्टम के विशाल बहुमत पर पर्याप्त है
Dietrich Epp

वास्तव में, calloc()अक्सर mallocकार्यान्वयन सुइट का हिस्सा होता है , और इस तरह से जब मेमोरी से कॉल नहीं किया जाता है तो अनुकूलित किया जाता bzeroहै mmap
मिराबिलोस

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

5
जबकि गति संबंधित नहीं है, callocयह भी कम बग प्रवण है। यही है, जहां large_int * large_intएक अतिप्रवाह में परिणाम होगा, calloc(large_int, large_int)रिटर्न NULL, लेकिन malloc(large_int * large_int)अपरिभाषित व्यवहार है, क्योंकि आप नहीं जानते कि मेमोरी ब्लॉक का वास्तविक आकार वापस आ रहा है।
टिब्बा

12

क्योंकि कई प्रणालियों पर, अतिरिक्त प्रसंस्करण समय में, ओएस अपने आप ही मुफ्त मेमोरी को शून्य पर सेट करने और इसे सुरक्षित करने के लिए चारों ओर चला जाता है calloc(), इसलिए जब आप कॉल करते हैं, तो calloc()यह आपके पास पहले से ही मुफ्त, शून्य मेमोरी हो सकती है।


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

@ डिट्रिच - निश्चित नहीं। मैंने इसे एक बार सुना था और यह calloc()अधिक कुशल बनाने के लिए एक उचित (और यथोचित सरल) तरीका लग रहा था ।
क्रिस लुट्ज़

@Pierreten - मैं calloc()विशिष्ट अनुकूलन पर कोई अच्छी जानकारी नहीं पा सकता और मुझे ओपी के लिए libc स्रोत कोड की व्याख्या करने का मन नहीं है। क्या आप यह दिखाने के लिए कुछ भी देख सकते हैं कि यह अनुकूलन मौजूद नहीं है / काम नहीं करता है?
क्रिस लुत्ज

13
@ डिट्रिच: फ्रीबीएसडी निष्क्रिय समय में पृष्ठों को शून्य-भरने के लिए माना जाता है: इसकी vm.idlezero_bable सेटिंग देखें।
ज़ैन लिंक्स

1
@DietrichEpp necro के लिए खेद है, लेकिन उदाहरण के लिए विंडोज ऐसा करता है।
एंड्रियास ग्रेपेंटिन

1

कुछ मोड में कुछ प्लेटफार्मों पर मॉलॉक मेमोरी को वापस करने से पहले आम तौर पर गैर-शून्य मूल्य पर शुरू करता है, इसलिए दूसरा संस्करण अच्छी तरह से मेमोरी को दो बार शुरू कर सकता है

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