python - बिना कोड के इंटरमीडिएट वेरिएबल्स का उपयोग कोड तेजी से क्यों होता है?




python-3.x cpython (2)

मेरे परिणाम आपके जैसे ही थे: पायथॉन 3.4 में इंटरमीडिएट चर का उपयोग करने वाला कोड लगातार कम से कम 10-20% तेज था। हालाँकि जब मैंने बहुत ही अजगर 3.4 दुभाषिया पर IPython का उपयोग किया, तो मुझे ये परिणाम मिले:

In [1]: %timeit -n10000 -r20 tuple(range(2000)) == tuple(range(2000))
10000 loops, best of 20: 74.2 µs per loop

In [2]: %timeit -n10000 -r20 a = tuple(range(2000));  b = tuple(range(2000)); a==b
10000 loops, best of 20: 75.7 µs per loop

विशेष रूप से, मैं कभी भी पूर्व के लिए 74.2 to के करीब पहुंचने में कामयाब नहीं हुआ था जब मैंने कमांड लाइन से -mtimeit इस्तेमाल किया था।

इसलिए यह हाइजेनबग काफी रोचक निकला। मैंने तय किया कि कमान को strace से चलाना है और वास्तव में कुछ गड़बड़ है:

% strace -o withoutvars python3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 134 usec per loop
% strace -o withvars python3 -mtimeit "a = tuple(range(2000));  b = tuple(range(2000)); a==b"
10000 loops, best of 3: 75.8 usec per loop
% grep mmap withvars|wc -l
46
% grep mmap withoutvars|wc -l
41149

अब वह अंतर का एक अच्छा कारण है। वह कोड जो चरों का उपयोग नहीं करता है, जो mmap सिस्टम कॉल को इंटरमीडिएट चर का उपयोग करने वाले की तुलना में लगभग 1000x अधिक कहते हैं।

256k क्षेत्र के लिए munmap mmap / withoutvars से भरा है; इन समान रेखाओं को बार-बार दोहराया जाता है:

mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0

_PyObject_ArenaMmap कॉल फ़ंक्शन से आ रहा लगता है _PyObject_ArenaMmap Objects/obmalloc.c ; obmalloc.c में मैक्रो obmalloc.c भी शामिल है, जो कि #define d होना है (256 << 10) (वह 262144 ); इसी तरह से munmap _PyObject_ArenaMunmap से obmalloc.c से मेल खाता है।

obmalloc.c कहना है कि

पायथन 2.5 से पहले, एरेनास कभी भी free() एड नहीं थे। पाइथन 2.5 से शुरू होकर, हम एरेनास को free() कोशिश करते free() , और कुछ हल्की अनुमानी रणनीतियों का उपयोग करते हैं, जिससे कि अंततः एरेनास को मुक्त किया जा सकता है।

इस प्रकार ये आंकड़े और यह तथ्य कि पायथन ऑब्जेक्ट एलोकेटर इन फ्री एरेनास को जल्द से जल्द जारी करता है जैसे ही वे python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))' लिए खाली हो जाते हैं python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))' पैथोलॉजिकल व्यवहार को ट्रिगर करते हैं जहां एक 256 kiB मेमोरी क्षेत्र को फिर से आवंटित और बार-बार जारी किया जाता है; और यह आवंटन mmap / munmap साथ होता है, जो तुलनात्मक रूप से महंगा है क्योंकि वे सिस्टम कॉल करते हैं - इसके अलावा, MAP_ANONYMOUS साथ mmap को नए मैप किए गए पृष्ठों को शून्य करना होगा - भले ही पायथन की देखभाल होगी।

व्यवहार उस कोड में मौजूद नहीं है जो मध्यवर्ती चर का उपयोग करता है, क्योंकि यह थोड़ा अधिक मेमोरी का उपयोग कर रहा है और कोई भी मेमोरी क्षेत्र को मुक्त नहीं किया जा सकता है क्योंकि इसमें अभी भी कुछ ऑब्जेक्ट आवंटित किए गए हैं। ऐसा इसलिए है क्योंकि timeit इसके विपरीत नहीं बल्कि एक लूप में बना देगा

for n in range(10000)
    a = tuple(range(2000))
    b = tuple(range(2000))
    a == b

अब व्यवहार यह है कि a और b दोनों तब तक बंधे रहेंगे जब तक वे पुन: असाइन नहीं हो जाते हैं, इसलिए दूसरी पुनरावृत्ति में, tuple(range(2000)) एक 3 tuple आवंटित करेगा, और असाइनमेंट a = tuple(...) पुराने टपल की संदर्भ संख्या को कम करें, जिससे इसे जारी किया जाए, और नए टपल के संदर्भ की संख्या में वृद्धि की जा सके; तब वही होता है b । इसलिए पहले पुनरावृत्ति के बाद इन ट्यूपलों में से कम से कम 2 हमेशा होते हैं, यदि 3 नहीं, तो जोर नहीं होता है।

सबसे विशेष रूप से यह गारंटी नहीं दी जा सकती है कि मध्यवर्ती चर का उपयोग करने वाला कोड हमेशा तेज होता है - वास्तव में कुछ सेटअपों में यह हो सकता है कि मध्यवर्ती चर का उपयोग करने पर अतिरिक्त mmap कॉल आएंगे, जबकि रिटर्न मानों की तुलना करने वाला कोड सीधे ठीक हो सकता है।

