Neden (değişken1% değişken2 == 0) verimsizdir?


179

Java'da yeniyim ve dün gece bazı kodlar çalıştırıyordum ve bu gerçekten beni rahatsız etti. For X döngüsünde her X çıkışını görüntülemek için basit bir program yapıyordum ve modül olarak variable % variablevs variable % 5000ya da değil olarak kullandığımda performansta büyük bir düşüş fark ettim . Birisi bana bunun neden olduğunu ve buna neden olduğunu açıklayabilir mi? Böylece daha iyi olabilirim ...

İşte "verimli" kodu (sözdizimi biraz yanlış alırsanız üzgünüm şu anda kod ile bilgisayarda değilim)

long startNum = 0;
long stopNum = 1000000000L;

for (long i = startNum; i <= stopNum; i++){
    if (i % 50000 == 0) {
        System.out.println(i);
    }
}

İşte "verimsiz kod"

long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;

for (long i = startNum; i <= stopNum; i++){
    if (i % progressCheck == 0) {
        System.out.println(i);
    }
}

Farkları ölçmek için bir tarih değişkenim olduğunu ve bir kez yeterince uzun olduğunda, birincisi 50ms, diğeri 12 saniye sürdü. Bilgisayarınız benimkinden daha verimli ise veya ne değilse , artırmanız stopNumveya azaltmanız gerekebilir progressCheck.

Bu soruyu internette aradım, ancak bir cevap bulamıyorum, belki de doğru sormuyorum.

EDIT: Sorumun çok popüler olmasını beklemiyordum, tüm cevapları takdir ediyorum. Alınan zamanın her yarısında bir kıyaslama yaptım ve verimsiz kod oldukça uzun sürdü, saniyenin 1 / 4'ü vs 10 saniyede veriliyor veya alıyor. Println kullanıyorlar, ancak her ikisi de aynı miktarda yapıyorlar, bu yüzden özellikle tutarsızlık tekrarlanabilir olduğundan, bunu çok fazla çarpıtacağını hayal etmem. Cevaplara gelince, Java'da yeni olduğum için, hangi cevabın en iyi olduğuna karar vermesine izin vereceğim. Çarşamba günü bir tane seçmeye çalışacağım.

EDIT2: Ben bu gece başka bir test yapacağım, burada modül yerine, sadece bir değişkeni arttırır ve progressCheck'e ulaştığında, bir tane gerçekleştirir ve sonra bu değişkeni 3. seçenek için 0'a sıfırlar.

EDIT3.5:

Bu kodu kullandım ve aşağıda sonuçlarımı göstereceğim .. Harika yardım için TÜM teşekkürler! Ben de uzun 0'ın kısa değerini karşılaştırmayı denedim, bu yüzden tüm yeni kontrollerimi tekrar tekrar "65536" kez gerçekleşir.

public class Main {


    public static void main(String[] args) {

        long startNum = 0;
        long stopNum = 1000000000L;
        long progressCheck = 65536;
        final long finalProgressCheck = 50000;
        long date;

        // using a fixed value
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if (i % 65536 == 0) {
                System.out.println(i);
            }
        }
        long final1 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        //using a variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                System.out.println(i);
            }
        }
        long final2 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();

        // using a final declared variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % finalProgressCheck == 0) {
                System.out.println(i);
            }
        }
        long final3 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        // using increments to determine progressCheck
        int increment = 0;
        for (long i = startNum; i <= stopNum; i++) {
            if (increment == 65536) {
                System.out.println(i);
                increment = 0;
            }
            increment++;

        }

        //using a short conversion
        long final4 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if ((short)i == 0) {
                System.out.println(i);
            }
        }
        long final5 = System.currentTimeMillis() - date;

                System.out.println(
                "\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
    }
}

Sonuçlar:

  • sabit = 874 ms (normalde 1000 ms civarında, ancak 2 gücü olması nedeniyle daha hızlı)
  • değişken = 8590 ms
  • son değişken = 1944 ms (50000 kullanılırken ~ 1000 ms)
  • artış = 1904 ms
  • Kısa Dönüşüm = 679 ms

Şaşırtıcı olmayan bir şekilde, bölünme eksikliği nedeniyle, Kısa Dönüşüm "hızlı" yoldan% 23 daha hızlıydı. Bu not etmek ilginç. Bir şeyi her 256 kez (veya orada) göstermeniz veya karşılaştırmanız gerekiyorsa, bunu yapabilir ve

