पायथन कोड एक फ़ंक्शन में तेज़ी से क्यों चलता है?


832
def main():
    for i in xrange(10**8):
        pass
main()

पायथन में कोड का यह टुकड़ा (नोट: लिनक्स में BASH में समय समारोह के साथ किया जाता है)।

real    0m1.841s
user    0m1.828s
sys     0m0.012s

हालाँकि, यदि लूप को किसी फ़ंक्शन के भीतर नहीं रखा गया है,

for i in xrange(10**8):
    pass

तब यह बहुत लंबे समय तक चलता है:

real    0m4.543s
user    0m4.524s
sys     0m0.012s

ऐसा क्यों है?


16
आपने वास्तव में टाइमिंग कैसे की?
एंड्रयू जफ

53
बस एक अंतर्ज्ञान, सुनिश्चित नहीं है कि क्या यह सच है: मुझे लगता है कि यह स्कोप्स के कारण होगा। फ़ंक्शन के मामले में, एक नया स्कोप बनाया जाता है (यानी उनके मूल्य के लिए चर नाम के साथ एक हैश)। एक फ़ंक्शन के बिना, वैरिएबल वैश्विक दायरे में हैं, जब आप बहुत सारे सामान पा सकते हैं, इसलिए लूप को धीमा कर रहा है।
श्हर्रॉन

4
@Scharron ऐसा प्रतीत नहीं होता। चल रहे समय को प्रभावित करने वाले विज़ुअली को प्रभावित किए बिना 200k डमी वैरिएबल को दायरे में परिभाषित करें।
दीस्तान

2
एलेक्स मार्टेली ने इस stackoverflow.com/a/1813167/174728 के
John La Rooy

53
@Scharron आप आधे सही हैं। यह स्कोप के बारे में है, लेकिन स्थानीय लोगों में इसका कारण यह है कि स्थानीय स्कोप वास्तव में शब्दकोशों के बजाय सरणियों के रूप में लागू किए जाते हैं (चूंकि उनका आकार संकलन-समय पर जाना जाता है)।
कातिल

जवाबों:


531

आप पूछ सकते हैं कि ग्लोबल्स की तुलना में स्थानीय वेरिएबल्स को स्टोर करना क्यों तेज़ है। यह एक CPython कार्यान्वयन विवरण है।

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

इसके विपरीत एक वैश्विक खोज ( LOAD_GLOBAL), जो एक सच्ची dictखोज हैश और इतने पर शामिल है। संयोग से, यही कारण है कि आपको global iयह निर्दिष्ट करने की आवश्यकता है कि क्या आप इसे वैश्विक होना चाहते हैं: यदि आप कभी भी किसी दायरे के भीतर एक चर को असाइन करते हैं, तो संकलक STORE_FASTइसकी पहुंच के लिए एस जारी करेगा जब तक कि आप इसे नहीं बताते हैं।

वैसे, वैश्विक लुकअप अभी भी बहुत अनुकूलित हैं। गुण लुकअप foo.barहैं वास्तव में धीमी गति से लोगों को!

यहाँ स्थानीय चर दक्षता पर छोटा चित्रण किया गया है।


6
यह PyPy पर भी लागू होता है, वर्तमान संस्करण तक (इस लेखन के समय 1.8)। ओपी का परीक्षण कोड एक फ़ंक्शन के अंदर की तुलना में वैश्विक दायरे में लगभग चार गुना धीमा है।
GDorn

4
@Walkerneo वे नहीं हैं, जब तक कि आपने इसे पीछे की ओर नहीं कहा। Katrielalex और ecatmur जो कह रहे हैं, उसके अनुसार, वैश्विक चर लुकअप भंडारण की विधि के कारण स्थानीय चर लुकअप की तुलना में धीमी है।
जेरेमी प्रिडेमोर

2
@Walkerneo यहां चल रही प्राथमिक बातचीत एक फ़ंक्शन और वैश्विक चर लुकअप के बीच स्थानीय चर लुकअप के बीच तुलना है जो मॉड्यूल स्तर पर परिभाषित किए गए हैं। यदि आप इस उत्तर के लिए अपने मूल टिप्पणी जवाब में देखते हैं, तो आपने कहा था "मुझे नहीं लगता था कि वैश्विक वैरिएबल लुकअप स्थानीय वैरिएबल प्रॉपर्टी लुक्स से तेज थे।" और वे नहीं हैं। katrielalex ने कहा कि, हालाँकि स्थानीय वैरिएबल लुकअप वैश्विक लोगों की तुलना में अधिक तेज़ होते हैं, यहाँ तक कि वैश्विक भी फीचर्स फीचर्स (जो अलग-अलग होते हैं) की तुलना में अधिक अनुकूलित और तेज़ होते हैं। इस टिप्पणी के लिए मेरे पास पर्याप्त जगह नहीं है।
जेरेमी प्रेडेमोर

