[] Neden list () 'den daha hızlı?


707

Geçenlerde bir işlem hızlarını karşılaştırıldığında []ve list()o keşfetmeye şaşırdı []çalışır hızlı üç defadan fazla daha list(). Ben aynı testi yaptı {}ve dict(): ve sonuçlar hemen hemen aynıydı []ve {}süre, hem etrafında 0.128sec / milyon döngüleri aldı list()ve dict()kabaca 0.428sec / milyon döngüleri her sürdü.

Bu neden? Do []ve {}(ve muhtemelen ()ve ''çok) onların açıkça adlandırılmış muadilleri (ederken hemen bazı boş stok sabitin bir kopyasını geri pas list(), dict(), tuple(), str()) tam aslında unsurları olsa da olmasa da, bir nesne oluşturma hakkında gitmek?

Bu iki yöntemin nasıl farklı olduğu hakkında hiçbir fikrim yok ama öğrenmek isterim. Dokümanlarda veya SO'da bir cevap bulamadım ve boş parantez aramanın beklediğimden daha sorunlu olduğu ortaya çıktı.

Ben arayarak Zamanlamam sonuçları geldi timeit.timeit("[]")ve timeit.timeit("list()")ve timeit.timeit("{}")ve timeit.timeit("dict()")sırasıyla listeleri ve sözlükleri karşılaştırmak. Python 2.7.9 kullanıyorum.

Geçenlerde keşfetti " Neden doğru değerini daha yavaş ise 1? " Performansını karşılaştırır if Trueiçin if 1ve üzerinde dokunma gibi görünüyor benzer edebi-versus-küresel senaryo; belki de dikkate değer.


2
Not: ()ve ''sadece boş olmadıkları için değişmezler ve bu nedenle onları tekil yapmak kolay bir kazançtır; onlar bile sadece boş için singleton yüklemek, yeni nesneler oluşturmak değil tuple/ ' str. Teknik olarak bir uygulama ayrıntı, ama onlar niye anlamakta zorlanıyor olmazdı boş önbelleğe tuple/ strperformans nedenleriyle. Yani bir hisse senedi hazırlığı hakkındaki sezginiz []ve {}geri dönüşünüz yanlıştı, ama ()ve için geçerli ''.
ShadowRanger

Yanıtlar:


758

Çünkü []ve {}olan edebi sözdizimi . Python, sadece liste veya sözlük nesnelerini oluşturmak için bayt kodu oluşturabilir:

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

list()ve dict()ayrı nesnelerdir. İsimlerinin çözülmesi, argümanların itilmesi için yığının dahil edilmesi, daha sonra almak için çerçevenin saklanması ve bir çağrı yapılması gerekir. Her şey daha fazla zaman alıyor.

Boş durum için, geçerli kareyi korumak LOAD_NAMEzorunda olan en azından a (global ad alanında ve __builtin__modülde arama yapmak zorundadır ) ve ardından a'ya sahip olduğunuz anlamına gelir CALL_FUNCTION:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

İsim aramasını aşağıdakilerle ayrı ayrı zamanlayabilirsiniz timeit:

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

Zaman tutarsızlığı muhtemelen sözlük karma çarpışmasıdır. Bu süreleri bu nesneleri çağırma zamanlarından çıkarın ve sonucu değişmez değerleri kullanma zamanlarıyla karşılaştırın:

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

Bu nedenle, nesneyi çağırmak 1.00 - 0.31 - 0.30 == 0.39her 10 milyon çağrı için bir saniye daha sürer .

Genel adları yerel ad olarak diğer adlarla diğer adlara ekleyerek genel arama maliyetinden kaçınabilirsiniz (bir timeitkurulum kullanarak , bir ada bağladığınız her şey yereldir):

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

ama asla bu CALL_FUNCTIONmaliyetin üstesinden gelemezsiniz.


150

list()genel bir arama ve bir işlev çağrısı gerektirir, ancak []tek bir talimatı derler. Görmek:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None

75

Çünkü bir dizeyi bir liste nesnesine dönüştürmek listiçin bir işlev[] , yarasa dışında bir liste oluşturmak için kullanılır. Bunu deneyin (sizin için daha anlamlı olabilir):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

Süre

y = ["wham bam"]
>>> y
["wham bam"]

İçine koyduğunuz her şeyi içeren gerçek bir liste verir.


7
Bu doğrudan soruyu ele almıyor. Soru neden oldu []daha hızlı olduğu list()değil, neden ['wham bam']daha hızlı olduğunu list('wham bam').
Jeremy Visser

