Java'da (a * b! = 0) neden (a! = 0 && b! = 0) daha hızlı?


412

Java, bazı noktada, program akışının iki int değişkenleri, "a" ve "b", sıfır olmayan olup olmadığı tarafından belirlendiği bazı kod yazıyorum (not: a ve b asla negatif ve asla tamsayı taşma aralığında).

İle değerlendirebilirim

if (a != 0 && b != 0) { /* Some code */ }

Veya alternatif olarak

if (a*b != 0) { /* Some code */ }

Kod parçasının çalışma başına milyonlarca kez çalışmasını beklediğimden hangisinin daha hızlı olacağını merak ediyordum. Ben büyük bir rasgele oluşturulan dizi onları karşılaştırarak deneyi yaptım ve ben de dizi (veri kesir = =) sonuçları ne kadar etkilediğini görmek için merak ettim:

long time;
final int len = 50000000;
int arbitrary = 0;
int[][] nums = new int[2][len];

for (double fraction = 0 ; fraction <= 0.9 ; fraction += 0.0078125) {
    for(int i = 0 ; i < 2 ; i++) {
        for(int j = 0 ; j < len ; j++) {
            double random = Math.random();

            if(random < fraction) nums[i][j] = 0;
            else nums[i][j] = (int) (random*15 + 1);
        }
    }

    time = System.currentTimeMillis();

    for(int i = 0 ; i < len ; i++) {
        if( /*insert nums[0][i]*nums[1][i]!=0 or nums[0][i]!=0 && nums[1][i]!=0*/ ) arbitrary++;
    }
    System.out.println(System.currentTimeMillis() - time);
}

Ve sonuçlar, "a" veya "b" nin zamanın ~% 3'ünden 0'a eşit olmasını beklerseniz, a*b != 0bundan daha hızlı olduğunu gösterir a!=0 && b!=0:

Sıfır olmayan bir AND b sonucunun grafik grafiği

Nedenini merak ediyorum. Biraz ışık tutabilen var mı? Derleyici mi yoksa donanım düzeyinde mi?

Düzenleme: Meraktan ... şimdi şube tahmini hakkında öğrendim, ben bir OR b için analog karşılaştırma göstermek ne olacağını merak ediyordum :

Sıfır olmayan a veya b grafiği

Şube tahmininin beklendiği gibi etkisini görüyoruz, ilginç bir şekilde grafik X ekseni boyunca biraz çevriliyor.

Güncelleme

1- !(a==0 || b==0)neler olduğunu görmek için analize ekledim .

2- Ben de dahildir a != 0 || b != 0, (a+b) != 0ve (a|b) != 0meraktan, dallanma tahmini öğrenmeye sonra. Ancak mantıksal olarak diğer ifadelere eşdeğer değildir, çünkü doğruya dönmek için yalnızca ORb'nin sıfırdan farklı olması gerekir, bu nedenle işlem verimliliği için karşılaştırılmaları amaçlanmaz.

3- Analiz için kullandığım gerçek ölçütü de ekledim, ki bu sadece keyfi bir int değişkeni yineliyor.

4- Bazı insanlar a != 0 & b != 0bunun aksine a != 0 && b != 0, daha yakın davranacağını a*b != 0öngörerek, şube tahmin etkisini ortadan kaldıracağımızı tahmin etmeyi önerdiler. &Boole değişkenleriyle kullanılabileceğini bilmiyordum , sadece tamsayılarla ikili işlemler için kullanıldığını düşündüm.

Not: Tüm bunları düşündüğüm bağlamda, int taşması bir sorun değil, ama genel bağlamlarda kesinlikle önemli bir husustur.

İşlemci: Intel Core i7-3610QM @ 2.3GHz

Java sürümü: 1.8.0_45
Java (TM) SE Çalışma Zamanı Ortamı (derleme 1.8.0_45-b14)
Java HotSpot (TM) 64 Bit Sunucu VM (derleme 25.45-b02, karışık mod)


11
Ne olmuş if (!(a == 0 || b == 0))? Mikrobenç işaretler kötü bir şekilde güvenilmezdir, bunun gerçekten ölçülebilir olması olası değildir (~% 3 bana bir hata payı gibi geliyor).
Elliott Frisch

