Kodokon kodokon.com

प्रदर्शन: प्रोफाइलिंग, GIL, जनरेटर बनाम लिस्ट, मेमोरी के जाल

ऑप्टिमाइज़ करने से पहले मापें: timeit और cProfile, GIL असल में क्या रोकता है, जनरेटर की मितव्ययिता और स्ट्रिंग तथा स्लाइस के मेमोरी जाल।

11 मिनट · 3 प्रश्न

इस पाठ को Kodokon में खोलें

कभी अनुमान न लगाएँ, मापें। timeit एक भरोसेमंद माइक्रो-बेंचमार्क अलग कर देता है: यह गार्बेज कलेक्टर को बंद कर देता है और शोर को समतल करने के लिए निष्पादन दोहराता है। cProfile पूरे प्रोग्राम की कॉल का नक्शा बनाता है - असली दोषियों की पहचान के लिए cumtime (संचयी समय, उप-कॉल सहित) से क्रमबद्ध करें। किसी भी प्रदर्शन कार्य का स्वर्णिम नियम: धीमी लाइन लगभग कभी वह नहीं होती जिस पर आपको शक है।

BASH
python -m timeit "sum(x * x for x in range(1000))"
python -m timeit -s "s = 'abc' * 100" "s.upper()"
python -m cProfile -s cumtime -m timeit "min(range(50))"
timeit का -s विकल्प संदर्भ को माप के बाहर तैयार करता है।

GIL (ग्लोबल इंटरप्रेटर लॉक) गारंटी देता है कि किसी प्रक्रिया के भीतर एक समय में केवल एक थ्रेड Python बाइटकोड निष्पादित करे। परिणाम: थ्रेड शुद्ध गणना को तेज़ नहीं करते। फिर भी वे I/O के लिए उत्कृष्ट बने रहते हैं, क्योंकि नेटवर्क या डिस्क पठन के दौरान GIL छोड़ दिया जाता है, और NumPy जैसी कई C लाइब्रेरियों द्वारा भी। गणना से कई कोर संतृप्त करने के लिए, multiprocessing या concurrent.futures.ProcessPoolExecutor से होकर जाएँ। Python 3.13 बिना GIL वाला एक प्रायोगिक "free-threaded" संस्करण देता है, जो प्रोडक्शन में अब भी दुर्लभ है।

PYTHON
import sys

squares_list = [x * x for x in range(1_000_000)]
squares_gen = (x * x for x in range(1_000_000))

print(sys.getsizeof(squares_list), "bytes")
print(sys.getsizeof(squares_gen), "bytes")
print(sum(squares_gen))
लिस्ट के लिए लगभग 8 MB, जनरेटर के लिए लगभग 200 बाइट।

कोई जनरेटर अनुक्रम की लंबाई चाहे जो हो, कुछ सौ बाइट का ही होता है: यह केवल अपनी निष्पादन स्थिति संग्रहीत करता है और हर वैल्यू माँग पर उत्पन्न करता है। उन्हें पाइपलाइन में जोड़ें - sum(x * x for x in data) - ताकि बिना कोई मध्यवर्ती लिस्ट बनाए धाराओं पर काम हो सके। एक समझौता जिसे जानना ज़रूरी है: कोई जनरेटर एक ही बार इस्तेमाल होता है। एक बार समाप्त हो जाने पर, वह चुपचाप कुछ और उत्पन्न नहीं करता - यह उन बग का क्लासिक स्रोत है जहाँ दूसरा इटरेशन "खाली" लगता है।

PYTHON
import timeit

def concat(n: int) -> str:
    out = ""
    for i in range(n):
        out += str(i)
    return out

def join(n: int) -> str:
    return "".join(str(i) for i in range(n))

t1 = timeit.timeit(lambda: concat(20_000), number=10)
t2 = timeit.timeit(lambda: join(20_000), number=10)
print(f"concat: {t1:.3f} s / join: {t2:.3f} s")
अपनी मशीन पर मापें: join पोर्टेबल और सुरक्षित विकल्प बना रहता है।

ज्ञान जांच

सुनिश्चित करें कि आपको इस पाठ के मुख्य बिंदु याद हैं।

  1. GIL ठीक-ठीक किसे रोकता है?
    • Python में हर तरह की समानांतरता, multiprocessing सहित
    • एक ही प्रक्रिया के कई थ्रेड द्वारा Python बाइटकोड का एक साथ निष्पादन - I/O और बहुत सा C कोड इसे छोड़ देते हैं
    • किसी एक प्रोग्राम में कई थ्रेड का उपयोग
    • एक ही वेरिएबल पर समवर्ती लेखन
  2. समकक्ष लिस्ट की तुलना में किसी जनरेटर का sys.getsizeof इतना छोटा क्यों रहता है?
    • जनरेटर कोई तत्व संग्रहीत नहीं करता: वह केवल अपनी निष्पादन स्थिति रखता है और हर वैल्यू माँग पर गणना करता है
    • getsizeof जनरेटर को माप नहीं सकता
    • जनरेटर अपना डेटा आंतरिक रूप से संपीड़ित करते हैं
  3. किसी लूप में दोहराए गए out += part को ''.join(parts) क्यों मात देता है?
    • join स्ट्रिंग जोड़ने के लिए कई थ्रेड इस्तेमाल करता है
    • यह एक मिथक है: Python 3 से प्रदर्शन एक जैसा है
    • चूँकि str अपरिवर्तनीय हैं, हर += पूरी स्ट्रिंग दोबारा कॉपी करता है (द्विघातीय लागत); join अंतिम आकार की गणना करता है और केवल एक बार आवंटन करता है
    • join जनरेटर के मध्यवर्ती str ऑब्जेक्ट बनाने से बचाता है