3
@Walkerneo foo.bar एक स्थानीय पहुंच नहीं है। यह किसी वस्तु का एक गुण है। (स्वरूपण की कमी को क्षमा करें) def foo_func: x = 5, xएक फ़ंक्शन के लिए स्थानीय है। पहुंच xस्थानीय है। foo = SomeClass(), foo.barविशेषता अभिगम है। val = 5वैश्विक वैश्विक है। गति के लिए स्थानीय> वैश्विक> यहाँ मैंने जो पढ़ा है उसके अनुसार विशेषता। तो तक पहुँचने xमें foo_funcसबसे तेजी से किया जाता है, जिसके बाद val, जिसके बाद foo.barfoo.attrएक स्थानीय लुकअप नहीं है क्योंकि इस कॉनवो के संदर्भ में, हम स्थानीय लुकअप्स के बारे में बात कर रहे हैं जो एक वेरिएबल का लुक है जो एक फ़ंक्शन से संबंधित है।
जेरेमी प्रेडेमोर

3
@thedoctar फ़ंक्शन पर एक नज़र है globals()। यदि आप इससे अधिक जानकारी चाहते हैं तो आपको पायथन के स्रोत कोड को देखना शुरू करना पड़ सकता है। और सीपीथॉन सिर्फ पायथन के सामान्य कार्यान्वयन के लिए नाम है - इसलिए आप शायद पहले से ही इसका उपयोग कर रहे हैं!
१५:४५ पर कैट्रील जूल

660

एक फ़ंक्शन के अंदर, बायटेकोड है:

  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (i)

  3          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        

शीर्ष स्तर पर, बाइटकोड है:

  1           0 SETUP_LOOP              20 (to 23)
              3 LOAD_NAME                0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_NAME               1 (i)

  2          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               2 (None)
             26 RETURN_VALUE        

अंतर यह है कि STORE_FASTतुलना में तेज (!) है STORE_NAME। ऐसा इसलिए है क्योंकि एक समारोह में, iएक स्थानीय है लेकिन सबसे ऊपर है यह एक वैश्विक है।

बाइटेकोड की जांच करने के लिए, disमॉड्यूल का उपयोग करें । मैं सीधे समारोह एकत्रित न करने के लिए, लेकिन उच्चस्तरीय कोड मैं का इस्तेमाल किया था एकत्रित न करने में सक्षम था compilebuiltin


171
प्रयोग द्वारा पुष्टि की गई। फ़ंक्शन global iमें सम्मिलित mainकरना रनिंग समय को समतुल्य बनाता है।
दीस्तान

44
यह सवाल का जवाब दिए बिना सवाल का जवाब देता है :) स्थानीय फ़ंक्शन चर के मामले में, CPython वास्तव में इनको एक टपल (जो सी कोड से उत्परिवर्तित होता है) में तब तक संग्रहीत करता है जब तक कि किसी शब्दकोश का अनुरोध नहीं किया जाता (उदाहरण के माध्यम से locals(), या inspect.getframe()आदि)। एक स्थिर पूर्णांक द्वारा एक सरणी तत्व को देखना एक तानाशाही को खोजने की तुलना में बहुत तेज है।
dmw

3
यह C / C ++ के साथ भी वैसा ही है, वैश्विक चर का उपयोग करने से महत्वपूर्ण मंदी होती है
कोडजैमर

3
यह पहली बार मैंने बाइटकोड देखा है .. कोई इसे कैसे देखता है, और यह जानना महत्वपूर्ण है?
जैक

4
@gkimsey मैं सहमत हूँ। बस दो चीजों को साझा करना चाहता था i) यह व्यवहार अन्य प्रोग्रामिंग भाषाओं में नोट किया गया है ii) कारण एजेंट अधिक वास्तु पक्ष है और सही अर्थों में भाषा नहीं है
कोडजमेर

41

