Karışık bir listeyi kopyalamak neden çok daha yavaş?


90

Karışık bir range(10**6)listeyi on kez kopyalamak yaklaşık 0,18 saniye sürüyor: (bunlar beş çalıştırmadır)

0.175597017661
0.173731403198
0.178601711594
0.180330912952
0.180811964451

Karıştırılmamış listeyi on kez kopyalamak yaklaşık 0,05 saniye sürüyor:

0.058402235973
0.0505464636856
0.0509734306934
0.0526022752744
0.0513324916184

İşte test kodum:

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)

İle kopyalamayı da denedim a[:], sonuçlar benzerdi (yani, büyük hız farkı)

Neden büyük hız farkı var? Ünlü dizideki hız farkını biliyorum ve anlıyorum Sıralı bir diziyi işlemek, sıralanmamış diziden neden daha hızlıdır? örnek, ancak burada benim işlememde hiçbir karar yok. Sadece listedeki referansları körü körüne kopyalıyor, değil mi?

Windows 10'da Python 2.7.12 kullanıyorum.

Düzenleme: Şimdi de Python 3.5.2'yi denedi, sonuçlar hemen hemen aynıydı (sürekli olarak 0.17 saniye civarında karıştırıldı, 0.05 saniye civarında tutarlı bir şekilde karıştırılmadı). İşte bunun kodu:

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


5
Lütfen bana bağırma, sana yardım etmeye çalışıyordum! Sırayı değiştirdikten sonra 0.25, testlerin her birinin yaklaşık her yinelemesinde alıyorum . Yani benim platformumda sıra önemli.
barak manos

1
@vaultah Teşekkürler, ama şimdi okudum ve katılmıyorum. Kodu orada gördüğümde, anında önbellek isabetlerini / özledikleri şeyleri düşündüm ki bu aynı zamanda yazarın da sonucudur. Ancak kodu , onlara bakmayı gerektiren sayıları ekler . Benim kodum değil. Benimki sadece referansları kopyalamalı, bunlar üzerinden erişmemeli.
Stefan Pochmann

2
Bir bağlantıda @vaultah tarafından verilen tam bir cevap var (şu anda biraz katılmıyorsunuz, görüyorum). Ama yine de düşük seviyeli özellikler için python kullanmamamız gerektiğini ve bu yüzden endişelenmemiz gerektiğini düşünüyorum. Ama yine de bu konu ilginç, teşekkürler.
Nikolay Prokopyev

1
@NikolayProkopyev Evet, endişelenmiyorum, bunu başka bir şey yaparken fark ettim, açıklayamadım ve merak ettim. Şimdi sorduğuma ve bir cevabım olduğuna sevindim :-)
Stefan Pochmann

Yanıtlar:


100

İlginç olan, tam sayıların ilk oluşturulduğu sıraya bağlı olmasıdır . Örneğin shuffle, rastgele bir sıra oluşturmak yerine 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))

Bu, kopyalamak kadar hızlıdır list(range(10**6))(ilk ve hızlı örnek).

Ancak karıştırdığınızda - o zaman tam sayılarınız artık ilk oluşturuldukları sırada değildir, bu onu yavaşlatır.

Hızlı bir intermezzo:

  • Tüm Python nesneleri öbek üzerindedir, bu nedenle her nesne bir göstericidir.
  • Bir listeyi kopyalamak sığ bir işlemdir.
  • Bununla birlikte, Python referans sayma kullanır, bu nedenle bir nesne yeni bir kaba konduğunda referans sayısının artırılması ( Py_INCREFinçlist_slice ) gerekir , bu nedenle Python'un gerçekten nesnenin olduğu yere gitmesi gerekir. Sadece referansı kopyalayamaz.

Yani listenizi kopyaladığınızda, bu listedeki her öğeyi alır ve yeni listeye "olduğu gibi" koyarsınız. Bir sonraki öğeniz mevcut öğeden kısa bir süre sonra oluşturulduğunda, yığının yanına kaydedilmesi için iyi bir şans (garanti yok!) Vardır.

Bilgisayarınız önbelleğe bir öğe yüklediğinde, xsonraki bellek içindeki öğeleri de (önbellek konumu) yüklediğini varsayalım . Daha sonra bilgisayarınız x+1, aynı önbellekteki öğeler için referans sayısı artışını gerçekleştirebilir !

Karıştırılmış sırayla, yine de bir sonraki bellekteki öğeleri yükler, ancak bunlar bir sonraki listede olanlar değildir. Dolayısıyla, bir sonraki öğeyi "gerçekten" aramadan referans sayısı artışını gerçekleştiremez.

TL; DR: Gerçek hız, kopyalamadan önce ne olduğuna bağlıdır: bu öğeler hangi sırayla ve bunlar listede hangi sırayla oluşturuldu.


Bunu aşağıdakilere bakarak doğrulayabilirsiniz id:

CPython uygulama ayrıntısı: Bu, bellekteki nesnenin adresidir.

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

Sadece kısa bir alıntı göstermek için:

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

Yani bu nesneler gerçekten "yığın üzerinde yan yana". İle shuffledeğiller:

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

Bu da bunların hafızada yan yana olmadığını gösteriyor:

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

Önemli Not:

Bunu kendim düşünmedim. Bilgilerin çoğu Ricky Stewart'ın blog yayınında bulunabilir .

Bu cevap Python'un "resmi" CPython uygulamasına dayanmaktadır. Diğer uygulamalardaki ayrıntılar (Jython, PyPy, IronPython, ...) farklı olabilir. Teşekkürler @ JörgWMittag bunu işaret ettiğiniz için .


