Python kodu neden bir işlevde daha hızlı çalışır?


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

Python'daki bu kod parçası çalışır (Not: Zamanlama Linux'ta BASH'daki zaman işleviyle yapılır.)

real    0m1.841s
user    0m1.828s
sys     0m0.012s

Ancak, for döngüsü bir işlev içine yerleştirilmezse,

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

daha uzun süre çalışır:

real    0m4.543s
user    0m4.524s
sys     0m0.012s

Bu neden?


16
Zamanlamayı gerçekte nasıl yaptınız?
Andrew Jaffe

53
Sadece bir sezgi, doğru olup olmadığından emin değilim: Sanırım kapsamları yüzünden. İşlev durumunda, yeni bir kapsam oluşturulur (yani, değişken adlarına değerlerine bağlı bir tür karma). Bir işlev olmadan, değişkenler küresel kapsamdadır, çok şey bulabileceğinizde, döngüyü yavaşlatır.
Scharron

4
@Scharron Öyle görünmüyor. Çalışma süresini gözle görülür biçimde etkilemeden 200k kukla değişkenleri kapsam içine tanımladı.
Deestan

2
Alex Martelli bu ilgilendiren bir iyi bir cevap yazdı stackoverflow.com/a/1813167/174728
John La Rooy

53
@Scharron yarı haklısın. Kapsamlarla ilgilidir, ancak yerellerde daha hızlı olmasının nedeni, yerel kapsamların aslında sözlükler yerine diziler olarak uygulanmasıdır (çünkü boyutları derleme zamanında bilinir).
Katriel

Yanıtlar:


532

Yerel değişkenleri depolamanın neden küresellerden daha hızlı olduğunu sorabilirsiniz . Bu bir CPython uygulama detayıdır.

CPython'un yorumlayıcının çalıştığı bayt koduna derlendiğini unutmayın. Bir fonksiyon derlenince, lokal değişkenler (sabit boyutlu bir dizide olmayan bir dict) ve değişken isimleri dizinler atanır. Bu, bir işleve dinamik olarak yerel değişkenler ekleyemediğiniz için mümkündür. Daha sonra yerel bir değişken almak, kelimenin tam anlamıyla listeye bir işaretçi araması ve üzerinde PyObjectönemsiz olan bir yeniden sayım artışıdır .

Bunu LOAD_GLOBAL, dictkarma ve benzeri gerçek bir arama olan global bir aramayla ( ) karşılaştırın. Bu nedenle, bu nedenle global iglobal olmasını isteyip istemediğinizi belirtmeniz gerekir : bir kapsam içindeki bir değişkene atadığınızda, derlemez, STORE_FASTsiz söylemezseniz erişim için s gönderir .

Bu arada, küresel aramalar hala oldukça optimize edilmiştir. Özellik aramaları foo.barvardır gerçekten yavaş olanlar!

Yerel değişken verimlilik üzerine küçük bir örnek .


6
Bu aynı zamanda geçerli versiyona kadar PyPy için de geçerlidir (bu yazının yazıldığı sırada 1.8). OP'nin test kodu, global kapsamda bir fonksiyonun içine kıyasla yaklaşık dört kat daha yavaş çalışır.
GDorn

4
@Walkerneo Geri gitmedikçe öyle değiller. Katrielalex ve ecatmur'un söylediklerine göre, küresel değişken aramaları, depolama yöntemi nedeniyle yerel değişken aramalarından daha yavaştır.
Jeremy Pridemore

2
@Walkerneo Burada devam eden birincil görüşme, bir işlev içindeki yerel değişken aramaları ile modül düzeyinde tanımlanan genel değişken aramaları arasındaki karşılaştırmadır. Orijinal yorumunuzda bu yanıtı fark ederseniz, "Global değişken aramalarının yerel değişken özellik aramalarından daha hızlı olduğunu düşünmezdim" dediniz. ve değiller. katrielalex, yerel değişken aramalarının küresel olanlardan daha hızlı olmasına rağmen, küresel olanların bile özellik aramalarından (farklı olan) oldukça optimize ve daha hızlı olduğunu söyledi. Daha fazla bilgi için bu yorumda yeterli yerim yok.
Jeremy Pridemore

