4 milyar yinelemeli bir Java döngüsü neden yalnızca 2 ms sürüyor?


113

2.7 GHz Intel Core i7'ye sahip bir dizüstü bilgisayarda aşağıdaki Java kodunu çalıştırıyorum. Kabaca 1.48 saniye (4 / 2.7 = 1.48) olmasını beklediğim 2 ^ 32 yinelemeli bir döngüyü bitirmenin ne kadar sürdüğünü ölçmesine izin vermek niyetindeydim.

Ama aslında 1,48 saniye yerine sadece 2 milisaniye sürüyor. Bunun altındaki herhangi bir JVM optimizasyonunun bir sonucu olup olmadığını merak ediyorum.

public static void main(String[] args)
{
    long start = System.nanoTime();

    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++){
    }
    long finish = System.nanoTime();
    long d = (finish - start) / 1000000;

    System.out.println("Used " + d);
}

69
İyi evet. Döngü gövdesinin hiçbir yan etkisi olmadığından, derleyici bunu mutlu bir şekilde ortadan kaldırır. Bayt kodunu javap -vgörmek için ile inceleyin.
Elliott Frisch

36
Bayt kodunda bunu tekrar görmeyeceksiniz. javacçok az gerçek optimizasyon yapar ve çoğunu JIT derleyicisine bırakır.
Jorn Vernee

4
Bunun, altındaki herhangi bir JVM optimizasyonunun bir sonucu olup olmadığını merak ediyorum. - Ne düşünüyorsun? JVM optimizasyonu değilse başka ne olabilir?
apangin

7
Bu sorunun cevabı temelde stackoverflow.com/a/25323548/3182664'te bulunur . Ayrıca, JIT'in bu gibi durumlar için ürettiği sonuç derlemesini (makine kodu) içerir ve döngünün JIT tarafından tamamen optimize edildiğini gösterir . ( Stackoverflow.com/q/25326377/3182664 adresindeki soru , döngü 4 milyar işlem yapmazsa biraz daha uzun sürebilir, ancak 4 milyar eksi bir ;-)). Bu soruyu neredeyse diğerinin bir kopyası olarak görüyorum - herhangi bir itiraz var mı?
Marco13

7
İşlemcinin Hz başına bir yineleme gerçekleştireceğini varsayarsınız. Bu geniş kapsamlı bir varsayımdır. Bugün işlemciler, @Rahul'un bahsettiği gibi her türlü optimizasyonu gerçekleştiriyor ve Core i7'nin nasıl çalıştığı hakkında daha fazla bilgi sahibi değilseniz, bunu varsayamazsınız.
Tsahi Asher

Yanıtlar:


106

Burada iki olasılıktan biri var:

  1. Derleyici, döngünün gereksiz olduğunu ve hiçbir şey yapmadığının farkına vardı, bu yüzden onu optimize etti.

  2. JIT (tam zamanında derleyici), döngünün fazlalık olduğunu ve hiçbir şey yapmadığını fark etti, bu yüzden onu optimize etti.

Modern derleyiciler çok zekidir; kodun ne zaman işe yaramadığını görebilirler. GodBolt'a boş bir döngü koymayı deneyin ve çıktıya bakın, ardından -O2optimizasyonları açın, çıktının aşağıdaki satırlar boyunca bir şey olduğunu göreceksiniz.

main():
    xor eax, eax
    ret

Bir şeyi açıklığa kavuşturmak isterim, Java'da optimizasyonların çoğu JIT tarafından yapılır. Diğer bazı dillerde (C / C ++ gibi) optimizasyonların çoğu ilk derleyici tarafından yapılır.


Derleyicinin bu tür optimizasyonları yapmasına izin var mı? Java'dan emin değilim, ancak .NET derleyicileri, JIT'in platform için en iyi optimizasyonları yapmasına izin vermek için genellikle bundan kaçınmalıdır.
IllidanS4, Monica'yı

1
@ IllidanS4 Genel olarak bu, dil standardına bağlıdır. Derleyici, standart tarafından yorumlanan kodun aynı etkiye sahip olduğu anlamına gelen optimizasyonları gerçekleştirebilirse, evet. Bununla birlikte, dikkate alınması gereken birçok incelik vardır, örneğin, kayan nokta hesaplamaları için, aşırı / yetersiz akış olasılığına yol açabilecek bazı dönüşümler vardır, bu nedenle herhangi bir optimizasyonun dikkatlice yapılması gerekir.
user1997744

