Erken dönüş neden diğerlerinden daha yavaş?


180

Bu, birkaç gün önce verdiğim cevabı takip eden bir soru . Düzenleme: Görünüşe göre bu sorunun OP zaten aynı soruyu sormak için ona gönderdiğim kodu kullandı , ama ben farkında değildi. Özür. Verilen cevaplar farklı!

Önemli olarak şunu gözlemledim:

>>> def without_else(param=False):
...     if param:
...         return 1
...     return 0
>>> def with_else(param=False):
...     if param:
...         return 1
...     else:
...         return 0
>>> from timeit import Timer as T
>>> T(lambda : without_else()).repeat()
[0.3011460304260254, 0.2866089344024658, 0.2871549129486084]
>>> T(lambda : with_else()).repeat()
[0.27536892890930176, 0.2693932056427002, 0.27011704444885254]
>>> T(lambda : without_else(True)).repeat()
[0.3383951187133789, 0.32756996154785156, 0.3279120922088623]
>>> T(lambda : with_else(True)).repeat()
[0.3305950164794922, 0.32186388969421387, 0.3209099769592285]

... ya da başka bir deyişle: koşulun tetiklenmesine elsebakılmaksızın daha hızlıdır if.

İkisi tarafından üretilen farklı bayt kodu ile ilgili olduğunu varsayıyorum, ancak herkes ayrıntılı olarak onaylayabilir / açıklayabilir mi?

DÜZENLEME: Herkes zamanlamalarımı çoğaltamaz gibi görünüyor, bu yüzden sistemim hakkında bilgi vermenin yararlı olabileceğini düşündüm. Varsayılan python yüklü olarak Ubuntu 11.10 64 bit çalıştırıyorum. pythonaşağıdaki sürüm bilgilerini oluşturur:

Python 2.7.2+ (default, Oct  4 2011, 20:06:09) 
[GCC 4.6.1] on linux2

İşte Python 2.7'deki sökme sonuçları:

>>> dis.dis(without_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  4     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
>>> dis.dis(with_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  5     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        

1
şimdi bulamıyorum SO üzerinde aynı bir soru vardı. Oluşturulan bayt kodunu kontrol ettiler ve bir adım daha vardı. Gözlenen farklılıklar test cihazına (makine, SO ..) çok bağımlıydı, bazen sadece çok küçük farklılıklar bulabiliyordu.
joaquin

3
3.x'te, her ikisi de ulaşılamayan bazı kodlar için aynı bayt kodu üretir ( LOAD_CONST(None); RETURN_VALUE- ancak belirtildiği gibi, asla ulaşılmaz) with_else. Ölü kodun bir işlevi daha hızlı hale getirdiğinden şüpheliyim. Birisi dis2.7 üzerinde bir sağlayabilir ?

4
Bunu yeniden üretemedim. Fonksiyonlar elseve Falsehepsinden yavaştı (152ns). İkinci en hızlı Trueolmadan else(143ns) ve diğer ikisi temelde (137ns ve 138ns) aynıydı. Varsayılan parametreyi kullanmadım ve %timeitiPython ile ölçtüm .
rplnt

Bu zamanlamaları çoğaltamıyorum, bazen with_else daha hızlı, bazen bu without_else versiyonu, benim için oldukça benzerler gibi görünüyor ...
Cédric Julien

1
Sökme sonuçları eklendi. Ubuntu 11.10, 64 bit, stok Python 2.7 kullanıyorum - @mac ile aynı yapılandırma. Aynı zamanda with_elsegözle görülür bir şekilde daha hızlı olduğu konusunda hemfikirim .
Chris Morgan

Yanıtlar:


387

Bu saf bir tahmin ve bunun doğru olup olmadığını kontrol etmenin kolay bir yolunu bulamadım, ama senin için bir teorim var.

Kodunuzu denedim ve sonuçların aynı olsun without_else(), tekrar tekrar biraz daha yavaş with_else():

>>> T(lambda : without_else()).repeat()
[0.42015745017874906, 0.3188967452567226, 0.31984281521812363]
>>> T(lambda : with_else()).repeat()
[0.36009842032996175, 0.28962249392031936, 0.2927151355828528]
>>> T(lambda : without_else(True)).repeat()
[0.31709728471076915, 0.3172671387005721, 0.3285821242644147]
>>> T(lambda : with_else(True)).repeat()
[0.30939889008243426, 0.3035132258429485, 0.3046679117038593]

Bayt kodunun aynı olduğu düşünüldüğünde, tek fark işlevin adıdır. Özellikle zamanlama testi genel isim aramaktadır. Yeniden adlandırmayı deneyin without_else()ve fark kaybolur:

>>> def no_else(param=False):
    if param:
        return 1
    return 0

>>> T(lambda : no_else()).repeat()
[0.3359846013948413, 0.29025818923918223, 0.2921801513879245]
>>> T(lambda : no_else(True)).repeat()
[0.3810395594970828, 0.2969634408842694, 0.2960104566362247]

Benim tahminim, without_elsebaşka bir şeyle karma bir çarpışmaya sahip olması, globals()böylece küresel isim aramasının biraz daha yavaş olması.

Düzenleme : 7 veya 8 tuşlu bir sözlük muhtemelen 32 yuvaya sahiptir, bu nedenle without_elseşu şekilde bir karma çarpışması vardır __builtins__:

>>> [(k, hash(k) % 32) for k in globals().keys() ]
[('__builtins__', 8), ('with_else', 9), ('__package__', 15), ('without_else', 8), ('T', 21), ('__name__', 25), ('no_else', 28), ('__doc__', 29)]

Hashenin nasıl çalıştığını netleştirmek için:

__builtins__ tablo boyutunu (32) azaltan modü -1196389688 hashleri, tablonun # 8 yuvasında saklandığı anlamına gelir.

without_else505588136 hash'i azaltıldı, bu da modulo 32'nin 8 olduğu bir çarpışma var. Bu Python hesaplamak için hesaplar:

İle başlayan:

j = hash % 32
perturb = hash

Boş bir alan bulana kadar bunu tekrarlayın:

j = (5*j) + 1 + perturb;
perturb >>= 5;
use j % 2**i as the next table index;

bir sonraki endeks olarak kullanılmasını 17 verir. Neyse ki bu ücretsizdir, bu yüzden döngü sadece bir kez tekrarlanır. Karma tablo büyüklüğü bu nedenle, bir 2 gücü 2**i, karma tablo büyüklüğü ikarma değeri kullanılan bit sayısıdır j.

Masaya her bir prob aşağıdakilerden birini bulabilir:

  • Yuva boş, bu durumda prob durur ve değerin tabloda olmadığını biliyoruz.

  • Yuva kullanılmıyor, ancak geçmişte kullanıldı, bu durumda yukarıdaki gibi hesaplanan bir sonraki değeri deneyin.

  • Yuva dolu, ancak tabloda saklanan tam karma değeri, aradığımız anahtarın karma değeriyle aynı değil ( __builtins__vs durumunda olan budur without_else).

  • Yuva dolu ve tam olarak istediğimiz hash değerine sahip, sonra Python, aradığımız anahtar ve nesnenin aynı nesne olup olmadığını kontrol eder (bu durumda, tanımlayıcı olabilecek kısa dizeler stajyer olduğundan özdeş tanımlayıcılar aynı dizeyi kullanır).

  • Sonunda yuva dolduğunda, karma tam olarak eşleşir, ancak anahtarlar aynı nesne değildir, ancak Python bunları eşitlik için karşılaştırmayı deneyecektir. Bu nispeten yavaştır, ancak isim söz konusu olduğunda aslında gerçekleşmemelidir.


9
@Chris, dizenin uzunluğu önemli olmamalı. Bir dizeyi ilk kez hashlattığınızda, dize uzunluğuyla orantılı zaman alır, ancak daha sonra hesaplanan karma dize nesnesinde önbelleğe alınır, böylece sonraki karmalar O (1) olur.
Duncan

1
Ah tamam, önbelleklemenin farkında değildim, ama bu mantıklı
Chris Eberle

9
Büyüleyici! Sana Sherlock diyebilir miyim? ;) Her neyse, umarım soru uygun olur olmaz size bir ödül ile ek puanlar vermeyi unutmam.
Voo

4
@mac, tam olarak değil. Karma çözünürlük hakkında biraz ekleyeceğim (yorumun içine sıkacaktım ama düşündüğümden daha ilginç).
Duncan

6
@Duncan - Karma işlemini göstermek için zaman ayırdığınız için çok teşekkür ederim. Birinci sınıf cevap! :)
mac
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.