सिस्टम प्रोग्रामरों के लिए Garbage Collection (2023)
(bitbashing.io)- kernel और driver जैसे कोड में भी, जहां performance और concurrency अहम होते हैं, memory release सिर्फ
free()call से खत्म नहीं होता; lock-free sharing techniques की जरूरत पड़ सकती है - RCU(Read, Copy, Update) में बार-बार पढ़े जाने और कम बदले जाने वाले data को copy करने के बाद pointer को atomically replace किया जाता है, ताकि read path block न हो
- अगर पुराने version को तुरंत
deleteकर दिया जाए, तो अभी भी पढ़ रहे thread में use-after-free हो सकता है; इसलिए RCU read sections को track करता है और release को safe समय तक delay करता है - RCU Linux में दसियों हजार बार इस्तेमाल होता है, Folly C++ library और Rust के
crossbeam-epochमें भी मौजूद है, और use हो रहा है या नहीं, इसके आधार पर बाद में cleanup होने की वजह से इसका रूप GC जैसा है - manual memory management हमेशा ज्यादा तेज और predictable होता है—यह binary सोच कमजोर है;
free(), reference counting और OS memory behavior में भी cost और uncertainty होती है, इसलिए modern GC भी systems programming का tool बन सकता है
kernel code RCU जैसी techniques क्यों इस्तेमाल करता है
- रोज चलने वाले programs में operating system performance sensitivity के लिहाज से ऊंचे स्तर पर आता है
- OS तेज हो तो users ज्यादा computation कर सकते हैं, इसलिए kernel और driver developers code optimization पर काफी मेहनत करते हैं
- operating system को user-space processes और threads के साथ-साथ kernel के अपने कई threads और hardware interrupt handlers भी संभालने होते हैं
- latency बढ़े तो users का समय बर्बाद होता है, इसलिए kernel code में threads के बीच बिना lock के data share करने की कई techniques सामने आती हैं
RCU का basic behavior
- RCU(Read, Copy, Update) बहुत बार read होने लेकिन कम ही write होने वाले data के लिए उपयुक्त तरीका है
- अभी connected USB devices का set ऐसा example है, जो लगभग नहीं बदलता, लेकिन बदल सकता है
- बदलाव atomic होना चाहिए, और पहले से read कर रहे readers को रोकना नहीं चाहिए
- writer shared state को इस sequence में update करता है
- existing data को pointer से read करता है
- existing data को copy करता है और जरूरी बदलाव लागू करके नया version बनाता है
- pointer को atomically update करके उसे नए version की ओर point कराता है
- reader सिर्फ shared pointer read करता है, इसलिए read path simple रहता है और बिना wait किए चलता है
- यह तरीका use करने में आसान और wait-free है, लेकिन पुराने versions को cleanup न किया जाए तो memory leak होता है
पुराने versions जिन्हें तुरंत release नहीं किया जा सकता
- pointer को नए version पर बदलने के तुरंत बाद पुराने version को
deleteकरने पर use-after-free का risk पैदा होता है - क्योंकि यह बिना lock के काम करता है, writer नहीं जान सकता कि कोई reader अभी भी पुराने version को पढ़ रहा है या नहीं
- reader
rcu_read_lock()औरrcu_read_unlock()से read-side critical section mark कर सकता है- reader फिर भी block नहीं होता
- writer उन readers के बाहर निकलने तक पुराना data remove नहीं करता
rcu_synchronize()को सभी readers के खत्म होने तक wait करने की जरूरत नहीं; सिर्फ उन पुराने readers के खत्म होने तक wait करना काफी है जिनके पास पुराने version को देखने की संभावना है- नया pointer देखने वाला reader नया version use करता है, इसलिए उसका पुराने version की lifetime से संबंध नहीं
delayed release और GC जैसा रूप
- writer update function के अंदर wait न भी करे, फिर भी पुराना data किसी समय safely release हो जाए तो code सही तरीके से काम करता है
rcu_defer(old)जैसा तरीका current readers के critical section से बाहर निकलने के बाद किसी भी समयoldको release करने देता है- dedicated thread द्वारा पुराने unreferenced versions को periodically clean up करना generational GC जैसा दिखता है
- RCU कोई thought experiment नहीं, बल्कि वास्तव में व्यापक रूप से इस्तेमाल होने वाली technique है
- Linux RCU का दसियों हजार बार इस्तेमाल करता है
- Facebook की Folly C++ library में RCU दिया गया है
- Rust में यह
crossbeam-epochनाम से इस्तेमाल होता है, और लोकप्रिय concurrency libraries का आधार बनता है
- RCU “सचमुच GC” है या नहीं, इस classification debate से ज्यादा अहम बात यह है कि memory use में है या नहीं इसके आधार पर बाद में cleanup होने वाली संरचना GC जैसी ही है
manual release की छिपी हुई costs
- GC manual memory management से मूल रूप से कम efficient है—यह आम धारणा implementation details देखने पर कमजोर पड़ती है
-
free()मुफ्त नहीं है- general-purpose memory allocator को kernel से मिले pages, size-wise bucket splitting, और use में मौजूद buckets जैसी internal global state manage करनी पड़ती है
- कई threads allocator state को lock करने की कोशिश करते हैं, जिससे contention पैदा हो सकता है
- jemalloc जैसे thread-local pools रखने पर भी इन्हें synchronize करने के लिए additional code चाहिए
-
RAII और lifetime भी allocator cost को खत्म नहीं करते
- Rust की lifetime या C++ का RAII memory release automation और correctness में मदद करते हैं, लेकिन allocator की internal structure की complexity खत्म नहीं करते
- कई scenarios में
shared_ptrयाArcपर लौटना पड़ता है - reference count नाम का extra metadata चाहिए, और यह value cores और caches के बीच आ-जा कर cost बना सकती है
- liveness graph के cycles leak भी हो सकते हैं
-
GC कुछ optimizations भी दे सकता है
- moving generational GC heap को periodically फिर से compact करता है
- allocation pointer increment के करीब हो जाता है, जिससे high throughput मिल सकता है
- sequential allocation की locality बेहतर होती है, जिससे cache performance में भी मदद मिलती है
memory management control को लेकर भ्रम
- GC का विरोध करने वाले कई developers soft real-time systems बना रहे होते हैं
- video game FPS या streaming codec की compression performance की तरह वे maximum fast behavior चाहते हैं
- लेकिन यह hard latency requirement नहीं होती जहां कभी-कभी 1 millisecond ज्यादा लगने से system टूट जाए या किसी की जान चली जाए
- programmer memory management कब होगा यह तय कर सकता है—यह विश्वास इतना simple नहीं है
- operating system hardware interactions को abstract करता है
- Linux default रूप से memory request पर लगभग कुछ नहीं करता, और सच में use करने की कोशिश होने पर memory दे सकता है
madvise(), memory-mapped I/O, और filesystem cache मिल जाएं तो “क्या कब allocate हुआ” का simple answer नहीं रहता- बुरे दिन में simple pointer access भी disk I/O तक जा सकता है
- programmer हमेशा जानता है कि memory management के लिए रुकने का अच्छा समय कब है—यह विश्वास भी सीमित है
- video game loading screen जैसे clear cases भी होते हैं
- बहुत से software में एकमात्र जवाब यही होता है कि जब वह अधिक जरूरी काम में busy न हो
shared_ptrऔरArcरखने वाला individual code पहले से नहीं जान सकता कि वह last owner बनकर cleanup अपने ऊपर लेगा या नहीं
free()call का मतलब memory तुरंत OS को वापस मिलना है—यह विश्वास भी हमेशा सही नहीं- memory OS से page units में allocate होती है
- allocator अक्सर program exit तक pages को पकड़े रखकर reuse करना चाहता है
- OS swap के जरिए pages reclaim भी कर सकता है
GC को systems programming tool मानने की वजह
- हर software को GC से फायदा नहीं मिलता
- लेकिन 2024 के करीब के समय में भी systems programmers के बीच GC पर चर्चा गलत binary thinking और fear, uncertainty, doubt में दब जाना आसान है
- GC इस्तेमाल करने वाली languages manual memory management languages से “स्पष्ट रूप से” धीमी होती हैं—यह विचार fact नहीं, बल्कि ideology के ज्यादा करीब है
- life-critical systems बनाने वाली teams में भी ऐसे examples हैं जहां लगभग हर line में allocation करने वाली GC language से sub-microsecond latency दी गई
- अगर system का कोई हिस्सा जरूरी तौर पर
nclock cycles के अंदर चलना चाहिए, तो उस specific हिस्से को non-GC code या hardware में अलग किया जा सकता है - GC कोई universal solution नहीं है, लेकिन यह toolbox का एक tool है जिसे बिना डर के इस्तेमाल किया जा सकता है
1 टिप्पणियां
Hacker News टिप्पणियां
आशाजनक आधुनिक parallel garbage collection तकनीकों में MPL या MaPLe और उसका नया Automatic Management of Parallelism देखने लायक है
POPL 2024 distinguished paper award और ACM SIGPLAN dissertation award 2023 जीतने वाले मूल योगदान दो हैं: a) disentanglement-आधारित, सिद्ध रूप से efficient parallel garbage collection, b) सिद्ध रूप से efficient automatic granularity control
[1] MaPLe (MPL): https://github.com/MPLLang/mpl
[2] Automatic Parallelism Management: https://dl.acm.org/doi/10.1145/3632880
लिंक किए गए paper के अलावा, MLKit भी है, जो region-based memory management के शुरुआती users और pioneers में से एक था
उदाहरण के लिए, क्या यह approach Go की garbage collection को काफी तेज़ बना सकता है, या फिर मौजूदा language design की समस्याओं से टकराएगा?
RCU का use case भरोसेमंद है, लेकिन दूसरे संदर्भों में garbage collection का अनुभव अच्छा नहीं रहा
यह लेख इस दावे जैसा कम पढ़ता है कि static lifetimes आम तौर पर dynamic lifetimes से बेहतर हैं, और इस दावे जैसा ज़्यादा कि custom memory management समाधान सबसे अच्छी performance दे सकते हैं
मैं यह नहीं मानता कि
free()memory को operating system को लौटाता है, बल्कि allocator को लौटाता है। यह operating system को लौटाने से कहीं बेहतर है, और system calls धीमी होती हैं। हालांकि mimalloc की तरह freed memory को अगलेmallocमें तुरंत इस्तेमाल न करके केवल समय-समय पर उपलब्ध कराया जा सकता है800 bytes allocate करके तुरंत
freeकरने का काम 10 लाख बार दोहराकर unique pointers की संख्या गिनने पर glibc malloc में 1, jemalloc में 1, mimalloc में 4, और Julia garbage collector में 62767 निकले62767, लगभग 48MiB, बहुत खराब नहीं है, लेकिन फिर भी यह मेरे कंप्यूटर के L3 cache को बाहर धकेल देता है। garbage collection इस्तेमाल करने पर नई allocation cache से नहीं बल्कि RAM से आएगी, यह लगभग तय हो जाता है, और allocation-heavy code की performance बिगड़ती है। सिर्फ memory management की speed ही नहीं, बल्कि उससे मिली memory पर आप कितनी तेजी से काम कर सकते हैं, यह भी महत्वपूर्ण है
Julia में इसे दिखाने वाला benchmark पोस्ट किया था: https://discourse.julialang.org/t/blog-post-rust-vs-julia-in...
malloc/free, अगर actual working memory पर्याप्त छोटी हो, तो उसे cache में hot बने रहने का मौका देते हैं। mimalloc जैसे allocators भी compacting garbage collection की तरह contiguous allocations को पास-पास रखने के लिए design किए गए हैं, और मैंने mimalloc में जो 4 unique pointers देखे वे 896 bytes की दूरी पर थे
अगर compacting garbage collection का अनुभव ज़्यादा रहा होता तो शायद मेरी राय थोड़ी कम cynic होती, लेकिन मुझे लगता है कि safe memory management के लिए garbage collection, Rust के borrow checker जैसी चीज़ों से कहीं ज़्यादा जटिल समाधान है। वह जटिलता compiler और runtime developers पर डाल दी जाती है, इसलिए users के लिए आम तौर पर ठीक रहता है, और performance-sensitive न होने वाला code लिखते समय यह स्वीकार्य trade-off है। static lifetimes वाला RAII भी ऐसे code के लिए reasonable trade-off है जिसे अधिक custom approach की ज़रूरत नहीं होती, और लेख का उदाहरण स्पष्ट रूप से ऐसा मामला है जहाँ custom समाधान चाहिए
compacting garbage collection में long-running programs में heap fragmentation TLB cache entries और objects के बीच की खाली जगह बर्बाद करती है, इसलिए लगभग हमेशा malloc से बेहतर cache utilization होता है। compacting garbage collection का bump allocator हर allocation पर नया pointer देता है क्योंकि
freememory reclaim नहीं करता, लेकिन वे allocations sequential होती हैं और अगर heap को लगातार consume करते हुए केवल सबसे recent objects को छुआ जा रहा हो, तो वे अब भी cache में रहती हैं। allocators और garbage collectors के ripple effects का benchmark करना बेहद कठिन है, और ऐसे synthetic benchmarks को मैं लगभग हमेशा संदेह से देखता हूँलोगों की चिंता यह है कि memory अब इस्तेमाल में नहीं है या नहीं, यह पता लगाने की प्रक्रिया allocator को सीधे यह बताने से अधिक inefficient और non-deterministic है कि resource का उपयोग खत्म हो गया है। मैंने किसी को केवल deallocation टालने को लेकर चिंतित होते नहीं देखा
पूरे live set को traverse करना दुर्लभ है, और पिछले 30 वर्षों में garbage collection algorithms को सुधारकर लगभग चेतन-स्तर तक पहुँचा दिया गया है, लेकिन यह वाक्य लोगों की वास्तविक आपत्ति को, चाहे जानबूझकर हो या नहीं, नज़रअंदाज़ करता है। जब किसी service में garbage collection की समस्या आती है, तो ऐसा लगता है जैसे इधर-उधर tuning करके क्रोधित आत्माओं को shadow world में वापस भेजने की आशा में किसी shaman को बुलाना पड़े
अगर तरीका garbage को mark करने और उसके उपयोग में न रहने पर notification पाने का हो, तो यह पूरी प्रक्रिया गायब हो जाती है। garbage collection में memory allocation बहुत तेज हो सकती है, लेकिन fair comparison के लिए marking और compaction की लागत को भी amortize करके शामिल करना चाहिए
एक और बड़ी समस्या यह है कि समान performance पाने के लिए garbage collection को आम तौर पर manual memory management की तुलना में कहीं ज़्यादा memory चाहिए होती है। यह बार-बार दोबारा जाँचने के लिए extra CPU भी चाहिए कि references अब भी मौजूद हैं या नहीं, और optimistic compaction के लिए अतिरिक्त memory copying भी झेलनी पड़ती है
अंत में, लेख यह आलोचना करता है कि lifetime अस्पष्ट होने पर Rust के Arc/Rc जैसे manual memory management की ज़रूरत पड़ती है, लेकिन यह नज़रअंदाज़ करता है कि garbage-collected languages में भी finalizer के निश्चित रूप से call होने पर भरोसा नहीं किया जा सकता, इसलिए external resources बंद करने के लिए व्यवहार में लगभग वही infrastructure बनाना पड़ता है
यह बहस पिछले 20–30 वर्षों में काफी बार दोहराई जा चुकी है, और यह लेख वैध garbage collection concerns को meme की तरह खारिज करने के अलावा कुछ नया लाता नहीं दिखता। memes मजेदार हैं, इसलिए ठीक है, लेकिन सही जवाब यह है कि कोई सामान्य सही जवाब नहीं है। system की design constraints को पूरा करने के लिए जो tool उपयुक्त हो, वही इस्तेमाल करें
बाकी long-lived allocations परिभाषा के अनुसार cache के नजरिए से control करना आसान नहीं है। locality garbage collection के बड़े फायदे में से एक है, और मेरी जानकारी में एकमात्र समस्या stop-the-world mark/sweep है। मुझे पता है कि modern garbage collection background threads रखता है, लेकिन फिर भी stop-the-world events होते हैं, ऐसा मेरी समझ है
free()memory को allocator को लौटाता है, लेकिन long-running server में memory fragmentation से निपटना बिल्कुल भी मजेदार नहीं हैखासकर slab allocator द्वारा manage किए गए pages की internal fragmentation, और यह सामान्य समस्या नहीं है लेकिन मुश्किल समस्या है
सौभाग्य से garbage collection को ऐसे सुविधाजनक ढंग से गढ़े गए उदाहरणों से मुकाबला करने वाले क्षेत्र ज्यादा नहीं हैं
उन खास मामलों को छोड़कर जहां सारी memory को आसानी से arena से संभाला जा सकता है, अच्छी tracing garbage collection throughput में बहुत पहले ही manual memory management से आगे निकल चुकी है, और हाल में latency पर असर भी ज़्यादातर applications में पर्याप्त रूप से स्वीकार्य हो गया है
OpenJDK का ZGC सामान्य pause time को दो-तीन अंकों वाले microseconds के स्तर पर रखता है, और वाजिब allocation rate पर worst case भी शायद ही 1ms से ऊपर जाता है; यह operating system से होने वाले pauses जैसी ही range में है
असली महत्वपूर्ण trade-off सिर्फ memory usage है। कुछ खास niche मामलों—जहां arena हर चीज़ में अच्छी तरह फिट बैठता है और worst latency कम microsecond range में होती है—को छोड़ दें तो मुख्य सवाल एक ही है: क्या मेरी application memory-constrained environment में चलती है, या RAM usage घटाने के लिए कुछ और sacrifice करना worth it है
ऐसे code में individual object lifetime track करना overkill है। आखिर memory management lifetime के बारे में ही है, और बहुत सारी अलग-अलग lifetimes के बजाय कम अलग-अलग lifetimes हमेशा बेहतर होती हैं, क्योंकि manual हो या automatic, करने का काम कम हो जाता है
object lifetime के बारे में न सोचना बहुत सुविधाजनक है, और इसी वजह से अच्छे garbage collector की internal complexity काफी होने के बावजूद garbage-collected languages सफल हुईं
unpredictable tail latency और कई exceptional situations का असर भी बना रहता है
ZGC में Shenandoah की तुलना में pauses की संख्या कम दिखती है, इसलिए लगता है कि वह हर pause में थोड़ा ज्यादा काम करता है, और performance थोड़ी बेहतर दिखती है
अभी production environment में test करना बाकी है, लेकिन अब तक ZGC, और Java 21 के बाद वाला generational ZGC इस्तेमाल करने पर garbage collection pauses काफी हद तक solved problem लगते हैं
Jai के नजरिए की तरह memory allocation को आम होने के क्रम में चार हिस्सों में बांटा जा सकता है: 1) बेहद कम समय तक जीने वाली चीजें जिन्हें function stack पर रखा जा सकता है, 2) कम समय तक जीने वाली और well-defined lifetime वाली चीजें जिन्हें frame/request-level memory arena में रखा जा सकता है, 3) लंबे समय तक जीने वाली और well-defined owner वाली चीजें जिन्हें subsystem-specific pool से manage किया जा सकता है, 4) लंबे समय तक जीने वाली और unclear owner वाली चीजें जिन्हें dynamic memory management चाहिए
यह दावा करने के लिए कि tracing garbage collection सामान्यतः manual memory management से आगे है, तुलना ऐसे systems से करनी चाहिए जो इस नजरिए को ध्यान में रखकर लिखे गए हों, न कि ऐसे system से जो जगह-जगह malloc/free call करता हो। modern C++/Rust practices से तुलना ज्यादा fair हो सकती है
मैं मानता हूं कि ज़्यादातर systems में tracing garbage collection पर rely करना शायद कहीं ज्यादा practical है, लेकिन वह बिल्कुल अलग दावा है
लेख RCU के लिए motivation देने के बाद U-turn लेकर general-purpose garbage collection का सामान्य समर्थन शुरू कर देता है
इसे Trojan horse तो नहीं कहूंगा, लेकिन बदलाव काफी अचानक लगता है
object तीन states में से एक में होता है और जितनी जल्दी हो सके transition करता है: active, obsolete but alive for old readers, deallocated
code लिखने के तरीके के अनुसार “obsolete-but-alive” object को “new” allocation के लिए safely reuse किया जा सकता है, लेकिन performance का पूरा analysis मैंने नहीं किया है
garbage collection discussions में जैसा आम है, कब
shared_ptr/Arcपर “पीछे हटना” चाहिए, यह बहुत अस्पष्ट है। असल में reference counting से बचना—यानी यह prove करना कि ownership पहले से है, या indirection को पूरी तरह avoid करना—serious reference-counting-based systems का core है। कुछ न करना, garbage collection के “कभी न कभी कुछ करने” से स्वाभाविक रूप से बेहतर हैमेरे द्वारा इस्तेमाल किए जाने वाले software में दो cases होते हैं। (1) hot paths जहां हमेशा custom allocator इस्तेमाल होता है और allocation से बचा जाता है, (2) बाकी सब कुछ
(1) में garbage collection हो या न हो, फर्क नहीं पड़ता, और मैं उससे बाहर निकल जाऊंगा। (2) में garbage collection सच में convenient और correct है
Java ने modern garbage collectors के साथ जो काम किया है वह impressive है, लेकिन वे भी Valhalla के जरिए indirectly मान रहे हैं कि no-allocation/low-allocation code की जगह है
आधुनिक उपयोगकर्ता-केंद्रित operating systems, यानी किसी खास RTOS से अलग operating systems, में built-in garbage collection होता है—यह अवलोकन यहाँ थोड़ा छूट जाता है
हम बस उसे ऐसा नहीं कहते, बल्कि memory management कहते हैं। जिन languages में built-in garbage collection होता है, उन्हें क्या कहते हैं? memory-managed languages
पुराने “ऊपर से नीचे” चलने वाले C programs में यह अक्सर दिखता है। allocate करते हैं, system resources साफ करते हैं, लेकिन
freeकी परवाह नहीं करते। program खत्म होने पर operating system वह सारी memory वापस ले लेता है, तो फिर क्यों करेंयहाँ अवसर है कि operating system स्तर का ऐसा garbage collector बनाया जाए जो program से कम अलग-थलग हो, और ऐसा operating system जो language runtime garbage collector की तरह resources को संभाले। लेकिन आम तौर पर garbage-collected languages में garbage collector runtime की लगभग हर line से जटिल रूप से जुड़ा होता है, इसलिए किसी एक operating system के लिए distribution में ही वह control operating system को सौंपना व्यावहारिक नहीं है
फिर भी अफसोस है। program-level memory management और operating-system-level memory management के कृत्रिम अलगाव से पैदा होने वाली पुरानी समस्याओं को सुधारने की काफी गुंजाइश है
memory release करने की एकमात्र वजह long-running application में, operating system से नई memory और लिए बिना, उसे दूसरी allocations के लिए reuse करना है। एक बार चलकर खत्म होने वाले command-line tools में आम तौर पर इसकी जरूरत नहीं होती
अगर process को काम पूरा होते ही exit करने के लिए design किया गया है, तो operating system को garbage collector की तरह इस्तेमाल किया जा सकता है
लेकिन चलते हुए program के भीतर कौन-सी memory अब इस्तेमाल नहीं हो रही, यह जानने का operating system के पास कभी कोई तरीका नहीं रहा। कुछ गूढ़ research operating systems अपवाद हो सकते हैं। इसलिए इसे छूटा हुआ अवसर कहने के बजाय, मुझे लगता है कि जिसे छूटा कहा जा रहा है वह meaningful रूप में मौजूद ही नहीं है
दूसरी ओर, बहुत सरल और कम समय तक जीवित रहने वाले programs लिखने की programming style पूरी तरह वैध है। CLI tools और उन्हें script करने वाली scripting languages ऐसे ही काम करती हैं, पुराने web servers भी CGI आदि से ऐसे ही काम करते थे, और आज भी यह पूरी तरह समझदार approach है
(1) RCU से general-purpose tracing garbage collection की ओर जाने वाला बदलाव bait-and-switch जैसा लगता है
(2) manual memory management सिर्फ malloc/free calls नहीं, बल्कि layout के बारे में है। उदाहरण के लिए structure arrays को अलग करना, inlining, implicit offsets, packing आदि
लगता है “array of structures” की बात की गई है; इसे tuple arrays से किया जा सकता है और target के हिसाब से naturally flatten/normalize हो जाता है। यानी native target पर यह array of structures बन जाता है
byte-accurate layout भी define किया जा सकता है[1], जिसका इस्तेमाल मुख्य रूप से दूसरे software के साथ interop या binary formats parse करने में होता है। algebraic data types को unbox किया जा सकता है, और जल्द ही algebraic data types की exact encoding तक control की जा सकेगी
Virgil garbage collection का इस्तेमाल करता है
[1] https://github.com/titzer/virgil/blob/master/doc/tutorial/La...
आधुनिक managed languages में memory layout control की कमी जरूर है, लेकिन low-level languages भी perfection से बहुत दूर हैं, और उस पर असर डालने के तरीके भी साफ तौर पर मौजूद हैं
इस लेख में एक बात छूट गई है कि async/await garbage collection के साथ बहुत अच्छी तरह मेल खाता है
अपनी कुछ निजी style-related वजहों से मैं async/await को पसंद नहीं करता, लेकिन इस पर लंबा नहीं बोलूँगा
TypeScript/JavaScript में इसे काफी इस्तेमाल किया है और Dart में भी इस्तेमाल किया है, और वहाँ यह उम्मीद के मुताबिक काम करता है
Rust में भी इस्तेमाल किया है, और मेरे हिसाब से यह disaster है। multi-threaded runtime में async/await इस्तेमाल करने के लिए जिस तरह की memory management को जबरन फिट करना पड़ता है, वह नर्क जैसा है
https://doc.rust-lang.org/std/pin/index.html
गार्बेज कलेक्शन के समर्थन में लिखे कई लेखों में एक बात छूट जाती है, और यह लेख भी ऐसा ही लगता है: मेमोरी भी सिर्फ एक तरह का resource है
खासकर system programming में, सही code को file handles, sockets जैसे बाहरी resources भी manage करने पड़ते हैं। गार्बेज कलेक्शन सिर्फ application memory वाला हिस्सा हल करता है, इसलिए ऐसे बाहरी resources को handle करने में बिल्कुल मदद नहीं करता। उल्टा, यह चीज़ों को कहीं ज़्यादा जटिल बना सकता है; .NET में ज़रा-सा भी non-trivial IDisposable सही तरीके से implement करने के लिए क्या-क्या चाहिए, बस वही देख लें
RAII या reference counting जैसे approaches मेरे अनुभव में memory और external resources को एक unified तरीके से संभालना काफी आसान बना देते हैं, जिससे सही code लिखना और उसके बारे में reason करना भी आसान हो जाता है
इसका मतलब यह नहीं कि मैं गार्बेज कलेक्शन का खुलकर विरोध कर रहा हूँ। बाकी हर चीज़ की तरह यह भी फायदे-नुकसान वाला एक tool है। लेख में बताया गया “manual GC” RCU approach कुछ खास कामों के लिए दिलचस्प है
यह यूँ ही नहीं है कि computation के ज्यादातर theoretical models infinite memory मानकर चलते हैं। operating system kernels या hard real-time applications जैसे कुछ software में processing power भी manually allocate की जाती है, लेकिन बहुत कम languages processing power को manually allocate करने की मांग करती हैं
इसी तरह के कारणों से automatic memory management computation को abstract करने में बहुत उपयोगी है। यह subroutine की memory details को caller तक leak नहीं होने देता, और CPU usage में भी ऐसी details बहुत कम ही सामने आती हैं
हर non-trivial computation में किसी-न-किसी non-constant मात्रा में processing और memory शामिल होती है, लेकिन I/O आम तौर पर system के edges पर होता है। I/O management भी बेशक बहुत महत्वपूर्ण है, लेकिन computation की अवधारणा और computational abstraction की centrality में यह processing और memory जितना मूलभूत नहीं है
उदाहरण के लिए Rust में आखिरकार I/O safety आ गई, इसलिए Unix का
OwnedFdजैसा file handle या Windows काOwnedHandleजैसा handle अब संख्या 4 जैसे integer नहीं, बल्कि owned objects हैंऊपर से यह handles पर arithmetic करने या reserved values को sentinel की तरह गलत इस्तेमाल करने जैसी मूर्खतापूर्ण गलतियों से बचने की बात लगती है, लेकिन ownership model की वजह से handles के साथ कठिन काम करने पर भी explicit ownership मिलती है, और बाद के maintainers के लिए भी यह transparent रहता है
RAII की value सच में बहुत ज्यादा महसूस हुई
किसी language में RAII होता है, कोई language keywords देती है, और कोई language arena जैसी management या implicit management वाली lambdas देती है। कोई language type system से थोड़ी मदद लेती है, और कोई ऊपर की चीज़ों को थोड़ा-थोड़ा मिलाती है
इसके अलावा, जैसे system developers को static analyzers पर निर्भर रहना पड़ता है, वैसे ही ऐसी languages के static analyzers भी type system अकेले पर्याप्त न होने पर छूटी हुई चीज़ों को verify कर सकते हैं
उदाहरण के लिए Java का try-with-resources statement यह guarantee देता है कि exception आने पर भी resources safely release होंगे: https://docs.oracle.com/javase/tutorial/essential/exceptions...
ऐसे basic building blocks से भी काफी मजबूत और resource-safe systems बनाए जा सकते हैं