9
@ IllidanS4 çalışma zamanı ortamı nasıl daha iyi optimizasyon yapabilmelidir? En azından, derleme sırasında kodu kaldırmaktan daha hızlı olamayacak olan kodu analiz etmelidir.
Gerhardh

2
@Gerhardh Çalışma zamanı kodun gereksiz parçalarını kaldırmada daha iyi bir iş çıkaramadığında bu kesin durumdan bahsetmiyordum, ancak elbette bu nedenin doğru olduğu bazı durumlar olabilir. Ve JRE için başka dillerden başka derleyiciler olabileceğinden, çalışma zamanı da bu optimizasyonları yapmalıdır , bu nedenle bunların hem çalışma zamanı hem de derleyici tarafından yapılması için potansiyel olarak hiçbir neden yoktur.
IllidanS4 Monica'yı

6
@ IllidanS4 herhangi bir çalışma zamanı optimizasyonu sıfır süreden daha az zaman alamaz. Derleyicinin kodu kaldırmasının engellenmesi bir anlam ifade etmeyecektir.
Gerhardh

55

Görünüşe göre JIT derleyicisi tarafından optimize edilmiş. Kapattığımda ( -Djava.compiler=NONE), kod çok daha yavaş çalışıyor:

$ javac MyClass.java
$ java MyClass
Used 4
$ java -Djava.compiler=NONE MyClass
Used 40409

OP'nin kodunu içine koydum class MyClass.


2
Tuhaf. Ben kodu iki yönde çalıştırdığınızda, bir hızlı bayrağı olmadan, sadece 10 faktör tarafından ve ekleyerek veya ayrıca döngü içinde yineleme sayısına sıfır atarak ve olmadan, on faktörler tarafından çalışma süresini etkiler bayrağı. Yani (benim için) döngü tamamen optimize edilmemiş gibi görünüyor, sadece bir şekilde 10 kat daha hızlı yapıldı. (Oracle Java 8-151)
tobias_k

@tobias_k döngünün JIT'in hangi aşamasından geçtiğine bağlıdır sanırım stackoverflow.com/a/47972226/1059372
Eugene

21

Sadece bariz olanı ifade edeceğim - bu gerçekleşen bir JVM optimizasyonu, döngü basitçe kaldırılacak. Burada, yalnızca etkinleştirildiğinde / etkinleştirildiğinde ve devre dışı bırakıldığında ne kadar büyük bir fark olduğunu gösteren küçük bir test var .JITC1 Compiler

Sorumluluk reddi beyanı: testleri böyle yazmayın - bu sadece gerçek döngü "kaldırmanın" şu durumlarda gerçekleştiğini kanıtlamak içindir C2 Compiler:

@Benchmark
@Fork(1)
public void full() {
    long result = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
        ++result;
    }
}

@Benchmark
@Fork(1)
public void minusOne() {
    long result = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
        ++result;
    }
}

@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-XX:TieredStopAtLevel=1" })
public void withoutC2() {
    long result = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
        ++result;
    }
}

@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-Xint" })
public void withoutAll() {
    long result = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
        ++result;
    }
}

Sonuçlar JIT, yöntemin hangi bölümünün etkinleştirildiğine bağlı olarak , yöntemin daha hızlı hale geldiğini gösteriyor (o kadar hızlı ki, "hiçbir şey yapmıyor" gibi görünüyor - döngü kaldırma, C2 Compiler- ki bu, maksimum seviyedir - gerçekleşiyor gibi görünüyor ):

 Benchmark                Mode  Cnt      Score   Error  Units
 Loop.full        avgt    2      10⁻⁷          ms/op
 Loop.minusOne    avgt    2      10⁻⁶          ms/op
 Loop.withoutAll  avgt    2  51782.751          ms/op
 Loop.withoutC2   avgt    2   1699.137          ms/op 

13

Daha önce de belirtildiği gibi, JIT (tam zamanında) derleyicisi, gereksiz yinelemeleri kaldırmak için boş bir döngüyü optimize edebilir. Ama nasıl?

Aslında, iki JIT derleyicisi vardır: C1 ve C2 . İlk olarak, kod C1 ile derlenir. C1 istatistikleri toplar ve JVM'nin% 100 durumlarda boş döngümüzün hiçbir şeyi değiştirmediğini ve faydasız olduğunu keşfetmesine yardımcı olur. Bu durumda C2 sahneye girer. Kod çok sık arandığında, toplanan istatistikler kullanılarak C2 ile optimize edilebilir ve derlenebilir.