किसी ने पूछा कि ऐसा क्यों होता है, जब समय कचरा संग्रहण को निष्क्रिय कर देता है। यह वास्तव में सच है कि समय ऐसा करता है :

ध्यान दें

डिफ़ॉल्ट रूप से, टाइमिंग timeit() समय के दौरान अस्थायी रूप से कचरा संग्रह बंद कर देता है। इस दृष्टिकोण का लाभ यह है कि यह स्वतंत्र समय को अधिक तुलनीय बनाता है। यह नुकसान यह है कि जीसी मापी जा रही फ़ंक्शन के प्रदर्शन का एक महत्वपूर्ण घटक हो सकता है। यदि हां, तो जीसी को सेटअप स्ट्रिंग में पहले स्टेटमेंट के रूप में फिर से सक्षम किया जा सकता है। उदाहरण के लिए:

हालांकि, अजगर का कचरा कलेक्टर केवल चक्रीय कचरा , अर्थात् उन वस्तुओं के संग्रह को पुनः प्राप्त करने के लिए है जिनके संदर्भ चक्र हैं। यहाँ ऐसा नहीं है; इसके बजाय इन वस्तुओं को तुरंत मुक्त कर दिया जाता है जब संदर्भ गणना शून्य तक गिर जाती है।

मैंने इस अजीब व्यवहार का सामना किया है और इसे समझाने में असफल रहा। ये बेंचमार्क हैं:

py -3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 97.7 usec per loop
py -3 -m timeit "a = tuple(range(2000));  b = tuple(range(2000)); a==b"
10000 loops, best of 3: 70.7 usec per loop

27% से अधिक अस्थायी चर के साथ एक लाइनर का उपयोग करने की तुलना में चर असाइनमेंट की तुलना कैसे तेजी से होती है?

पायथन डॉक्स द्वारा, समय के दौरान कचरा संग्रह को निष्क्रिय कर दिया जाता है, इसलिए ऐसा नहीं हो सकता। क्या यह किसी प्रकार का अनुकूलन है?

परिणाम कुछ हद तक कम हालांकि पायथन 2.x में पुन: पेश किया जा सकता है।

विंडोज 7, सीपीथॉन 3.5.1, इंटेल i7 3.40 गीगाहर्ट्ज, 64 बिट ओएस और पायथन दोनों चल रहा है। एक अलग मशीन की तरह लगता है जिसे मैंने इंटेल i7 3.60 गीगाहर्ट्ज पर पायथन 3.5.0 के साथ चलाने की कोशिश की है, परिणाम को पुन: पेश नहीं करता है।

timeit.timeit() @ 10000 लूप के साथ क्रमशः क्रमशः 0.703 और 0.804 उत्पादन के साथ एक ही पायथन प्रक्रिया का उपयोग करके चल रहा है। फिर भी कुछ हद तक कम होता है। (~ 12.5%)


यहाँ पहला सवाल यह है कि क्या यह पुनरुत्पन्न है? हम में से कुछ के लिए कम से कम यह निश्चित रूप से है, हालांकि अन्य लोग कहते हैं कि वे प्रभाव नहीं देख रहे हैं। फेडोरा पर यह समानता परीक्षण के साथ बदल गया is क्योंकि वास्तव में एक तुलना परिणाम के लिए अप्रासंगिक लगती है, और सीमा 200,000 तक बढ़ जाती है जो प्रभाव को अधिकतम करने के लिए लगता है:

$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7.03 msec per loop
$ python3 -m timeit "a = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 9.99 msec per loop
$ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.1 msec per loop
$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7 msec per loop
$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7.02 msec per loop

मैं ध्यान देता हूं कि रन के बीच भिन्नताएं, और जिस क्रम में भाव चलते हैं, परिणाम के लिए बहुत कम अंतर होता है।

धीमी संस्करण में a और b में असाइनमेंट जोड़ना इसे गति नहीं देता है। वास्तव में जैसा कि हम उम्मीद कर सकते हैं कि स्थानीय चर को असाइन करने का नगण्य प्रभाव पड़ता है। केवल एक चीज जो इसे गति देती है वह अभिव्यक्ति को पूरी तरह से दो में विभाजित करती है। अंतर केवल इतना होना चाहिए कि यह अभिव्यक्ति का मूल्यांकन करते समय पायथन द्वारा उपयोग की जाने वाली अधिकतम स्टैक गहराई को कम कर देता है (4 से 3 तक)।

यह हमें सुराग देता है कि प्रभाव स्टैक गहराई से संबंधित है, शायद अतिरिक्त स्तर स्टैक को किसी अन्य मेमोरी पेज में धकेल देता है। यदि ऐसा है तो हमें यह देखना चाहिए कि स्टैक को प्रभावित करने वाले अन्य परिवर्तन करने से बदलाव आएगा (सबसे अधिक संभावना प्रभाव को मार देगा), और वास्तव में यही वह है जो हम देखते हैं:

$ python3 -m timeit -s "def foo():
   tuple(range(200000)) is tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
   tuple(range(200000)) is tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
   a = tuple(range(200000));  b = tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 9.97 msec per loop
$ python3 -m timeit -s "def foo():
   a = tuple(range(200000));  b = tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 10 msec per loop

इसलिए, मुझे लगता है कि प्रभाव पूरी तरह से समय प्रक्रिया के दौरान पायथन स्टैक का उपभोग करने के कारण है। हालांकि यह अभी भी अजीब है।





python-internals