"X <y <z", "x <y ve y <z" den daha hızlı mı?


129

Gönderen bu sayfayı , bunu biliyoruz:

Zincirleme karşılaştırmalar, andoperatörü kullanmaktan daha hızlıdır . x < y < zBunun yerine yazın x < y and y < z.

Ancak, aşağıdaki kod parçacıklarını test eden farklı bir sonuç aldım:

$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y < z"
1000000 loops, best of 3: 0.322 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y and y < z"
1000000 loops, best of 3: 0.22 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y < z"
1000000 loops, best of 3: 0.279 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y and y < z"
1000000 loops, best of 3: 0.215 usec per loop

Görünüşe göre x < y and y < zdaha hızlı x < y < z. Neden?

(Gibi bu sitede bazı mesajları arama yaptıktan sonra bu bir ) O "sadece bir kere değerlendirilir" için anahtardır biliyorum x < y < zancak ben hala kafam karıştı. Daha fazla çalışma yapmak için, bu iki işlevi kullanarak şu iki işlevi söktüm dis.dis:

import dis

def chained_compare():
        x = 1.2
        y = 1.3
        z = 1.1
        x < y < z

def and_compare():
        x = 1.2
        y = 1.3
        z = 1.1
        x < y and y < z

dis.dis(chained_compare)
dis.dis(and_compare)

Ve çıktı:

## chained_compare ##

  4           0 LOAD_CONST               1 (1.2)
              3 STORE_FAST               0 (x)

  5           6 LOAD_CONST               2 (1.3)
              9 STORE_FAST               1 (y)

  6          12 LOAD_CONST               3 (1.1)
             15 STORE_FAST               2 (z)

  7          18 LOAD_FAST                0 (x)
             21 LOAD_FAST                1 (y)
             24 DUP_TOP
             25 ROT_THREE
             26 COMPARE_OP               0 (<)
             29 JUMP_IF_FALSE_OR_POP    41
             32 LOAD_FAST                2 (z)
             35 COMPARE_OP               0 (<)
             38 JUMP_FORWARD             2 (to 43)
        >>   41 ROT_TWO
             42 POP_TOP
        >>   43 POP_TOP
             44 LOAD_CONST               0 (None)
             47 RETURN_VALUE

## and_compare ##

 10           0 LOAD_CONST               1 (1.2)
              3 STORE_FAST               0 (x)

 11           6 LOAD_CONST               2 (1.3)
              9 STORE_FAST               1 (y)

 12          12 LOAD_CONST               3 (1.1)
             15 STORE_FAST               2 (z)

 13          18 LOAD_FAST                0 (x)
             21 LOAD_FAST                1 (y)
             24 COMPARE_OP               0 (<)
             27 JUMP_IF_FALSE_OR_POP    39
             30 LOAD_FAST                1 (y)
             33 LOAD_FAST                2 (z)
             36 COMPARE_OP               0 (<)
        >>   39 POP_TOP
             40 LOAD_CONST               0 (None)

Görünüşe göre x < y and y < zdaha az parçalanmış komutlara sahip x < y < z. Daha x < y and y < zhızlı düşünmeli miyim x < y < z?

Python 2.7.6 ile Intel (R) Xeon (R) CPU E5640 @ 2.67GHz üzerinde test edilmiştir.


8
Daha fazla çözülmüş komut, daha fazla karmaşıklık veya daha yavaş kod anlamına gelmez . Ancak timeittestlerinizi görünce bununla ilgilendim.
Marco Bonelli

6
"Bir kez değerlendirildi" den hız farkının, ysadece değişken bir arama değil, aynı zamanda bir işlev çağrısı gibi daha pahalı bir süreç olduğunda geldiğini varsaydım. Yani 10 < max(range(100)) < 15daha hızlıdır 10 < max(range(100)) and max(range(100)) < 15çünkü max(range(100))her iki karşılaştırma için bir kez çağrılır.
zehnpaard

2
@MarcoBonelli Bu yapar demonte kodu 1) bu noktada mainloop yükü önemli hale geldiği için her bir bayt kodu, çok çok hızlı) döngüler içeren ve 2 vermediğinde.
Bakuriu