9
Veya a != 0 & b != 0.
Louis Wasserman

16
Öngörülen dal yanlışsa dallanma yavaştır. a*b!=0bir şubesi daha az
Erwin Bolwidt

19
(1<<16) * (1<<16) == 0ancak her ikisi de sıfırdan farklıdır.
CodesInChaos

13
@Gene: Önerilen optimizasyonunuz geçerli değil. Hatta taşma yok sayarak, a*beğer sıfırdır biri arasında ave bsıfırdır;a|byalnızca ikisi birden olduğunda sıfırdır.
hmakholm Monica

Yanıtlar:


240

Kıyaslamanızın hatalı olabileceği sorununu görmezden geliyorum ve sonucu nominal değere taşıyorum.

Derleyici mi yoksa donanım düzeyinde mi?

Sonuncusu, sanırım:

  if (a != 0 && b != 0)

2 bellek yüküne ve iki koşullu dalı derleyecek

  if (a * b != 0)

2 bellek yüküne, çarpma ve bir koşullu dala derlenecektir.

Donanım düzeyinde dal tahmininin etkisiz olması durumunda, çarpanın ikinci koşullu daldan daha hızlı olması muhtemeldir. Oranı artırdıkça ... şube tahmini daha az etkili hale geliyor.

Koşullu dalların daha yavaş olmasının nedeni, talimat yürütme boru hattının durmasına neden olmalarıdır. Şube tahmini, şubenin hangi yöne gideceğini tahmin ederek ve buna dayalı olarak bir sonraki talimatı seçerek duraktan kaçınmakla ilgilidir. Tahmin başarısız olursa, diğer yöndeki talimatlar yüklenirken bir gecikme olur.

(Not: yukarıdaki açıklama basitleştirilmiştir. Daha doğru bir açıklama için, CPU üreticisi tarafından montaj dili kodlayıcıları ve derleyici yazarları için sağlanan literatüre bakmanız gerekir. Şube Yordayan iyi arka plan budur.)


Ancak, bu optimizasyonda dikkat etmeniz gereken bir şey var. a * b != 0Yanlış cevap verecek değerler var mı? Ürünü hesaplamanın tamsayı taşmasına neden olduğu durumları düşünün.


GÜNCELLEME

Grafikleriniz söylediklerimi doğrulama eğilimindedir.

  • Koşullu dalda da "dal tahmini" etkisi vardır a * b != 0 durumunda ve bu grafiklerde ortaya çıkar.

  • Eğrileri X ekseninde 0,9'un ötesine yansıtırsanız, 1) yaklaşık 1,0 ve 2'de buluşacak gibi görünür) buluşma noktası kabaca X = 0,0 ile aynı Y değerinde olacaktır.


GÜNCELLEME 2

Eğrilerin a + b != 0ve a | b != 0vakalar için neden farklı olduğunu anlamıyorum . Orada olabilir dallanma öngörüsü mantık akıllı bir şey. Veya başka bir şeye işaret edebilir.

(Bu tür bir şeyin belirli bir çip model numarasına veya hatta sürüme özgü olabileceğini unutmayın. Kıyaslama sonuçlarınızın sonuçları diğer sistemlerde farklı olabilir.)

Ancak, her ikisi de bütün negatif olmayan değerleri için çalışma avantajına sahiptir ave b.


1
@DebosmitRay - 1) SW'ler olmamalıdır. Ara sonuçlar bir sicilde tutulacaktır. 2) İkinci durumda, mevcut iki dal vardır: biri "bazı kod" yürütmek için, diğeri ise sonraki ifadeye atlamak için if.
Stephen C

1
Eğrileri, çünkü b | @StephenC Bir + b ve yaklaşık karıştırılmamalıdır haklısınız olan aynı, ben renkleri gerçekten yakın olmanın düşünüyorum. Kör insanları renklendirmek için özür dileriz!
Maljam

3
@ njzk2 olasılık perspektifinden bakıldığında, bu vakalar% 50'de eksene göre simetrik olmalıdır (sıfır a&bve olasılığı a|b). Onlar, ama mükemmel değil, bu bulmaca.
Antonín Lejsek

