एक फेरबदल सूची की नकल बहुत धीमी क्यों है?


89

range(10**6)दस बार एक फेरबदल सूची की नकल मुझे लगभग 0.18 सेकंड लगती है: (ये पांच रन हैं)

0.175597017661
0.173731403198
0.178601711594
0.180330912952
0.180811964451

दस बार अप्रमाणित सूची की प्रतिलिपि बनाने में मुझे लगभग 0.05 सेकंड का समय लगता है:

0.058402235973
0.0505464636856
0.0509734306934
0.0526022752744
0.0513324916184

यहाँ मेरा परीक्षण कोड है:

from timeit import timeit
import random

a = range(10**6)
random.shuffle(a)    # Remove this for the second test.
a = list(a)          # Just an attempt to "normalize" the list.
for _ in range(5):
    print timeit(lambda: list(a), number=10)

मैंने भी नकल करने की कोशिश की a[:], परिणाम समान थे (यानी, बड़ी गति अंतर)

बड़ी स्पीड में अंतर क्यों? मैं प्रसिद्ध में गति के अंतर को जानता हूं और समझता हूं कि अनारक्षित सरणी की तुलना में सॉर्ट किए गए सरणी को संसाधित करना अधिक तेज़ क्यों है? उदाहरण, लेकिन यहाँ मेरे प्रसंस्करण का कोई निर्णय नहीं है। यह सिर्फ आँख बंद करके सूची के अंदर के संदर्भों की नकल कर रहा है, नहीं?

मैं विंडोज 10 पर पायथन 2.7.12 का उपयोग कर रहा हूं।

संपादित करें: पायथन 3.5.2 की कोशिश की और साथ ही अब, परिणाम लगभग समान थे (लगभग 0.17 सेकंड तक लगातार फेरबदल, लगभग 0.05 सेकंड तक लगातार अप्रकाशित)। यहाँ उस के लिए कोड है:

a = list(range(10**6))
random.shuffle(a)
a = list(a)
for _ in range(5):
    print(timeit(lambda: list(a), number=10))


5
कृपया मुझ पर चिल्लाओ मत, मैं तुम्हारी मदद करने की कोशिश कर रहा था! आदेश बदलने के बाद, मुझे 0.25परीक्षणों में से प्रत्येक के प्रत्येक पुनरावृत्ति में लगभग मिलता है । तो मेरे मंच पर, आदेश मायने रखता है।
बरक मानस

1
@vaultah धन्यवाद, लेकिन मैंने पढ़ा है कि अब और मैं असहमत हूं। जब मैंने वहां कोड देखा, तो मैंने तुरंत कैट्स के कैश हिट्स / मिसेज के बारे में सोचा, जो लेखक का निष्कर्ष भी है। लेकिन उसका कोड नंबर जोड़ता है, जो उन्हें देखने की आवश्यकता है। मेरा कोड नहीं है खदान को केवल संदर्भों की प्रतिलिपि बनाने की आवश्यकता है, उनके माध्यम से नहीं।
स्टीफन पोचमैन

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

1
@NikolayProkopyev हाँ, मैं इसके बारे में चिंतित नहीं हूं, बस कुछ और करते समय इस पर ध्यान दिया, इसे समझा नहीं सका, और उत्सुक हो गया। और मुझे खुशी है कि मैंने पूछा और अब इसका जवाब है :-)
स्टीफन पोचमैन

जवाबों:


100

दिलचस्प बिट यह है कि यह उस क्रम पर निर्भर करता है जिसमें पूर्णांक पहले बनाए जाते हैं। उदाहरण के लिए इसके shuffleसाथ यादृच्छिक क्रम बनाने के बजाय random.randint:

from timeit import timeit
import random

a = [random.randint(0, 10**6) for _ in range(10**6)]
for _ in range(5):
    print(timeit(lambda: list(a), number=10))

यह आपके list(range(10**6))(पहले और तेज़ उदाहरण) की नकल करने जितना तेज़ है ।

हालाँकि जब आप फेरबदल करते हैं - तब आपके पूर्णांक उस क्रम में नहीं होते हैं जो वे पहले बनाए गए थे, इसीलिए यह धीमा हो जाता है।

एक त्वरित अंतरिम:

  • सभी पायथन ऑब्जेक्ट्स ढेर पर हैं, इसलिए प्रत्येक ऑब्जेक्ट एक पॉइंटर है।
  • एक सूची की प्रतिलिपि बनाना एक उथला संचालन है।
  • हालाँकि पायथन संदर्भ गिनती का उपयोग करता है, इसलिए जब एक वस्तु को एक नए कंटेनर में रखा जाता है , तो यह संदर्भ संख्या Py_INCREFमेंlist_slice वृद्धि ( में ) होनी चाहिए , इसलिए पायथन को वास्तव में उस स्थान पर जाने की आवश्यकता है जहां ऑब्जेक्ट है। यह सिर्फ संदर्भ की नकल नहीं कर सकता।