Örnek olarak, sonraki kod parçacığını test edeceğim ( JDK'm slowdebug build 9-internal olarak ayarlandı ):

public class Demo {
    private static void run() {
        for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
        }
        System.out.println("Done!");
    }
}

Aşağıdaki komut satırı seçenekleriyle:

-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Demo.run

Ve çalıştırma yöntemimin C1 ve C2 ile uygun şekilde derlenmiş farklı versiyonları var . Benim için son değişken (C2) şuna benzer:

...

; B1: # B3 B2 <- BLOCK HEAD IS JUNK  Freq: 1
0x00000000125461b0: mov   dword ptr [rsp+0ffffffffffff7000h], eax
0x00000000125461b7: push  rbp
0x00000000125461b8: sub   rsp, 40h
0x00000000125461bc: mov   ebp, dword ptr [rdx]
0x00000000125461be: mov   rcx, rdx
0x00000000125461c1: mov   r10, 57fbc220h
0x00000000125461cb: call  indirect r10    ; *iload_1

0x00000000125461ce: cmp   ebp, 7fffffffh  ; 7fffffff => 2147483647
0x00000000125461d4: jnl   125461dbh       ; jump if not less

; B2: # B3 <- B1  Freq: 0.999999
0x00000000125461d6: mov   ebp, 7fffffffh  ; *if_icmpge

; B3: # N44 <- B1 B2  Freq: 1       
0x00000000125461db: mov   edx, 0ffffff5dh
0x0000000012837d60: nop
0x0000000012837d61: nop
0x0000000012837d62: nop
0x0000000012837d63: call  0ae86fa0h

...

Biraz dağınık, ancak yakından bakarsanız, burada uzun süren bir döngü olmadığını fark edebilirsiniz. 3 blok vardır: B1, B2 ve B3 ve yürütme adımları B1 -> B2 -> B3veya olabilir B1 -> B3. Nerede Freq: 1- bir blok yürütmenin normalleştirilmiş tahmini sıklığı.


8

Döngünün hiçbir şey yapmadığını algılamak için geçen süreyi ölçüyorsunuz, kodu arka planda derleyin ve kodu ortadan kaldırın.

for (int t = 0; t < 5; t++) {
    long start = System.nanoTime();
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
    }
    long time = System.nanoTime() - start;

    String s = String.format("%d: Took %.6f ms", t, time / 1e6);
    Thread.sleep(50);
    System.out.println(s);
    Thread.sleep(50);
}

Bunu birlikte çalıştırırsanız -XX:+PrintCompilation, kodun arka planda seviye 3 veya C1 derleyicisine ve birkaç döngüden sonra C4'ün 4. seviyesine derlendiğini görebilirsiniz.

    129   34 %     3       A::main @ 15 (93 bytes)
    130   35       3       A::main (93 bytes)
    130   36 %     4       A::main @ 15 (93 bytes)
    131   34 %     3       A::main @ -2 (93 bytes)   made not entrant
    131   36 %     4       A::main @ -2 (93 bytes)   made not entrant
0: Took 2.510408 ms
    268   75 %     3       A::main @ 15 (93 bytes)
    271   76 %     4       A::main @ 15 (93 bytes)
    274   75 %     3       A::main @ -2 (93 bytes)   made not entrant
1: Took 5.629456 ms
2: Took 0.000000 ms
3: Took 0.000364 ms
4: Took 0.000365 ms

Döngüyü bir kullanmak için değiştirirseniz, longoptimize edilmiş olmaz.

    for (long i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
    }

onun yerine alırsın

0: Took 1579.267321 ms
1: Took 1674.148662 ms
2: Took 1885.692166 ms
3: Took 1709.870567 ms
4: Took 1754.005112 ms

Bu garip ... Neden bir longsayaç aynı optimizasyonun olmasını engellesin?
Ryan Amos

@RyanAmos optimizasyonu, yalnızca type intnote char ve short, bayt kodu düzeyinde etkin bir şekilde aynıysa , ortak ilkel döngü sayısına uygulanır .
Peter Lawrey

-1

Başlangıç ​​ve bitiş zamanını nanosaniye olarak kabul edersiniz ve gecikmeyi hesaplamak için 10 ^ 6'ya bölersiniz

long d = (finish - start) / 1000000

Olması gereken 10^9, çünkü 1ikinci = 10^9nanosaniye.


Önerdiğin şey benim açımdan alakasız. Merak ettiğim şey ne kadar sürdüğü ve bu sürenin milisaniye veya saniye cinsinden basılması / temsil edilmesi önemli değil.
twimo
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.