6
@augurar Bir referansın kopyalanması, nesnede bulunan referans sayacının artırılması anlamına gelir (bu nedenle nesne erişimi kaçınılmazdır)
Leon

1
@StefanPochmann Kopyalamayı yapan fonksiyon list_sliceve 453. satırda Py_INCREF(v);yığın tahsisli nesneye erişmesi gereken çağrıyı görebilirsiniz .
MSeifert

1
@MSeifert Başka bir iyi deney, kullanmaktan a = [0] * 10**7bile daha hızlı olan a = range(10**7)( 10 ** 6'dan itibaren) kullanmaktır (yaklaşık 1.25 kat). Açıkçası, çünkü bu önbelleğe almak için daha da iyi.
Stefan Pochmann

1
Python 64bit ile 64bit bir bilgisayarda neden 32bit tamsayılarım olduğunu merak ediyordum. Ama aslında bu önbelleğe almak için de iyidir :-) Hatta [0,1,2,3]*((10**6) // 4)kadar hızlıdır a = [0] * 10**6. Bununla birlikte, 0-255 arası tam sayılarla gelen başka bir gerçek daha var: Bunlar, bunlarla birlikte yaratılma sırası (betiğinizin içinde) artık önemli değil - çünkü python'a başladığınızda oluşturulurlar.
MSeifert

2
Şu anda mevcut dört üretime hazır Python uygulamasından yalnızca birinin referans sayımı kullandığını unutmayın. Dolayısıyla, bu analiz gerçekten yalnızca tek bir uygulama için geçerlidir.
Jörg W Mittag

24

Liste öğelerini karıştırdığınızda, referans bölgeleri daha kötüdür ve bu da daha kötü önbellek performansına yol açar.

Listeyi kopyalamanın nesneleri değil, yalnızca referansları kopyaladığını düşünebilirsiniz, bu nedenle yığın üzerindeki konumlarının önemi olmamalıdır. Bununla birlikte, kopyalama yine de refcount'u değiştirmek için her nesneye erişmeyi içerir.


Bu benim için daha iyi bir cevap olabilir (en azından MSeifert'inki gibi "kanıtlama" ile bir bağlantısı olsaydı) çünkü eksik olduğum tek şey buydu ve çok kısa ama sanırım MSeifert'inkine bağlı kalacağım diğerleri için daha iyi. Yine de buna olumlu oy verdi, teşekkürler.
Stefan Pochmann

Ayrıca pentioidlerin, atletlerin vb. Adres modellerini tespit etmek için mistik mantığa sahip olduklarını ve bir model gördüklerinde verileri önceden getirmeye başlayacaklarını da ekleyecektir. Bu durumda, sayılar sıralı olduğunda verileri önceden getirmek (önbellek kayıplarını azaltmak) için devreye girebilir. Bu etki, elbette, yerelden gelen isabetlerin artan yüzdesine ilavedir.
greggo

5

Başkaları tarafından açıklandığı gibi, bu sadece referanslar kopyalayarak değil, aynı zamanda nesnelerin içindeki başvuru sayıları artar ve böylece nesneler değil edilir erişilen ve önbellek bir rol oynar.

Burada sadece daha fazla deney eklemek istiyorum. Karıştırılmış ve karıştırılmamış (burada bir öğeye erişmek önbelleği kaçırabilir, ancak aşağıdaki öğeleri önbelleğe alarak vurulmaları için). Ancak, aynı öğeye daha sonraki erişimlerin, öğe hala önbellekte olduğu için önbelleğe çarpabileceği yinelenen öğeler hakkında.

Normal bir aralığı test etmek:

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

Aynı boyutta ancak tek bir öğenin defalarca tekrarlandığı bir liste daha hızlıdır çünkü önbelleğe her zaman ulaşır:

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

Ve kaç numara olduğu önemli değil:

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

İlginç bir şekilde, aynı iki veya dört öğeyi tekrarladığımda daha da hızlanıyor:

>>> 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]

Sanırım aynı tek sayaçtan hoşlanmayan bir şey her zaman arttı. Belki bir miktar boru hattı durur çünkü her artış bir önceki artışın sonucunu beklemek zorundadır, ancak bu çılgın bir tahmin.

Her neyse, bunu daha fazla sayıda tekrarlanan öğe için denemek:

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

Çıktı (ilk sütun farklı öğelerin sayısıdır, her biri için üç kez test edip ortalamayı alırım):

       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

Yani tek bir (tekrarlanan) element için yaklaşık 2.8 saniyeden, 2, 4, 8, 16, ... farklı elementler için yaklaşık 2.2 saniyeye düşer ve yüzbinlere kadar yaklaşık 2.2 saniyede kalır. Bunun L2 önbelleğimi kullandığını düşünüyorum (4 × 256 KB, i7-6700'üm var ).

Daha sonra birkaç adımda süre 3,5 saniyeye çıkar. Sanırım bu, "bitene" kadar L2 önbelleğim ve L3 önbelleğimin (8 MB) bir karışımını kullanıyor.

Sonunda yaklaşık 3,5 saniyede kalıyor, çünkü önbelleklerim artık tekrarlanan öğelere yardımcı olmuyor.


0

Karıştırmadan önce, yığın içinde tahsis edildiğinde, bitişik indeks nesneleri bellekte bitişiktir ve erişildiğinde bellek isabet oranı yüksektir; karıştırmadan sonra, yeni listenin bitişik dizininin nesnesi bellekte değil. Bitişikte, isabet oranı çok düşük.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.