इसलिए जब आप अपनी सूची की प्रतिलिपि बनाते हैं तो आपको उस सूची का प्रत्येक आइटम मिलता है और नई सूची में "जैसा है" डालते हैं। जब आपका अगला आइटम चालू के तुरंत बाद बनाया गया था तो एक अच्छा मौका (कोई गारंटी नहीं है!) है कि यह उसके बगल में सहेजा गया है।

मान लेते हैं कि जब भी आपका कंप्यूटर कैश में किसी आइटम को लोड करता है, तो वह xअगली मेमोरी मेमोरी आइटम (कैश लोकलिटी) को भी लोड करता है । फिर आपका कंप्यूटर x+1उसी कैश पर आइटम के लिए संदर्भ गणना वृद्धि कर सकता है !

फेरबदल अनुक्रम के साथ यह अभी भी अगले-इन-मेमोरी आइटम को लोड करता है, लेकिन ये अगले-इन-सूची नहीं हैं। तो यह "वास्तव में" अगले आइटम की तलाश के बिना संदर्भ-गणना वृद्धि नहीं कर सकता है।

TL; DR: वास्तविक गति इस बात पर निर्भर करती है कि कॉपी से पहले क्या हुआ था: ये आइटम किस क्रम में बनाए गए थे और ये किस क्रम में सूची में हैं।


आप इसे देख सकते हैं id:

CPython कार्यान्वयन विवरण: यह मेमोरी में ऑब्जेक्ट का पता है।

a = list(range(10**6, 10**6+100))
for item in a:
    print(id(item))

बस एक छोटा सा अंश दिखाने के लिए:

1496489995888
1496489995920  # +32
1496489995952  # +32
1496489995984  # +32
1496489996016  # +32
1496489996048  # +32
1496489996080  # +32
1496489996112
1496489996144
1496489996176
1496489996208
1496489996240
1496507297840
1496507297872
1496507297904
1496507297936
1496507297968
1496507298000
1496507298032
1496507298064
1496507298096
1496507298128
1496507298160
1496507298192

तो ये वस्तुएं वास्तव में "ढेर पर एक दूसरे के बगल में" हैं। उनके साथ shuffleनहीं हैं:

import random
a = list(range(10**6, 100+10**6))
random.shuffle(a)
last = None
for item in a:
    if last is not None:
        print('diff', id(item) - id(last))
    last = item

यह दर्शाता है कि ये वास्तव में स्मृति में एक दूसरे के बगल में नहीं हैं:

diff 736
diff -64
diff -17291008
diff -128
diff 288
diff -224
diff 17292032
diff -1312
diff 1088
diff -17292384
diff 17291072
diff 608
diff -17290848
diff 17289856
diff 928
diff -672
diff 864
diff -17290816
diff -128
diff -96
diff 17291552
diff -192
diff 96
diff -17291904
diff 17291680
diff -1152
diff 896
diff -17290528
diff 17290816
diff -992
diff 448

महत्वपूर्ण लेख:

मैंने यह खुद नहीं सोचा है। अधिकांश रिकी स्टीवर्ट के ब्लॉगपोस्ट में पाया जा सकता है ।

यह उत्तर पायथन के "आधिकारिक" CPython कार्यान्वयन पर आधारित है। अन्य कार्यान्वयनों में विवरण (Jython, PyPy, IronPython, ...) अलग हो सकते हैं। धन्यवाद @ JörgWMittag इसे इंगित करने के लिए


6
@augurar एक संदर्भ की प्रतिलिपि बनाने का तात्पर्य उस संदर्भ काउंटर को बढ़ाना है जो वस्तु में है (इस प्रकार वस्तु की पहुंच अपरिहार्य है)
लियोन

1
@StefanPochmann प्रतिलिपि बनाने वाला फ़ंक्शन है list_sliceऔर पंक्ति 453 में आप उस Py_INCREF(v);कॉल को देख सकते हैं जिसे ढेर-आवंटित ऑब्जेक्ट तक पहुंचने की आवश्यकता है।
MSeifert