3
@StephenC Farklı a*b != 0ve a+b != 0karşılaştırmalı değerlendirmenin farklı olmasının nedeni a+b != 0, hiç eşdeğer olmaması ve asla karşılaştırılmamış olması gerektiğidir. Örneğin, ile a = 1, b = 0ilk ifade yanlış olarak, ikincisi doğru olarak değerlendirilir. Çarpma, bir ve operatörü gibi davranırken, ekleme bir veya operatörü gibi davranır .
JS1

2
@ AntonínLejsek Bence olasılıklar farklı olurdu. Eğer varsa no zaman hem olasılığı sıfır ave bsıfır artar olmak n. Bir ANDoperasyonda, nbunlardan birinin olasılığı daha yüksek birinin sıfırdan artar ve koşul karşılanır. Bu bir ORişlem için zıttır (bunlardan birinin sıfır olma olasılığı ile artar n). Bu matematiksel bir bakış açısına dayanmaktadır. Donanımın böyle çalışıp çalışmadığından emin değilim.
WYSIWYG

70

Karşılaştırmanızın bazı kusurları olduğunu düşünüyorum ve gerçek programlar hakkında çıkarım yapmak için yararlı olmayabilir. İşte düşüncelerim:

  • (a|b)!=0ve (a+b)!=0deney halinde ya da buna bir değer, sıfır olmayan bir a != 0 && b != 0ve (a*b)!=0, test hem olmayan sıfırdır. Yani sadece aritmetiğin zamanlamasını karşılaştırmıyorsunuz: eğer durum daha sık doğruysa, ifvücudun daha fazla yürütülmesine neden olur , bu da daha fazla zaman alır.

  • (a+b)!=0 sıfıra eşit olan pozitif ve negatif değerler için yanlış bir şey yapar, bu yüzden burada çalışsa bile genel durumda kullanamazsınız.

  • Benzer şekilde, (a*b)!=0taşan değerler için yanlış bir şey yapacaktır. (Rastgele örnek: 196608 * 327680 0'dır, çünkü gerçek sonuç 2 32'ye bölünebilir , bu nedenle düşük 32 biti 0'dır ve bu bitler bir işlemse elde edeceğiniz her şeydir int.)

  • VM, dış ( fraction) döngüsünün ilk birkaç çalışması sırasında fraction, 0 olduğunda, dallar neredeyse hiç alınmadığında ifadeyi optimize eder . fraction0,5'ten başlarsanız optimize edici farklı şeyler yapabilir .

  • Sanal Makine burada dizi sınır kontrollerinin bazılarını ortadan kaldıramadığı sürece, ifadede sadece sınır kontrolleri nedeniyle dört dal daha vardır ve bu, düşük bir seviyede neler olduğunu anlamaya çalışırken karmaşık bir faktördür. İki düz diziler içine iki boyutlu bir dizi bölünmüş eğer değişen, farklı sonuçlar elde edebilirsiniz nums[0][i]ve nums[1][i]hiç nums0[i]ve nums1[i].

  • CPU şube belirleyicileri, verilerdeki kısa modelleri veya alınan veya alınmayan tüm dalların çalışmalarını algılar. Rastgele oluşturulmuş kıyaslama verileriniz, bir şube öngörücüsü için en kötü senaryodur . Gerçek dünya verileri tahmin edilebilir bir desene sahipse veya sıfırdan ve sıfırdan farklı değerlerden oluşan uzun çalışmalara sahipse, dallar çok daha düşük maliyetli olabilir .

  • Koşul karşılandıktan sonra yürütülen belirli kod, koşulun kendisini değerlendirme performansını etkileyebilir, çünkü döngünün açılıp açılmayacağı, hangi CPU kayıtlarının kullanılabilir olduğu ve getirilen numsdeğerlerden herhangi birinin durumu değerlendirdikten sonra tekrar kullanılabilir. Karşı ölçütte yalnızca bir sayacı artırmak, gerçek kodun yapacağı şey için mükemmel bir yer tutucu değildir.

  • System.currentTimeMillis()çoğu sistemde +/- 10 ms'den daha doğru değildir. System.nanoTime()genellikle daha doğrudur.

Çok fazla belirsizlik vardır ve bu tür mikro optimizasyonlarla kesin bir şey söylemek her zaman zordur, çünkü bir VM veya CPU'da daha hızlı olan bir numara diğerinde daha yavaş olabilir. 64 bit sürümü yerine 32 bit HotSpot JVM çalıştırıyorsanız, bunun iki çeşide sahip olduğunu unutmayın: "İstemci" VM'si ile "Sunucu" VM'sine kıyasla farklı (daha zayıf) optimizasyonlara sahip.

VM tarafından üretilen makine kodunu parçalara ayırabiliyorsanız , ne yaptığını tahmin etmek yerine bunu yapın!


24

Buradaki cevaplar iyi, ancak bazı şeyleri iyileştirebilecek bir fikrim vardı.

İki dal ve ilişkili dal tahmini olası suçlu olduğundan, mantığı değiştirmeden dallanmayı tek bir dalda azaltabiliriz.

bool aNotZero = (nums[0][i] != 0);
bool bNotZero = (nums[1][i] != 0);
if (aNotZero && bNotZero) { /* Some code */ }

Bunu yapmak da işe yarayabilir

int a = nums[0][i];
int b = nums[1][i];
if (a != 0 && b != 0) { /* Some code */ }

Nedeni, kısa devre kurallarına göre, eğer birinci boole yanlışsa, ikincisi değerlendirilmemelidir. Yanlış nums[1][i]olup olmadığını değerlendirmekten kaçınmak için ekstra bir dal gerçekleştirmesi gerekir nums[0][i]. Şimdi, bunun nums[1][i]değerlendirilmesini umursamayabilirsiniz , ancak derleyici bunu yaptığınızda aralık dışında veya null ref atmayacağından emin olamaz. İf bloğunu basit boollere indirgeyerek derleyici, ikinci booleanın gereksiz yere değerlendirilmesinin olumsuz yan etkilere sahip olmayacağını anlayacak kadar akıllı olabilir.


3
Bu soruya tam olarak cevap vermediğini hissetmeme rağmen onaylandı
Pierre Arlaud

3
Bu, dallanmadan mantığı değiştirmeden bir dalı tanıtmanın bir yoludur (elde ettiğiniz ave byan etkileriniz varsa onları koruyacaksınız). Hala &&bir şubeniz var.
Jon Hanna

11

Bir sayı 0 olsa bile çarpımı aldığımızda ürün 0'dır. Yazarken

    (a*b != 0)

Ürünün sonucunu değerlendirir, böylece tekrarlamadan ilk birkaç tekrarını 0'dan başlayarak ortadan kaldırır. Sonuç olarak, karşılaştırmalar koşul olduğundakinden daha azdır.

   (a != 0 && b != 0)

Her öğenin 0 ile karşılaştırıldığı ve değerlendirildiği yer. Dolayısıyla gereken süre daha azdır. Ancak ikinci koşulun size daha doğru bir çözüm sağlayabileceğine inanıyorum.


4
İkinci ifadede asıfır ise , btüm ifade zaten yanlış olduğundan değerlendirilmeye gerek yoktur. Yani her element karşılaştırılır doğru değildir.
Kuba Wyrostek

9

Dalları öngörülemeyen hale getiren rasgele girdi verileri kullanıyorsunuz. Uygulamada dallar genellikle (~% 90) öngörülebilir olduğundan gerçek kodda dallı kodun daha hızlı olması muhtemeldir.

Bahsedilen. Nasıl a*b != 0daha hızlı olabileceğini göremiyorum (a|b) != 0. Genel olarak tamsayı çarpımı, bitsel VEYA'dan daha pahalıdır. Ama böyle şeyler zaman zaman garipleşiyor. Örneğin, İşlemci Önbellek Efektleri Galerisi'ndeki "Örnek 7: Donanım karmaşıklıkları" örneğine bakın .


2
&"bitwise OR" değil (bu durumda) bir "mantıksal AND" değildir, çünkü her iki işlenen de |
booleandır

1
@siegi TIL Java '&' aslında kısa devre olmadan mantıklı bir VE.
StackedCrooked
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.