स्थानीय / वैश्विक वैरिएबल स्टोर के समय के अलावा, ओपकोड भविष्यवाणी कार्य को तेज करती है।

जैसा कि अन्य उत्तर बताते हैं, फ़ंक्शन STORE_FASTलूप में ओपकोड का उपयोग करता है । यहाँ फ़ंक्शन के लूप के लिए बायटेकोड है:

    >>   13 FOR_ITER                 6 (to 22)   # get next value from iterator
         16 STORE_FAST               0 (x)       # set local variable
         19 JUMP_ABSOLUTE           13           # back to FOR_ITER

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

इस मामले में, हर बार पायथन देखता है FOR_ITER(लूप के ऊपर), यह "भविष्यवाणी" करेगा जो कि STORE_FASTअगले ओपोड को निष्पादित करना है। अजगर फिर अगले ओपकोड में झांकता है और अगर भविष्यवाणी सही थी, तो वह सीधे कूद जाता है STORE_FAST। इसमें दो opcodes को एक opcode में निचोड़ने का प्रभाव होता है।

दूसरी ओर, STORE_NAMEवैश्विक स्तर पर लूप में ओपकोड का उपयोग किया जाता है। जब यह ओपकोड देखता है तो पायथन * * नहीं करता है । इसके बजाय, यह मूल्यांकन-लूप के शीर्ष पर वापस जाना चाहिए जिसमें लूप निष्पादित होने की गति के लिए स्पष्ट निहितार्थ हैं।

इस अनुकूलन के बारे में कुछ और तकनीकी जानकारी देने के लिए, यहाँ ceval.cफ़ाइल का एक उद्धरण (पायथन की वर्चुअल मशीन का "इंजन") दिया गया है:

कुछ ऑपकोड जोड़े में आते हैं और इस प्रकार पहले कोड के चलते दूसरे कोड की भविष्यवाणी करना संभव हो जाता है। उदाहरण के लिए, GET_ITERअक्सर इसके बाद होता है FOR_ITER। तथाFOR_ITER अक्सरSTORE_FAST या उसके बाद होता हैUNPACK_SEQUENCE

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

हम FOR_ITERओपकोड के लिए स्रोत कोड में देख सकते हैं, जहां के लिए भविष्यवाणी STORE_FASTकी गई है:

case FOR_ITER:                         // the FOR_ITER opcode case
    v = TOP();
    x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
    if (x != NULL) {                     
        PUSH(x);                       // put x on top of the stack
        PREDICT(STORE_FAST);           // predict STORE_FAST will follow - success!
        PREDICT(UNPACK_SEQUENCE);      // this and everything below is skipped
        continue;
    }
    // error-checking and more code for when the iterator ends normally                                     

PREDICTकार्य करने के लिए फैलता है if (*next_instr == op) goto PRED_##opयानी हम सिर्फ भविष्यवाणी opcode के शुरू करने के लिए कूद। इस मामले में, हम यहां कूदते हैं:

PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
    v = POP();                     // pop x back off the stack
    SETLOCAL(oparg, v);            // set it as the new local variable
    goto fast_next_opcode;

स्थानीय चर अब सेट हो गया है और अगला ओपोड निष्पादन के लिए तैयार है। पायथन जब तक अंत तक नहीं पहुंचता, तब तक चलता रहता है, जब तक कि हर बार सफल भविष्यवाणी न हो जाए।

अजगर विकि पृष्ठ कैसे CPython के आभासी मशीन काम करता है के बारे में अधिक जानकारी नहीं है।


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

अप्रत्याशित कूद नहीं होता है यहां तक कि सबसे वजह से नई (CPython के बनाता है, अजगर 3.1 के रूप में , 3.2 में डिफ़ॉल्ट रूप से सक्षम अभिकलन gotos व्यवहार); जब उपयोग किया जाता है, तो PREDICTमैक्रो पूरी तरह से अक्षम है; इसके बजाय अधिकांश मामले DISPATCHसीधे उस शाखाओं में समाप्त होते हैं। लेकिन सीपीयू की भविष्यवाणी करने वाली शाखा पर, इसका प्रभाव समान है PREDICT, क्योंकि ब्रांचिंग (और भविष्यवाणी) प्रति ओपकोड है, जिससे सफल शाखा की भविष्यवाणी की संभावना बढ़ जाती है।
शैडो रेंजर
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.