Rust के `collect:<Vec<_>>()` मेमोरी-लीक फुटगन की पहचान

Rust के beta में `collect::<Vec<_>>()` पर एक optimization, एक `Vec` को दूसरे `Vec` में convert करते समय allocations को reuse करती है, जिससे बड़े unused capacities चुपचाप बने रह सकते हैं—खासकर जब बड़े element types से छोटे element types में mapping की जाती है—और live data की तुलना में memory usage सैकड़ों गुना बढ़ सकता है। टिप्पणीकार इस पर बहस करते हैं कि क्या यह एक वास्तविक “memory leak” है या growth और reuse strategies का अपेक्षित परिणाम, और “space leaks”, principle of least astonishment, तथा अन्य भाषाएँ over-allocation को कैसे संभालती हैं, जैसे विषयों पर चर्चा होती है। कई remedies और alternatives सुझाए जाते हैं, जिनमें explicit `shrink_to_fit` calls, fixed data के लिए `Box<[T]>` या `Box<str>` का उपयोग, और memory-intensive code में data structures पर पुनर्विचार शामिल है, जबकि एक खुला Rust bug report ट्रैक करता है कि इस optimization को कैसे समायोजित किया जाना चाहिए।

क्या यह सचमुच एक “मेमोरी लीक” है?

  • कई लोगों का तर्क है कि यह वास्तविक लीक नहीं है: मेमोरी अभी भी Vec के स्वामित्व में है और ड्रॉप होने पर मुक्त हो जाती है; यह केवल अधिक-आवंटन है।
  • अन्य लोग नोट करते हैं कि मैनेज्ड-लैंग्वेज संस्कृति में इस तरह का स्थायी अधिक-आवंटन अक्सर बोलचाल में “मेमोरी लीक” या “स्पेस लीक” कहा जाता है।
  • कई पोस्टर इस बात पर ज़ोर देते हैं कि Rust में “लीक” का पहले से ही एक सटीक अर्थ है, इसलिए इसे ढीले ढंग से इस्तेमाल करना भ्रामक है, हालांकि व्यावहारिक प्रभाव (RAM समाप्त होना) फिर भी गंभीर है।

नया collect::<Vec<_>>() ऑप्टिमाइज़ेशन क्या करता है

  • बीटा Rust into_iter().collect() के जरिए कन्वर्ट करते समय मौजूदा Vec आवंटन का पुन: उपयोग करता है, यहाँ तक कि टाइप बदलने पर भी।
  • यह पुन: उपयोग दस्तावेज़ित नहीं है और कई प्रोग्रामरों की उस मानसिक मॉडल का उल्लंघन करता है जिसमें collect एक नया, उचित आकार का वेक्टर बनाता है।
  • इसका उद्देश्य आवंटन और कॉपीिंग बचाना था, लेकिन अब यह बड़े, अप्रचलित कैपेसिटी को बनाए रख सकता है।

आश्चर्यजनक कैपेसिटी वृद्धि (200x वाला मामला)

  • उदाहरण पैटर्न: बड़े कैपेसिटी वाले बड़े Vec<T> का निर्माण करें, फिर map/filter करके बहुत छोटे प्रकार U के कम आइटम्स बनाएं, और फिर collect::<Vec<U>>() करें।
  • पहले, एक नई, उचित आकार की आवंटन बनाई जाती थी; अब पुराना बड़ा बफर पुन: उपयोग होता है, इसलिए हर inner Vec में भारी मात्रा में अप्रयुक्त कैपेसिटी बनी रहती है।
  • सैकड़ों हज़ार inner vectors वाले Vec<Vec<_>> में, यह मिलकर गीगाबाइट्स RAM की बर्बादी में बदल जाता है।
  • कुछ लोग इसे स्पष्ट बग मानते हैं; अन्य कहते हैं कि जब कैपेसिटी मायने रखती है तो कोड को स्पष्ट रूप से shrink_to_fit() कॉल करना चाहिए।

मेमोरी व्यवहार, अपेक्षाएँ, और मानक

  • “principle of least astonishment” पर तीखी बहस है: कई लोगों को टाइप/आकार परिवर्तन के पार, विशेषकर जब cap >> len हो, पुन: उपयोग बहुत चौंकाने वाला लगता है।
  • अन्य लोग ज़ोर देते हैं कि मानक/लाइब्रेरी केवल एक कलेक्शन का वादा करती है, किसी विशिष्ट आवंटन व्यवहार का नहीं।
  • एक व्यापक साइड-ट्रैक C++ के IFNDR/UB मॉडल की Rust की अधिक सख़्त, compiler-checked semantics से तुलना करता है; Rust आम तौर पर चौंकाने वाले प्रोग्रामों को चुपचाप स्वीकार करने के बजाय उन्हें अस्वीकार करना पसंद करता है।

वास्तविक दुनिया का अधिक-आवंटन और विकल्प

  • ऐसी ही एक समस्या Aho–Corasick implementation में दिखाई दी: doubling strategy से बढ़ने वाले कई छोटे Vecs ने C implementation की तुलना में peak memory को दोगुना कर दिया।
  • linked-list-शैली की संरचनाओं (indices के साथ) पर स्विच करने से memory काफ़ी घट गई, जिससे पता चलता है कि Vec हमेशा सही विकल्प नहीं होता।

Mitigations और वैकल्पिक प्रकार

  • सुझाए गए उपाय: shrink_to_fit का उपयोग करें, immutable arrays के लिए Box<[T]>, strings के लिए Box<str>/Arc<str>, या ऐसी data structures डिज़ाइन करें जो कई growable Vecs से बचें।
  • कुछ लोग collect में heuristics प्रस्तावित करते हैं (जैसे, जब capacity length के गुणज से अधिक हो तो shrink करना), लेकिन अन्य hidden overhead और complexity को लेकर चिंतित हैं।