if ((byte)integer == 0) {'Perform progress check code here'}

65536 (güzel bir sayı değil) ile "Nihai bildirilen Değişken" modülünde modül kullanılarak bir son FAALİYET NOTU sabit değerin yarısı kadar yavaştı (daha yavaş). Daha önce aynı hızda kıyaslama yapıyordu.


29
Aslında aynı sonucu aldım. Makinemde, ilk döngü yaklaşık 1,5 saniye ve ikinci döngü yaklaşık 9 saniye içinde çalışır. Ben eklerseniz finalönünde progressCheckdeğişkeni, hem yine aynı hızda çalışır. Bu, derleyicinin veya JIT'in progressChecksabit olduğunu bildiğinde döngüyü optimize etmeyi başardığına inanmamı sağlıyor .
marstran


24
Bir sabit ile bölme , çarpma tersi ile kolayca bir çarpmaya dönüştürülebilir . Bir değişkene bölünemez. Ve 32-bit bir bölüm,
x86'daki

2
@phuclv note 32 bit bölme burada bir sorun değildir, her iki durumda da 64 bit kalan bir işlemdir
user85421

4
@RobertCotterman, değişkeni son olarak bildirirseniz, derleyici sabit (eclipse / Java 11) kullanarak aynı bayt kodunu oluşturur ((değişken için bir bellek yuvası daha kullanmasına rağmen))
user85421

Yanıtlar:


139

OSR (yığın üzerinde değiştirme) saplamasını ölçüyorsunuz .

OSR saplaması , özellikle yöntem çalışırken yürütmeyi yorumlanmış moddan derlenmiş koda aktarmaya yönelik derlenmiş yöntemin özel bir sürümüdür.

OSR saplamaları, normal yöntemler kadar optimize edilmez, çünkü yorumlanan çerçeveyle uyumlu bir çerçeve düzenine ihtiyaç duyarlar. Bunu şu cevaplarda zaten gösterdim: 1 , 2 , 3 .

Benzer bir şey burada da olur. "Verimsiz kod" uzun bir döngü çalıştırırken, yöntem doğrudan döngü içinde yığın üzerindeki değiştirme için derlenir. Durum, yorumlanan çerçeveden OSR-derlenmiş yönteme aktarılır ve bu durum progressCheckyerel değişkeni içerir . Bu noktada JIT değişkeni sabitle değiştiremez ve bu nedenle güç azalması gibi bazı optimizasyonları uygulayamaz .

Özellikle bu, JIT'in tamsayı bölmesini çarpma ile değiştirmediği anlamına gelir . ( Bu optimizasyonlar etkinleştirilirse, değer satır içi / sabit yayılmadan sonra derleme zamanı sabiti olduğunda, vaktinden önce bir derleyiciden gelen asıl hile için GCC neden tamsayı bölünmesini uygulamada garip bir sayı ile çarpmayı kullanıyor? . An değişmez sağ tamsayı %da optimize alır ifade gcc -O0bile bir OSR tutucusundadır JITer tarafından optimize edilmiş Buraya kadar, benzer.)

Ancak, aynı yöntemi birkaç kez çalıştırırsanız, ikinci ve sonraki çalıştırmalar, tamamen optimize edilmiş normal (OSR olmayan) kodu yürütür. İşte teoriyi kanıtlamak için bir kriter ( JMH kullanılarak karşılaştırılmıştır ):

@State(Scope.Benchmark)
public class Div {

    @Benchmark
    public void divConst(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % 50000 == 0) {
                blackhole.consume(i);
            }
        }
    }

    @Benchmark
    public void divVar(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;
        long progressCheck = 50000;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                blackhole.consume(i);
            }
        }
    }
}

Ve sonuçlar:

# Benchmark: bench.Div.divConst

# Run progress: 0,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration   1: 126,967 ms/op
# Warmup Iteration   2: 105,660 ms/op
# Warmup Iteration   3: 106,205 ms/op
Iteration   1: 105,620 ms/op
Iteration   2: 105,789 ms/op
Iteration   3: 105,915 ms/op
Iteration   4: 105,629 ms/op
Iteration   5: 105,632 ms/op


# Benchmark: bench.Div.divVar