3
@Walkerneo foo.bar yerel bir erişim değildir. Bir nesnenin niteliğidir. (Biçimlendirme eksikliğini affet) def foo_func: x = 5, xbir işlev için yereldir. Erişim xyereldir. foo = SomeClass(), foo.baröznitelik erişimidir. val = 5küresel küreseldir. Burada okuduğum şeye göre speed local> global> özelliğine gelince. Yani erişimde xiçinde foo_funcen hızlı, takip eder val, ardından foo.bar. foo.attryerel bir arama değil, çünkü bu konvo bağlamında, yerel aramaların bir işleve ait bir değişkenin araması olması hakkında konuşuyoruz.
Jeremy Pridemore

3
@ thedoctar bu globals()işleve bir göz atın . Bundan daha fazla bilgi istiyorsanız, Python için kaynak koduna bakmaya başlamanız gerekebilir. Ve CPython sadece Python'un olağan uygulaması için bir isim - bu yüzden muhtemelen zaten kullanıyorsunuz!
Katriel

661

Bir işlevin içinde bayt kodu:

  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        

En üst düzeyde, bayt kodu:

  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        

Fark, STORE_FASTdaha hızlı (!) Olmasıdır STORE_NAME. Bunun nedeni, bir işlevde, iyerel bir değerdir ancak en üst düzeyde bir küresel olmasıdır.

Bayt kodunu incelemek için dismodülü kullanın . Fonksiyonu doğrudan sökebildim, ancak üst düzey kodu sökmek için compileyerleşik'i kullanmak zorunda kaldım .


171
Deneme ile onaylandı. Takma global iiçine mainfonksiyonu çalıştıran kez eşdeğer hale getirir.
Deestan

44
Bu, soruyu cevaplamadan soruyu cevaplar :) Yerel işlev değişkenleri söz konusu olduğunda, CPython bunları bir sözlük istenene kadar (örn . locals(), inspect.getframe()Vb. Yoluyla) bir demet halinde (C kodundan değiştirilebilir) saklar . Bir dizi öğesini sabit bir tamsayı ile aramak, bir diksiyon aramaktan çok daha hızlıdır.
dmw

3
C / C ++ ile de aynıdır, küresel değişkenleri kullanmak önemli yavaşlamaya neden olur
codejammer 0

3
Bu bytecode'u ilk gördüm .. Birisine nasıl bakar ve bilmek önemlidir?
Zack

4
@gkimsey Kabul ediyorum. Sadece iki şeyi paylaşmak istedim i) Bu davranış diğer programlama dillerinde belirtilmiştir ii) Nedensel ajan gerçek anlamda dilin kendisi değil, mimari taraftır
codejammer

41

Yerel / global değişken mağaza sürelerinin yanı sıra, opcode tahmini işlevi daha hızlı hale getirir.

Diğer cevapların açıkladığı gibi, fonksiyon STORE_FASTdöngüdeki opcode'u kullanır . İşte işlevin döngüsü için bayt kodu:

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

Normalde bir program çalıştırıldığında, Python her opcode'u birbiri ardına yürütür, bir yığının kaydını tutar ve her opcode yürütüldükten sonra yığın karesinde diğer kontrolleri önceden yapar. Opcode tahmini, bazı durumlarda Python'un bir sonraki opcode'a doğrudan atlayabileceği ve böylece bu ek yükün bir kısmından kaçınabileceği anlamına gelir.