1
@MSeifert एक और अच्छा प्रयोग कर रहा है a = [0] * 10**7(10 ** 6 से ऊपर क्योंकि वह बहुत अस्थिर था), जो कि उपयोग करने से भी तेज है a = range(10**7)(लगभग 1.25 के कारक से)। जाहिर है क्योंकि यह कैशिंग के लिए बेहतर है।
स्टीफन पोचमैन

1
मैं बस सोच रहा था कि मुझे 64 बिट कंप्यूटर पर अजगर 64 बिट के साथ 32 बिट पूर्णांक क्यों मिला। लेकिन वास्तव में यह कैशिंग के लिए अच्छा है :-) यहां तक [0,1,2,3]*((10**6) // 4)कि जितनी तेजी से है a = [0] * 10**6। हालाँकि 0-255 से पूर्णांक के साथ एक और तथ्य सामने आ रहा है: ये निर्माण के क्रम के साथ जुड़े हुए हैं (इसलिए आपकी स्क्रिप्ट के अंदर) अब महत्वपूर्ण नहीं है - क्योंकि जब आप अजगर शुरू करते हैं, तो वे बनाए जाते हैं।
MSeifert

2
ध्यान दें कि वर्तमान में मौजूद चार उत्पादन-तैयार पायथन कार्यान्वयन, केवल एक ही संदर्भ गिनती का उपयोग करते हैं। तो, यह विश्लेषण वास्तव में केवल एक ही कार्यान्वयन पर लागू होता है।
जोर्ग डब्ल्यू मित्तग

24

जब आप सूची आइटम को फेरबदल करते हैं, तो उनके पास संदर्भ की बदतर स्थिति होती है, जिससे कैश प्रदर्शन खराब हो जाता है।

आप सोच सकते हैं कि सूची को कॉपी करना सिर्फ संदर्भों को कॉपी करता है, वस्तुओं को नहीं, इसलिए ढेर पर उनके स्थान मायने नहीं रखते। हालाँकि, प्रतिलिपि में अभी भी प्रत्येक ऑब्जेक्ट तक पहुँचने के लिए रिफंड को संशोधित करना शामिल है।


यह मेरे लिए एक बेहतर जवाब हो सकता है (कम से कम अगर इसमें MSeifert की तरह "प्रमाण" का लिंक होता तो) क्योंकि यह सब मुझे याद आ रहा था और यह बहुत रसीला है, लेकिन मुझे लगता है कि मैं MSeifert के साथ चिपक जाऊंगा क्योंकि मुझे लगता है कि यह हो सकता है दूसरों के लिए बेहतर है। इस के रूप में अच्छी तरह से, धन्यवाद।
स्टेफन पोचमैन

यह भी जोड़ देगा कि पता पैटर्न का पता लगाने के लिए पेंटियोइड्स, एथलम्स आदि उनमें रहस्यमय तर्क हैं, और जब वे एक पैटर्न देखते हैं तो डेटा को प्रीफ़ेट करना शुरू कर देंगे। इस मामले में, जब संख्याएँ क्रम में होती हैं, तो डेटा को प्रीफ़ैच करने (कैश मिस को कम करने) के लिए किक किया जा सकता है। यह प्रभाव, निश्चित रूप से, इलाके से हिट के बढ़े हुए% तक है।
greggo

5

दूसरों के द्वारा समझाया, यह सिर्फ संदर्भ को कॉपी नहीं कर रहा है, लेकिन यह भी वस्तुओं के अंदर संदर्भ में गिना जाता है बढ़ जाती है और इस प्रकार की वस्तुओं रहे हैं पहुँचा और कैश एक भूमिका निभाता है।

यहां मैं केवल और प्रयोग जोड़ना चाहता हूं। उतने घिसे-पिटे बनाम अछूते नहीं हैं (जहां एक तत्व तक पहुंचने से कैश की कमी हो सकती है लेकिन निम्नलिखित तत्वों को कैश में प्राप्त करें ताकि वे हिट हो जाएं)। लेकिन तत्वों को दोहराने के बारे में, जहां बाद में उसी तत्व की पहुंच कैश से टकरा सकती है क्योंकि तत्व अभी भी कैश में है।

एक सामान्य श्रेणी का परीक्षण:

>>> from timeit import timeit
>>> a = range(10**7)
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[5.1915339142808925, 5.1436351868889645, 5.18055115701749]

समान आकार की सूची लेकिन बार-बार दोहराए जाने वाले केवल एक तत्व के साथ तेजी से होता है क्योंकि यह हर समय कैश को हिट करता है:

>>> a = [0] * 10**7
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[4.125743135926939, 4.128927210087596, 4.0941229388550795]

और इससे कोई फर्क नहीं पड़ता कि यह किस संख्या में है:

>>> a = [1234567] * 10**7
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[4.124106479141709, 4.156590225249886, 4.219242600790949]

दिलचस्प है, यह और भी तेज हो जाता है जब मैं इसके बजाय एक ही दो या चार तत्वों को दोहराता हूं:

>>> a = [0, 1] * (10**7 / 2)
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[3.130586101607932, 3.1001001764957294, 3.1318465707127814]

>>> a = [0, 1, 2, 3] * (10**7 / 4)
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[3.096105435911994, 3.127148431279352, 3.132872673690855]

मुझे लगता है कि कुछ एक ही काउंटर हर समय वृद्धि की तरह नहीं है। शायद कुछ पाइपलाइन स्टाल क्योंकि प्रत्येक वृद्धि को पिछली वृद्धि के परिणाम के लिए इंतजार करना पड़ता है, लेकिन यह एक जंगली अनुमान है।

वैसे भी, दोहराया तत्वों की भी बड़ी संख्या के लिए यह कोशिश कर रहा है:

from timeit import timeit
for e in range(26):
    n = 2**e
    a = range(n) * (2**25 / n)
    times = [timeit(lambda: list(a), number=20) for _ in range(3)]
    print '%8d ' % n, '  '.join('%.3f' % t for t in times), ' => ', sum(times) / 3

आउटपुट (पहला कॉलम विभिन्न तत्वों की संख्या है, प्रत्येक के लिए मैं तीन बार परीक्षण करता हूं और फिर औसत लेता हूं):

       1  2.871  2.828  2.835  =>  2.84446732686
       2  2.144  2.097  2.157  =>  2.13275338734
       4  2.129  2.297  2.247  =>  2.22436720645
       8  2.151  2.174  2.170  =>  2.16477771575
      16  2.164  2.159  2.167  =>  2.16328197911
      32  2.102  2.117  2.154  =>  2.12437970598
      64  2.145  2.133  2.126  =>  2.13462250728
     128  2.135  2.122  2.137  =>  2.13145065221
     256  2.136  2.124  2.140  =>  2.13336283943
     512  2.140  2.188  2.179  =>  2.1688431668
    1024  2.162  2.158  2.167  =>  2.16208440826
    2048  2.207  2.176  2.213  =>  2.19829998424
    4096  2.180  2.196  2.202  =>  2.19291917834
    8192  2.173  2.215  2.188  =>  2.19207065277
   16384  2.258  2.232  2.249  =>  2.24609975704
   32768  2.262  2.251  2.274  =>  2.26239771771
   65536  2.298  2.264  2.246  =>  2.26917420394
  131072  2.285  2.266  2.313  =>  2.28767871168
  262144  2.351  2.333  2.366  =>  2.35030805124
  524288  2.932  2.816  2.834  =>  2.86047313113
 1048576  3.312  3.343  3.326  =>  3.32721167007
 2097152  3.461  3.451  3.547  =>  3.48622758473
 4194304  3.479  3.503  3.547  =>  3.50964316455
 8388608  3.733  3.496  3.532  =>  3.58716466865
16777216  3.583  3.522  3.569  =>  3.55790996695
33554432  3.550  3.556  3.512  =>  3.53952594744

तो एक एकल (बार-बार) तत्व के बारे में 2.8 सेकंड से यह 2, 4, 8, 16 के लिए लगभग 2.2 सेकंड तक चला जाता है ... विभिन्न तत्व और सौ हजारों तक लगभग 2.2 सेकंड तक रहता है। मुझे लगता है कि यह मेरा L2 कैश (4 × 256 KB, मेरे पास i7-6700 है ) का उपयोग करता है।

फिर कुछ चरणों में, समय 3.5 सेकंड तक चला जाता है। मुझे लगता है कि यह मेरे L2 कैश और मेरे L3 कैश (8 MB) के मिश्रण का उपयोग करता है, जब तक कि यह "समाप्त" भी नहीं हो जाता।

अंत में यह लगभग 3.5 सेकंड पर रहता है, मुझे लगता है क्योंकि मेरे कैश अब दोहराया तत्वों के साथ मदद नहीं करते हैं।


0

फेरबदल से पहले, जब ढेर में आवंटित किया जाता है, तो आसन्न सूचकांक ऑब्जेक्ट्स मेमोरी में आसन्न होते हैं, और एक्सेस किए जाने पर मेमोरी हिट दर अधिक होती है; फेरबदल के बाद, नई सूची के आसन्न सूचकांक की वस्तु स्मृति में नहीं है। बगल में, हिट दर बहुत खराब है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.