# Run progress: 50,00% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration   1: 844,708 ms/op          <-- much slower!
# Warmup Iteration   2: 105,893 ms/op          <-- as fast as divConst
# Warmup Iteration   3: 105,601 ms/op
Iteration   1: 105,570 ms/op
Iteration   2: 105,475 ms/op
Iteration   3: 105,702 ms/op
Iteration   4: 105,535 ms/op
Iteration   5: 105,766 ms/op

divVarVerimli bir şekilde derlenmiş OSR saplamasından dolayı ilk yinelemesi gerçekten çok daha yavaştır. Ancak yöntem baştan başlar başlamaz, tüm mevcut derleyici optimizasyonlarını kullanan yeni kısıtsız sürüm yürütülür.


5
Buna oy vermekte tereddüt ediyorum. Bir yandan, "Kriterlerinizi batırdın, JIT hakkında bir şeyler okuyun" demenin ayrıntılı bir yolu gibi geliyor. Öte yandan, OSR'nin buradaki en önemli nokta olduğundan neden bu kadar emin olduğunuzu merak ediyorum. Demek istediğim, içeren (mikro) bir kıyaslama yapmak System.out.printlnneredeyse mutlaka çöp sonuçları üretecek ve her iki versiyonun da eşit derecede hızlı olması , bildiğim kadarıyla özellikle OSR ile herhangi bir şey yapmak zorunda değil ..
Marco13

2
(Merak ediyorum ve bunu anlamak istiyorum. Umarım yorumlar rahatsız edici değildir, daha sonra silebilir, ancak 1:) Bağlantı biraz şüpheli - boş döngü de tamamen optimize edilebilir. İkincisi buna daha çok benziyor. Eğer OSR farkı bağlıyor neden Ama yine, açık değil özellikle . Ben sadece şunu söyleyebilirim: Bir noktada, yöntem JIT'tir ve daha hızlı olur. Anladığım kadarıyla, OSR yalnızca nihai, optimize edilmiş kodun kullanılmasının (kabaca) bir sonraki optimizasyon geçişine ertelenmesine neden olur. (devam ...)
Marco13

1
(devam :) Özel erişim noktası günlüklerini özel olarak analiz etmedikçe, farklılığın JIT ve JIT olmayan kodları karşılaştırarak mı yoksa JITed ve OSR-saplama kodunu karşılaştırarak mı kaynaklandığını söyleyemezsiniz. Ve kesinlikle sorunun gerçek kodu veya tam bir JMH karşılaştırmasını içermediğini kesinlikle söyleyemezsiniz. Bu nedenle, farkın OSR seslerinden kaynaklandığını savunmak, benim için, genel olarak JIT'in neden olduğunu söylemeye kıyasla uygunsuz bir şekilde (ve "haksız"). (Suç yok - sadece merak ediyorum ...)
Marco13

4
@ Marco13 basit bir sezgisel tarama vardır: JIT'in etkinliği olmadan, her %işlem aynı ağırlığa sahip olacaktır, çünkü optimize edilmiş bir yürütme ancak bir optimize edici gerçek iş yaptıysa mümkündür. Bu nedenle, bir döngü varyantının diğerinden önemli ölçüde daha hızlı olması, bir iyileştiricinin varlığını kanıtlar ve ayrıca döngülerden birini diğeriyle aynı derecede optimize edemediğini kanıtlar (aynı yöntemde!). Bu cevap her iki döngüyü de aynı derecede optimize etme yeteneğini kanıtladığından, optimizasyonu engelleyen bir şey olmalıdır. Ve bu, tüm vakaların% 99,9'unda OSR
Holger

4
@ Marco13 Bu, HotSpot Runtime'ın bilgisine ve daha önce benzer sorunları analiz etme deneyimine dayanan "eğitimli bir tahmin" idi. Böyle uzun bir döngü, OSR'den başka bir şekilde, özellikle de basit el yapımı bir kıyaslamada derlenemez. Şimdi, OP tam kodu gönderdiğinde, sadece bir kez kodu ile çalıştırarak akıl yürütmeyi onaylayabilirim -XX:+PrintCompilation -XX:+TraceNMethodInstalls.
apangin

42

Takip etmek ise @phuclv yorumun , ben JIT tarafından üretilen kod kontrol 1 , sonuç olarak şu şunlardır:

için variable % 5000(sabit bölüm):

mov     rax,29f16b11c6d1e109h
imul    rbx
mov     r10,rbx
sar     r10,3fh
sar     rdx,0dh
sub     rdx,r10
imul    r10,rdx,0c350h    ; <-- imul
mov     r11,rbx
sub     r11,r10
test    r11,r11
jne     1d707ad14a0h