Bu durumda, Python her FOR_ITERdöngüde (döngünün üst kısmı) her gördüğünde STORE_FAST, yürütmesi gereken bir sonraki opcode olan "tahmini" olur. Python daha sonra bir sonraki opcode'a göz atar ve tahmin doğruysa doğrudan doğruya atlar STORE_FAST. Bunun, iki opodu tek bir opcode olarak sıkma etkisi vardır.

Öte yandan, STORE_NAMEopcode döngüde global düzeyde kullanılır. Python, bu opcode'u gördüğünde benzer tahminlerde bulunmaz * . Bunun yerine, döngünün yürütüldüğü hız için belirgin etkileri olan değerlendirme döngüsünün en üstüne geri dönmelidir.

Bu optimizasyon hakkında daha fazla teknik ayrıntı vermek için, ceval.cdosyadan bir alıntı (Python'un sanal makinesinin "motoru"):

Bazı opodlar çiftler halinde gelir, böylece ilk kod çalıştırıldığında ikinci kodu tahmin etmeyi mümkün kılar. Örneğin GET_ITER, genellikle onu takip eder FOR_ITER. Ve FOR_ITERgenellikleSTORE_FAST veya ile devam eder UNPACK_SEQUENCE.

Tahmini doğrulamak, bir kayıt değişkeninin bir sabite karşı tek bir yüksek hızlı testine mal olur. Eşleştirme iyiyse, işlemcinin kendi dahili şube tahmininin yüksek bir başarı olasılığı vardır ve bu da bir sonraki opcode'a neredeyse sıfır ek yüke geçişle sonuçlanır. Başarılı bir tahmin, öngörülemeyen iki dalı olan HAS_ARGtest ve anahtar durumu da dahil olmak üzere eval-loop üzerinden bir yolculuk kaydeder . İşlemcinin dahili şube tahmini ile birleştiğinde, başarılı bir şekilde PREDICT, iki opcodun, gövdeleri birleştirilmiş yeni bir tek opcodemuş gibi çalıştırması etkisi vardır.

FOR_ITEROpcode'un kaynak kodunda tam olarak tahminin STORE_FASTyapıldığı yeri görebiliriz:

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                                     

PREDICTFonksiyon için genişler if (*next_instr == op) goto PRED_##opbiz sadece tahmin işlem kodu başlangıcına atlamak yani. Bu durumda, buraya atlıyoruz:

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;

Yerel değişken ayarlanmıştır ve bir sonraki opcode yürütülmeye hazırdır. Python her seferinde başarılı bir tahminde bulunarak sonuna kadar tekrarlanabilir.

Python wiki sayfası CPython sanal makine nasıl çalıştığı hakkında daha fazla bilgi vardır.


Küçük güncelleme: CPython 3.6'dan itibaren tahminlerden tasarruf biraz azalır; iki öngörülemeyen dal yerine, sadece bir tane var. Değişiklik, bayt kodundan wordcode'a geçişten kaynaklanmaktadır ; şimdi tüm "kelime kodları" bir argüman var, talimat mantıklı bir argüman almazsa sadece sıfırdan çıkar. Bu nedenle, HAS_ARGtest asla gerçekleşmez (normal derlemenin yapmadığı derleme ve çalışma zamanında düşük düzeyli izleme etkinleştirildiğinde hariç), yalnızca öngörülemeyen bir atlama bırakır.
ShadowRanger

Çoğu CPython yapısında bu tahmin edilemeyen sıçrama bile gerçekleşmez, çünkü yeni ( Python 3.1'den itibaren , 3.2'de varsayılan olarak etkinleştirilmiştir ) hesaplanmış gotos davranışı; kullanıldığında PREDICTmakro tamamen devre dışı bırakılır; bunun yerine çoğu vaka DISPATCHdoğrudan dallanan bir ile biter . Ancak, şube tahmin CPU'larda etki, PREDICTdallanma (ve tahmin) opcode başına olduğundan, başarılı şube tahmini olasılığını artıran etki ile benzerdir .
ShadowRanger
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.