2
@JeremyVisser Bana çok mantıklı gelmedi çünkü []/ list()tam olarak aynı ['wham']/ list('wham')çünkü 1000/10aynı 100/1matematikte olduğu gibi aynı değişken farklılıklara sahipler . Teorik olarak alıp götürebilirsiniz wham bamve gerçek hala aynı olurdu, bu list()bir fonksiyon adını çağırarak bir şeyi dönüştürmeye çalışırken [], sadece değişkeni dönüştürecektir. İşlev çağrıları farklı evet, bu sadece sorunun bir mantıksal özeti, örneğin bir şirketin ağ haritası da bir çözüm / sorunun mantığıdır. İstediğiniz kadar oy verin.
Torxed

@JeremyVisser ise içerik üzerinde farklı işlemler yaptıklarını gösteriyor.
Baldrickk

20

Buradaki cevaplar harika, bu soruyu tamamen kapsıyor. İlgilenenler için bayt kodundan bir adım daha atacağım. Ben CPython en son repo kullanıyorum; eski sürümler bu konuda benzer davranır, ancak küçük değişiklikler olabilir.

İşte bunların her biri için yürütme bir ara aşağı var BUILD_LISTiçin []ve CALL_FUNCTIONiçin list().


BUILD_LISTtalimat:

Sadece dehşete bakmalısın:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();

Çok kıvrık, biliyorum. Bu ne kadar basit:

  • Yığındaki bağımsız değişkenlerin sayısını bildirerek yeni bir liste oluşturun PyList_New(bu temel olarak yeni bir liste nesnesinin belleğini ayırır) oparg. Noktasına doğru.
  • Hiçbir şeyin yanlış gitmediğini kontrol edin if (list==NULL).
  • Yığında bulunan PyList_SET_ITEM(bir makro) bağımsız değişkenler (bizim durumumuzda bu yürütülmez) ekleyin .

Hızlı olduğuna şaşmamalı! Yeni listeler oluşturmak için özel olarak üretilmiştir, başka bir şey değil :-)

CALL_FUNCTIONtalimat:

Kod işlemeye göz attığınızda gördüğünüz ilk şey CALL_FUNCTION:

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

Oldukça zararsız görünüyor, değil mi? Hayır, maalesef hayır, call_functionfonksiyonu hemen çağıracak basit bir adam değil, yapamaz. Bunun yerine, nesneyi yığından tutar, yığının tüm argümanlarını alır ve sonra nesnenin türüne göre değişir; bu bir:

Biz aradığınız listtürünü geçirilen argümanı call_functionolduğunu PyList_Type. CPython şimdi, çağrılabilir nesneleri adlandırmak için genel bir işlev çağırmak zorunda _PyObject_FastCallKeywords, yay daha fazla işlev çağrısı.

Bu işlev yine belirli işlev türleri için bazı kontroller yapar (nedenini anlayamıyorum) ve sonra gerekirse kwargs için bir diksiyon oluşturduktan sonra çağrı yapmaya devam eder _PyObject_FastCallDict.

_PyObject_FastCallDictsonunda bizi bir yere götürür! Gerçekleştirdikten sonra daha da çek o çekimden tp_callgelen yuvasıtype arasında typebiz olduğunu, bu yakalar, geçtiğiniz type.tp_call. Daha sonra aktarılan argümanlardan bir demet oluşturmaya devam eder _PyStack_AsTupleve sonunda bir çağrı yapılabilir !

tp_calleşleşmesi type.__call__devralınır ve sonunda liste nesnesini oluşturur. Hafızaya __new__karşılık gelen PyType_GenericNewve hafızayı tahsis eden listeleri çağırır PyType_GenericAlloc: Bu aslında PyList_Newnihayet yetiştiği kısımdır . Öncekilerin tümü, nesneleri genel bir şekilde işlemek için gereklidir.

Sonunda, mevcut argümanlarla listeyi type_callçağırır list.__init__ve başlatır, sonra geldiğimiz şekilde geri dönüyoruz. :-)

Son olarak, LOAD_NAMEburaya katkıda bulunan başka bir adam olduğunu hatırlayın .


Girişimizle uğraşırken, Python'un Cişi yapmak için uygun işlevi gerçekten bulmak için genellikle çemberlerden atlamak zorunda olduğunu görmek kolaydır . Dinamik olarak çağırdığı için bir çağrıştırma niteliği yoktur, birileri maskeleyebilir list( ve erkek birçok insan yapar ) ve başka bir yol izlenmelidir.