2
Dal tahmini, testlerinizi bozabilir.
Corey Ogburn

2
@zehnpaard Sana katılıyorum. "Y" basit bir değerden daha fazlası olduğunda (örneğin, bir işlev çağrısı veya hesaplama) "y" nin x <y <z'de bir kez değerlendirilmesinin çok daha belirgin bir etkiye sahip olmasını beklerdim. Sunulan durumda, hata çubuklarının içindeyiz: (başarısız) dal tahmininin etkileri, optimal olmayan bayt kodu ve diğer platforma / CPU'ya özgü efektler baskındır. Bu, bir kez değerlendirmenin daha iyi olduğu (daha okunabilir olduğu) genel kuralı geçersiz kılmaz, ancak bunun uç noktalarda çok fazla değer katmayabileceğini gösterir.
MartyMacGyver

Yanıtlar:


111

Aradaki fark, x < y < z ygirişin yalnızca bir kez değerlendirilmesidir. Bu, y'nin bir değişken olması durumunda büyük bir fark yaratmaz, ancak hesaplaması biraz zaman alan bir işlev çağrısı olduğunda yapar.

from time import sleep
def y():
    sleep(.2)
    return 1.3
%timeit 1.2 < y() < 1.8
10 loops, best of 3: 203 ms per loop
%timeit 1.2 < y() and y() < 1.8
1 loops, best of 3: 405 ms per loop

18
Elbette anlamsal bir farklılık da olabilir. Yalnızca y () iki farklı değer döndürmekle kalmaz, aynı zamanda bir değişkenle, x <y'deki küçüktür operatörünün değerlendirilmesi y'nin değerini değiştirebilir. Bu nedenle, genellikle bayt kodunda optimize edilmez (örneğin, y yerel olmayan bir değişkense veya bir kapanışa katılıyorsa)
Rastgele832

Sadece merak ediyorum, neden sleep()fonksiyonun içine ihtiyacınız oldu ?
Prof

@Prof Bu, sonucu hesaplaması biraz zaman alan bir işlevi simüle etmektir. Fonksiyonlar hemen geri dönerse, iki timeit sonucu arasında çok fazla fark olmayacaktır.
Rob

@Rob Neden pek bir fark olmasın? 3ms vs 205ms olurdu, bu onu yeterince iyi gösteriyor değil mi?
Prof

@Prof y () 'nin iki kez hesaplandığı noktayı kaçırıyorsunuz, yani 1x200ms yerine 2x200ms. Kalan (3/5 ms), zamanlama ölçümünde önemsiz gürültüdür.
Rob

22

Tanımladığınız her iki işlev için en uygun bayt kodu

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

çünkü karşılaştırmanın sonucu kullanılmaz. Karşılaştırmanın sonucunu geri döndürerek durumu daha ilginç hale getirelim. Ayrıca sonucun derleme sırasında bilinemeyeceğini de varsayalım.

def interesting_compare(y):
    x = 1.1
    z = 1.3
    return x < y < z  # or: x < y and y < z

Yine, karşılaştırmanın iki versiyonu anlamsal olarak aynıdır, bu nedenle en uygun bayt kodu her iki yapı için de aynıdır. Yapabileceğim en iyi şekilde, şöyle görünecektir. Her satıra, her işlem kodundan önce ve sonra yığın içeriği ekledim, Dördüncü gösterimde (yığının üst kısmı sağda, --önce ve sonra bölünür, sonda ?orada olabilecek veya olmayabilecek bir şeyi belirtir). O Not RETURN_VALUEdeğeri döndürülür altında olur atar herşeyi yığın bırakılamayacak.

          0 LOAD_FAST                0 (y)    ;          -- y
          3 DUP_TOP                           ; y        -- y y
          4 LOAD_CONST               0 (1.1)  ; y y      -- y y 1.1
          7 COMPARE_OP               4 (>)    ; y y 1.1  -- y pred
         10 JUMP_IF_FALSE_OR_POP     19       ; y pred   -- y
         13 LOAD_CONST               1 (1.3)  ; y        -- y 1.3
         16 COMPARE_OP               0 (<)    ; y 1.3    -- pred
     >>  19 RETURN_VALUE                      ; y? pred  --