için variable % variable:

mov     rax,r14
mov     rdx,8000000000000000h
cmp     rax,rdx
jne     22ccce218edh
xor     edx,edx
cmp     rbx,0ffffffffffffffffh
je      22ccce218f2h
cqo
idiv    rax,rbx           ; <-- idiv
test    rdx,rdx
jne     22ccce218c0h

Bölme işlemi her zaman çarpma işleminden daha uzun sürdüğü için, son kod snippet'i daha az performans gösterir.

Java sürümü:

java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

1 - Kullanılan VM seçenekleri: -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main


14
"Daha yavaş" boyutta bir büyüklük vermek için, x86_64 için: imul3 döngüdür, idiv30 ila 90 döngü arasındadır. Yani tamsayı bölme, tamsayı çarpımından 10x ile 30x daha yavaştır.
Matthieu M.

2
Bunların tümünün, toplayıcı konuşmayan ve ilgilenen okuyucular için ne anlama geldiğini açıklayabilir misiniz?
Nico Haase

7
@NicoHaase Yorum yapılan iki satır sadece önemli olanlardır. İlk bölümde kod bir tamsayı çarpımı gerçekleştirirken, ikinci bölümde kod bir tamsayı bölme gerçekleştirmektedir. Elle çarpma ve bölme yapmayı düşünüyorsanız, çoğaldığınızda genellikle bir grup küçük çarpma ve sonra büyük bir toplama kümesi yaparsınız, ancak bölme küçük bir bölme, küçük bir çarpma, çıkarma ve tekrarlamadır. Bölme yavaştır, çünkü aslında bir sürü çarpma işlemi yapıyorsunuzdur.
MBraedley

4
@ MBraedley girişiniz için teşekkürler, ancak bu açıklama cevabın kendisine eklenmeli ve yorum bölümünde gizlenmemelidir
Nico Haase

6
@MBraedley: Daha da önemlisi, modern bir CPU'da çarpma hızlıdır çünkü kısmi ürünler bağımsızdır ve bu nedenle ayrı bir şekilde hesaplanabilirken, bir bölümün her aşaması önceki aşamalara bağlıdır.
supercat

26

Diğerlerinin de belirttiği gibi, genel modül işlemi bir bölümün yapılmasını gerektirir. Bazı durumlarda, bölme (derleyici tarafından) bir çarpma ile değiştirilebilir. Ancak her ikisi de toplama / çıkarma ile karşılaştırıldığında yavaş olabilir. Bu nedenle, en iyi performans bu çizgilerdeki bir şey tarafından beklenebilir:

long progressCheck = 50000;

long counter = progressCheck;

for (long i = startNum; i <= stopNum; i++){
    if (--counter == 0) {
        System.out.println(i);
        counter = progressCheck;
    }
}

