python - एक समारोह में पाइथन कोड तेजी से क्यों चलता है?




performance profiling benchmarking cpython (4)

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

जैसा कि अन्य उत्तरों समझाते हैं, फ़ंक्शन लूप में 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 अगला STORE_FAST है जिसे इसे निष्पादित करना है। पाइथन फिर अगले STORE_FAST पर STORE_FAST और, अगर भविष्यवाणी सही थी, तो यह सीधे STORE_FAST तक STORE_FAST । इसका दो ऑपोडोड को एक ही ऑपोड में निचोड़ने का असर पड़ता है।

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

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

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

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

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

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;

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

पाइथन विकी पेज में सीपीथॉन की आभासी मशीन कैसे काम करती है, इस बारे में अधिक जानकारी है।

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

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

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

ऐसा क्यों है?


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

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

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

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

यहां स्थानीय परिवर्तनीय दक्षता पर छोटा illustration गया है।


एक समारोह के अंदर, बाइटकोड है

  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 एक स्थानीय i लेकिन पर्याप्त रूप से यह एक वैश्विक है।

बाइटकोड की जांच करने के लिए, डी मॉड्यूल का उपयोग करें। मैं सीधे फ़ंक्शन को अलग करने में सक्षम था, लेकिन अपूर्ण कोड को अलग करने के लिए मुझे compile निर्मित का उपयोग करना पड़ा।


source प्रयोग करें, ल्यूक!

सीपीथॉन में, range(...).__contains__ (एक विधि रैपर) अंततः एक साधारण गणना के लिए प्रतिनिधि होगा जो जांचता है कि मूल्य संभवतः सीमा में हो सकता है या नहीं। यहां गति का कारण यह है कि हम श्रेणी वस्तु के प्रत्यक्ष पुनरावृत्ति के बजाय सीमाओं के बारे में गणितीय तर्क का उपयोग कर रहे हैं। उपयोग किए गए तर्क की व्याख्या करने के लिए:

  1. जांचें कि संख्या start और stop start बीच है, और
  2. जांचें कि स्ट्रैड वैल्यू हमारे नंबर पर "कदम" नहीं है।

उदाहरण के लिए, 994 range(4, 1000, 2) क्योंकि:

  1. 4 <= 994 < 1000 , और
  2. (994 - 4) % 2 == 0

पूर्ण सी कोड नीचे शामिल किया गया है, जो स्मृति प्रबंधन और संदर्भ गिनती विवरणों के कारण थोड़ा अधिक वर्बोज़ है, लेकिन बुनियादी विचार वहां है:

static int
range_contains_long(rangeobject *r, PyObject *ob)
{
    int cmp1, cmp2, cmp3;
    PyObject *tmp1 = NULL;
    PyObject *tmp2 = NULL;
    PyObject *zero = NULL;
    int result = -1;

    zero = PyLong_FromLong(0);
    if (zero == NULL) /* MemoryError in int(0) */
        goto end;

    /* Check if the value can possibly be in the range. */

    cmp1 = PyObject_RichCompareBool(r->step, zero, Py_GT);
    if (cmp1 == -1)
        goto end;
    if (cmp1 == 1) { /* positive steps: start <= ob < stop */
        cmp2 = PyObject_RichCompareBool(r->start, ob, Py_LE);
        cmp3 = PyObject_RichCompareBool(ob, r->stop, Py_LT);
    }
    else { /* negative steps: stop < ob <= start */
        cmp2 = PyObject_RichCompareBool(ob, r->start, Py_LE);
        cmp3 = PyObject_RichCompareBool(r->stop, ob, Py_LT);
    }

    if (cmp2 == -1 || cmp3 == -1) /* TypeError */
        goto end;
    if (cmp2 == 0 || cmp3 == 0) { /* ob outside of range */
        result = 0;
        goto end;
    }

    /* Check that the stride does not invalidate ob's membership. */
    tmp1 = PyNumber_Subtract(ob, r->start);
    if (tmp1 == NULL)
        goto end;
    tmp2 = PyNumber_Remainder(tmp1, r->step);
    if (tmp2 == NULL)
        goto end;
    /* result = ((int(ob) - start) % step) == 0 */
    result = PyObject_RichCompareBool(tmp2, zero, Py_EQ);
  end:
    Py_XDECREF(tmp1);
    Py_XDECREF(tmp2);
    Py_XDECREF(zero);
    return result;
}

static int
range_contains(rangeobject *r, PyObject *ob)
{
    if (PyLong_CheckExact(ob) || PyBool_Check(ob))
        return range_contains_long(r, ob);

    return (int)_PySequence_IterSearch((PyObject*)r, ob,
                                       PY_ITERSEARCH_CONTAINS);
}

विचार में "मांस" का उल्लेख लाइन में किया गया है :

/* result = ((int(ob) - start) % step) == 0 */ 

अंतिम नोट के रूप में - कोड स्निपेट के नीचे range_contains फ़ंक्शन देखें। यदि सटीक प्रकार की जांच विफल हो जाती है तो हम वर्णित चतुर एल्गोरिदम का उपयोग नहीं करते हैं, बल्कि _PySequence_IterSearch का उपयोग करके श्रेणी की गूंगा पुनरावृत्ति खोज पर वापस _PySequence_IterSearch ! आप इस व्यवहार को दुभाषिया में देख सकते हैं (मैं यहां v3.5.0 का उपयोग कर रहा हूं):

>>> x, r = 1000000000000000, range(1000000000000001)
>>> class MyInt(int):
...     pass
... 
>>> x_ = MyInt(x)
>>> x in r  # calculates immediately :) 
True
>>> x_ in r  # iterates for ages.. :( 
^\Quit (core dumped)




python performance profiling benchmarking cpython