Dilin bir uygulaması, CPython, PyPy, ne olursa olsun, her iki varyasyon için bu bayt kodunu (veya kendi eşdeğer işlem sırasını) üretmezse, bu , bayt kodu derleyicisinin kalitesizliğini gösterir . Yukarıya gönderdiğiniz bayt kodu dizilerinden hareket etmek çözülmüş bir sorundur (bence bu durum için tek ihtiyacınız olan sürekli katlama , ölü kodun ortadan kaldırılması ve yığın içeriğinin daha iyi modellenmesidir; ortak alt ifade eliminasyonu da ucuz ve değerli olacaktır. ) ve bunu modern bir dil uygulamasında yapmamak için hiçbir mazeret yok.

Şimdi, dilin tüm güncel uygulamalarının düşük kaliteli bayt kodu derleyicilerine sahip olduğu görülüyor. Ancak kodlama yaparken bunu göz ardı etmelisiniz ! Bayt kodu derleyicisinin iyi olduğunu varsayın ve en okunabilir kodu yazın. Muhtemelen yeterince hızlı olacak. Değilse, önce algoritmik geliştirmeleri arayın ve ikinci olarak Cython'a bir şans verin - bu, aynı çaba için uygulayabileceğiniz herhangi bir ifade seviyesi ayarından çok daha fazla iyileştirme sağlayacaktır.


Tüm optimizasyonların en önemlisi ilk etapta mümkün olmalıdır: satır içi. Ve bu, bir işlevin uygulanmasını dinamik olarak değiştirmeye izin veren dinamik diller için "çözülmüş bir problem" olmaktan çok uzaktır (yine de yapılabilir - HotSpot benzer şeyler yapabilir ve Graal gibi şeyler, bu tür optimizasyonları Python ve diğer dinamik diller için kullanılabilir hale getirmek için çalışır. ). Ve işlevin kendisi farklı modüllerden çağrılabileceğinden (veya bir çağrı dinamik olarak oluşturulabilir!) Bu optimizasyonları orada gerçekten yapamazsınız.
Voo

1
@Voo El için optimize edilmiş bayt kodum, keyfi dinamizmin varlığında bile orijinaliyle tamamen aynı anlamlara sahiptir (bir istisna: a <b ≡ b> a varsayılır). Ayrıca, satır içi yapmak abartılıyor. Eğer çok fazla yaparsanız - ve çok fazla yapmak çok kolaysa - I-önbelleği patlatır ve kazandığınız her şeyi ve sonra bazılarını kaybedersiniz.
zwol