Burası list()çok kaybediyor: Keşfetmek Python'un ne yapması gerektiğini bulmak için yapması gerekiyor.

Öte yandan edebi sözdizimi tam olarak bir şey ifade eder; değiştirilemez ve her zaman önceden belirlenmiş bir şekilde davranır.

Dipnot: Tüm işlev adları bir sürümden diğerine değişebilir. Nokta hala duruyor ve büyük olasılıkla gelecekteki sürümlerde duracak, şeyleri yavaşlatan dinamik görünüm.


13

Neden daha []hızlı list()?

En büyük neden, Python'un list()kullanıcı tanımlı bir işlev gibi davranmasıdır , bu da başka bir şeyi diğer adlara takma listve farklı bir şey (kendi alt sınıf listenizi veya belki de bir deque kullanmak gibi) yaparak araya girebileceğiniz anlamına gelir .

Hemen bir yerleşik listenin yeni bir örneğini oluşturur [].

Açıklamam size bunun için sezgiyi vermeye çalışıyor.

açıklama

[] genel olarak değişmez sözdizimi olarak bilinir.

Gramerde buna "liste ekranı" denir. Dokümanlardan :

Liste ekranı, köşeli parantez içine alınmış muhtemelen boş bir ifade dizisidir:

list_display ::=  "[" [starred_list | comprehension] "]"

Liste ekranı yeni bir liste nesnesi verir; içindekiler bir ifade listesi veya kavrama ile belirtilir. Virgülle ayrılmış bir ifade listesi sağlandığında, öğeleri soldan sağa değerlendirilir ve bu sırada liste nesnesine yerleştirilir. Anlama sağlandığında, liste kavrama sonucu ortaya çıkan unsurlardan oluşturulur.

Kısacası, bu yerleşik türde bir nesnenin listyaratıldığı anlamına gelir .

Bunu atlatmak yok - yani Python bunu olabildiğince çabuk yapabilir.

Öte yandan, yerleşik liste yapıcısını kullanarak list()bir yerleşik oluşturmaktan kesilebilir list.

Örneğin, listelerimizin gürültülü bir şekilde oluşturulmasını istediğimizi düşünelim:

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

Daha sonra listmodül düzeyinde global kapsamdaki adı arayabiliriz ve sonra bir oluşturduğumuzda list, aslında alt tür listemizi yaratırız:

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

Benzer şekilde, onu global ad alanından kaldırabiliriz

del list

ve yerleşik ad alanına yerleştirin:

import builtins
builtins.list = List

Ve şimdi:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

Liste ekranının koşulsuz olarak bir liste oluşturduğunu unutmayın:

>>> list_1 = []
>>> type(list_1)
<class 'list'>

Muhtemelen bunu sadece geçici olarak yapıyoruz, bu yüzden değişikliklerimizi geri alalım - önce yeni Listnesneyi yerleşiklerden kaldırın :

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined

Oh, hayır, orijinalin izini kaybettik.

Endişelenmeyin, yine de alabiliriz list- bu bir liste değişmezinin türüdür:

>>> builtins.list = type([])
>>> list()
[]

Yani...

Neden daha []hızlı list()?

Gördüğümüz gibi - üzerine yazabiliriz list- ancak gerçek türün yaratılmasına müdahale edemeyiz. Kullandığımızda list, orada bir şey olup olmadığını görmek için arama yapmalıyız.

O zaman aradığımız her çağrılabilir şeyi aramalıyız. Dilbilgisinden:

Bir çağrı, muhtemelen boş bir argüman dizisiyle çağrılabilir bir nesneyi (örn. Bir işlev) çağırır:

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"

Sadece liste için değil, herhangi bir isim için de aynı şeyi yaptığını görebiliriz:

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

İçin []Python baytkodu düzeyinde hiçbir işlev çağrı var:

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

Sadece bayt kodu düzeyinde arama veya arama yapmadan listeyi oluşturmaya devam eder.

Sonuç

Biz gösterdik listkapsam kuralları kullanarak kullanıcı kodu ile müdahale edebilecekleri ve bu list()daha sonra çağrılabilir için görünüyor ve onu çağırır.

Oysa []bir liste ekranı ya da bir değişmezdir ve bu nedenle ad arama ve işlev çağrısından kaçınır.


2
Kaçırdığınızı listve python derleyicisinin gerçekten boş bir liste döndürüp döndüremeyeceğinden emin olmak için +1 işareti.
Beefster
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.