(Küçük bir optimizasyon denemesi olarak, burada 0bir aritmetik işlemden hemen sonra karşılaştırılan birçok mimaride tam olarak 0 talimat / CPU döngüsüne kıyasla bir ön azaltma önleyici kullanıyoruz çünkü ALU'nun bayrakları önceki işlem tarafından uygun şekilde ayarlanmış. Ancak derleyici, siz yazsanız bile bu optimizasyonu otomatik olarak yapar if (counter++ == 50000) { ... counter = 0; }.)

Çoğunlukla gerçekten modül istemediğinize / ihtiyaç duymadığınıza dikkat edin, çünkü döngü sayacınızın ( i) veya yalnızca 1 arttırıldığını biliyorsunuz ve modülün size vereceği gerçek geri kalanı umursamıyorsunuz, sadece bakın tek tek artan sayaç bir değere ulaşırsa.

Başka bir 'hile' iki değer / sınırın gücünü kullanmaktır, ör progressCheck = 1024;. Modül, ikinin gücü bitsel olarak and, yani hızlı bir şekilde hesaplanabilir if ( (i & (1024-1)) == 0 ) {...}. Bu da oldukça hızlı olmalı ve bazı mimarilerde counteryukarıdaki açıktan daha iyi olabilir .


3
Akıllı bir derleyici buradaki döngüleri tersine çevirir. Veya bunu kaynakta yapabilirsiniz. if()Vücut bir dış döngü gövdesi olur ve dışında şeyler if()bir iç döngü gövdesi haline gelmesi için çalışır min(progressCheck, stopNum-i)tekrarlamalar. Yani başlangıçta ve her zaman counter0'a ulaştığında, long next_stop = i + min(progressCheck, stopNum-i);bir for(; i< next_stop; i++) {}döngü için ayar yaparsınız . Bu durumda, iç döngü boştur ve umarım tamamen optimize etmelisiniz, bunu kaynakta yapabilir ve JITer için kolaylaştırabilirsiniz, böylece döngünüzü i + = 50k'ye düşürürsünüz.
Peter Cordes

2
Ancak evet, genel olarak bir aşağı sayaç, fizzbuzz / progresscheck tipi şeyler için iyi ve verimli bir tekniktir.
Peter Cordes

Sorumun eklendi ve bir artışı yaptılar --counterbenim artım versiyonu olarak aynı şekilde hızlıdır, ama daha az olması gerektiği daha olması gerektiği takdirde, ben merak ediyorum 1 düşüktü code.also counter--tam olarak istediğiniz numarayı almak için , çok fazla fark yok
Robert Cotterman

@PeterCordes Akıllı bir derleyici sayıları yazdırır, hiç döngü oluşturmaz. (Sanırım sadece biraz daha önemsiz ölçütler belki 10 yıl önce bu şekilde başarısız olmaya başladı.)
Peter - Monica'yı yeniden eski haline

2
@RobertCotterman Evet, --counterbire bir kapalı. counter--size tam olarak progressCheckyineleme sayısını verecektir (ya da progressCheck = 50001;tabii ki ayarlayabilirsiniz ).
JimmyB

4

Yukarıdaki kodların performansını da görmek beni şaşırttı. Her şey derleyici tarafından belirtilen değişkene göre programın yürütülmesi için geçen süre ile ilgilidir. İkinci (verimsiz) örnekte:

for (long i = startNum; i <= stopNum; i++) {
    if (i % progressCheck == 0) {
        System.out.println(i)
    }
}

İki değişken arasında modül işlemini gerçekleştiriyorsunuz. Burada, derleyici stopNumveprogressCheck bu bir değişkendir ve değeri değiştirmek olabilir, çünkü her tekrardan sonra bu değişkenler için her defasında bulunan belirli bellek bloğuna gitmek için.

Bu nedenle, her yineleme derleyicisinden sonra değişkenlerin en son değerini kontrol etmek için bellek konumuna gitti. Bu nedenle derleme zamanında derleyici verimli bayt kodu oluşturamadı.

İlk kod örneğinde, bir değişken ile yürütme içinde değişmeyecek sabit bir sayısal değer arasında modül operatörü gerçekleştiriyorsunuz ve derleyici, bu sayısal değerin değerini bellek konumundan kontrol etmeye gerek yok. Bu yüzden derleyici verimli bayt kodu oluşturabildi. Eğer bildirirseniz progressCheckbir şekilde finalveya olarak final statico zaman nihai bir değişken olduğunu ve bunun değeri değiştirmek derleyici sonra değişecek değil çalışma zamanı / derleme zamanı derleyici bilmesi sırasında değişken progressCheckile 50000kodunda:

for (long i = startNum; i <= stopNum; i++) {
    if (i % 50000== 0) {
        System.out.println(i)
    }
}

Artık bu kodun ilk (verimli) kod örneğine de benzediğini görebilirsiniz. İlk kodun performansı ve yukarıda belirttiğimiz gibi her iki kod da verimli bir şekilde çalışacaktır. Her iki kod örneğinin yürütme süresinde çok fazla fark olmayacaktır.


1
BÜYÜK bir fark var, i operasyonu bir trilyon kez yapıyordu, bu yüzden 1 trilyondan fazla işlem "verimli" kodunu yapmak için% 89 zaman kazandı. Eğer sadece birkaç bin kez yapıyorsanız, çok küçük bir farktan bahsediyor olsaydınız, muhtemelen büyük bir sorun değil. yani 1000'den fazla işlem, 7 saniyenin 1 milyonda birini kurtaracak.
Robert Cotterman

1
@Bishal Dubey "Her iki kodun yürütme süresinde fazla bir fark olmayacak." Soruyu okudun mu?
Grant Foster

"Her iterasyon derleyici bellek konumuna gittikten sonra neden en değişkenlerinin son değerini kontrol etmek That" - değişken ilan edildi sürece volatile'derleyici' olacak değil defalarca RAM, değerini okuyun.
JimmyB
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.