Haklısın interesting_compare, üstteki basit bayt koduna göre optimize etmek istediğini kastettiğini düşünmüştüm (bu sadece satır içi ile çalışır). Tamamen konu dışı ama: Inlining, herhangi bir derleyici için en temel optimizasyonlardan biridir. HotSpot ile bazı karşılaştırmaları gerçek programlarda çalıştırmayı deneyebilirsiniz (zamanının% 99'unu elle optimize edilmiş bir sıcak döngüde harcayan bazı matematik testleri değil [ve bu nedenle artık daha fazla işlev çağrısı yoktur]) devre dışı bıraktığınızda herhangi bir şeyi satır içi yapma yeteneği - büyük gerilemeler göreceksiniz.
Voo

@Voo Evet, en üstteki basit bayt kodunun OP'nin orijinal kodunun "en uygun sürümü" olması gerekiyordu, değil interesting_compare.
zwol

"bir istisna: a <b ≡ b> a varsayılır" → ki bu Python'da doğru değildir. Artı, bence CPython, yçok sayıda hata ayıklama aracına sahip olduğu için, yığın üzerinde işlemlerin gerçekten değişmediğini varsayamaz .
Veedrac

8

Çıktıdaki fark, optimizasyon eksikliğinden kaynaklanıyor gibi göründüğünden, çoğu durumda bu farkı göz ardı etmeniz gerektiğini düşünüyorum - farkın ortadan kalkması olabilir. Aradaki fark, yyalnızca bir kez değerlendirilmesi gerektiğidir ve bu, fazladan gerektiren yığında çoğaltılarak çözülür POP_TOP- yine de kullanılacak çözüm LOAD_FASTmümkün olabilir.

Ancak önemli fark x<y and y<z, ikincinin doğru yolarak değerlendirilirse iki kez x<ydeğerlendirilmesi gerektiğidir, bu, değerlendirilmesi yönemli ölçüde zaman alırsa veya yan etkilere sahipse bunun sonuçları vardır .

Çoğu senaryoda x<y<z, biraz daha yavaş olmasına rağmen kullanmalısınız .


6

İki farklı yapılar çünkü şeyden önce, senin karşılaştırılması hemen hemen anlamsız değil o dayalı diğer yerine birini kullanıp kullanmayacağına karar olmamalı bu yüzden, bir performans artışı sağlamak için tanıtıldı.

x < y < zyapısı:

  1. Daha açık ve anlamıyla daha doğrudandır.
  2. Evalute: Its semantik Eğer karşılaştırma "matematiksel anlamı" dan beklediğiniz olduğunu x, yve z bir kez ve tüm koşul tutan olmadığını kontrol edin. Kullanımı birden çok kez anddeğerlendirerek semantiği değiştiriry , bu da sonucu değiştirebilir .

Öyleyse, istediğiniz anlam bilgisine ve eşdeğer olmaları durumunda birinin diğerinden daha okunaklı olup olmadığına bağlı olarak birini diğerinin yerine seçin.

Bu şunları söyledi: daha demonte kodu mu vermez yavaş kod ima. Bununla birlikte, daha fazla bayt kodu işlemi yürütmek, her işlemin daha basit olduğu ve yine de ana döngünün yinelemesini gerektirdiği anlamına gelir. O Bu araçlar ise , yapmakta olduğunuz işlemleri (örneğin yerel değişken arama yapıyorsun orada gibi) hızlı derece, o zaman havai daha baytkodu işleme aynı önem teşkil edebilir.

Ancak bu sonucun daha genel durumda geçerli olmadığını , yalnızca profilinize aldığınız "en kötü durum" için geçerli olduğunu unutmayın. Diğerlerinin de belirttiği gibi, ybiraz daha fazla zaman alan bir şeye geçerseniz, sonuçların değiştiğini görürsünüz, çünkü zincirleme notasyon bunu yalnızca bir kez değerlendirir.

Özetleme:

  • Performanstan önce semantiği düşünün.
  • Okunabilirliği hesaba katın.
  • Mikro ölçütlere güvenmeyin. Bir fonksiyonun / ifade zamanlamasının söz konusu parametrelere göre nasıl davrandığını görmek için her zaman farklı türde parametrelerle profil oluşturun ve onu nasıl kullanmayı planladığınızı düşünün.

5
Sanırım cevabınız, alıntı yapılan sayfanın, özellikle soru durumunda, kayan sayıları karşılaştırmanın basitçe yanlış olduğu gerçeğini içermiyor. Zincirleme karşılaştırma, hem ölçümlerde hem de oluşturulan bayt kodunda görüldüğü gibi daha hızlı değildir.
pvg

30
Performans etiketli bir soruyu "belki de performansı bu kadar çok düşünmemelisin" şeklinde cevaplamak bana pek faydalı görünmüyor. Soruyu soran kişinin genel programlama ilkelerini kavrayışı hakkında potansiyel olarak patronluk taslayan varsayımlar yapıyorsunuz ve ardından eldeki konu yerine çoğunlukla onlardan bahsediyorsunuz.
Ben Millwood

@Veerdac yorumu yanlış yorumluyorsunuz. OP'nin dayandığı orijinal belgede önerilen optimizasyon, minimumda dalgalanmalar durumunda yanlıştır. Daha hızlı değil.
pvg
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.