Mengapa loop Java 4 miliar iterasi hanya membutuhkan waktu 2 ms?

113

Saya menjalankan kode Java berikut di laptop dengan 2,7 GHz Intel Core i7. Saya bermaksud untuk membiarkannya mengukur berapa lama waktu yang dibutuhkan untuk menyelesaikan satu loop dengan 2 ^ 32 iterasi, yang saya perkirakan kira-kira 1,48 detik (4 / 2,7 = 1,48).

Tapi sebenarnya hanya butuh 2 milidetik, bukan 1,48 s. Saya ingin tahu apakah ini adalah hasil dari pengoptimalan JVM di bawahnya?

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);
}
twimo
sumber
69
Baiklah. Karena badan perulangan tidak memiliki efek samping, penyusun dengan senang hati menghilangkannya. Periksa kode byte dengan javap -vuntuk melihat.
Elliott Frisch
36
Anda tidak akan melihatnya kembali dalam kode byte. javacmelakukan pengoptimalan aktual yang sangat sedikit dan menyerahkan sebagian besar ke kompiler JIT.
Jorn Vernee
4
'Saya ingin tahu apakah ini hasil dari pengoptimalan JVM di bawahnya?' - Bagaimana menurut anda? Apa lagi jika bukan pengoptimalan JVM?
apangin
7
Jawaban atas pertanyaan ini pada dasarnya ada di stackoverflow.com/a/25323548/3182664 . Ini juga berisi rakitan yang dihasilkan (kode mesin) yang dihasilkan JIT untuk kasus seperti itu, menunjukkan bahwa loop sepenuhnya dioptimalkan oleh JIT . (Pertanyaan di stackoverflow.com/q/25326377/3182664 menunjukkan bahwa mungkin diperlukan waktu lebih lama jika loop tidak melakukan 4 miliar operasi, tetapi 4 miliar dikurangi satu ;-)). Saya hampir menganggap pertanyaan ini sebagai duplikat dari yang lain - ada keberatan?
Marco13
7
Anda berasumsi prosesor akan melakukan satu iterasi per Hz. Itu adalah asumsi yang menjangkau jauh. Prosesor saat ini melakukan semua jenis pengoptimalan, seperti yang disebutkan @Rahul, dan kecuali Anda tahu lebih banyak tentang cara kerja Core i7, Anda tidak dapat berasumsi demikian.
Tsahi Asher

Jawaban:

106

Ada satu dari dua kemungkinan yang terjadi di sini:

  1. Kompiler menyadari bahwa loop itu redundan dan tidak melakukan apa-apa sehingga dioptimalkan.

  2. JIT (just-in-time compiler) menyadari bahwa loop itu mubazir dan tidak melakukan apa-apa, jadi itu dioptimalkan.

Penyusun modern sangat cerdas; mereka dapat melihat jika kode tidak berguna. Coba letakkan loop kosong ke GodBolt dan lihat outputnya, lalu aktifkan -O2pengoptimalan, Anda akan melihat bahwa outputnya adalah sesuatu di sepanjang baris

main():
    xor eax, eax
    ret

Saya ingin menjelaskan sesuatu, di Jawa sebagian besar pengoptimalan dilakukan oleh JIT. Dalam beberapa bahasa lain (seperti C / C ++), sebagian besar pengoptimalan dilakukan oleh kompilator pertama.

van dench
sumber
Apakah kompilator diizinkan untuk melakukan pengoptimalan seperti itu? Saya tidak tahu pasti untuk Java, tetapi compiler .NET umumnya harus menghindari ini untuk memungkinkan JIT melakukan pengoptimalan terbaik untuk platform.
IllidanS4 ingin Monica kembali
1
@ IllidanS4 Secara umum, ini tergantung pada standar bahasa. Jika kompilator dapat melakukan pengoptimalan yang berarti kode tersebut, yang ditafsirkan oleh standar, memiliki efek yang sama, maka ya. Ada banyak kehalusan yang harus dipertimbangkan, misalnya ada beberapa transformasi untuk perhitungan floating point yang dapat mengakibatkan kemungkinan terjadinya over / underflow, sehingga setiap optimasi harus dilakukan dengan hati-hati.
pengguna1997744
9
@ IllidanS4 bagaimana seharusnya lingkungan runtime dapat melakukan pengoptimalan yang lebih baik? Paling tidak ia harus menganalisis kode yang tidak bisa lebih cepat daripada menghapus kode selama kompilasi.
Gerhardh
2
@Gerhardh Saya tidak berbicara tentang kasus yang tepat ini, ketika runtime tidak dapat melakukan pekerjaan yang lebih baik dalam menghapus bagian kode yang berlebihan, tetapi tentu saja mungkin ada beberapa kasus ketika alasan ini benar. Dan karena mungkin ada compiler lain untuk JRE dari bahasa lain, runtime juga harus melakukan pengoptimalan ini, jadi tidak ada alasan bagi mereka untuk melakukannya baik oleh runtime maupun compiler.
IllidanS4 ingin Monica kembali
6
@ IllidanS4, pengoptimalan waktu proses apa pun tidak boleh kurang dari nol waktu. Mencegah kompilator menghapus kode tidak masuk akal.
Gerhardh
55

Sepertinya itu dioptimalkan oleh kompiler JIT. Saat saya mematikannya ( -Djava.compiler=NONE), kode berjalan lebih lambat:

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

Saya memasukkan kode OP di dalam class MyClass.

Akavall
sumber
2
Aneh. Ketika saya menjalankan kode kedua cara, itu adalah lebih cepat tanpa bendera, tetapi hanya dengan faktor 10, dan menambahkan atau menghapus nol untuk jumlah iterasi dalam loop juga mempengaruhi waktu berjalan oleh faktor dari sepuluh, dengan dan tanpa bendera. Jadi (bagi saya) loop sepertinya tidak dioptimalkan sepenuhnya, hanya dibuat 10 kali lebih cepat, entah bagaimana. (Oracle Java 8-151)
tobias_k
@tobias_k itu tergantung pada tahap JIT apa yang dilalui loop saya kira stackoverflow.com/a/47972226/1059372
Eugene
21

Saya hanya akan menyatakan yang sudah jelas - bahwa ini adalah pengoptimalan JVM yang terjadi, loop hanya akan dihapus sama sekali. Berikut adalah tes kecil yang menunjukkan perbedaan besarJIT ketika diaktifkan / diaktifkan hanya untuk C1 Compilerdan dinonaktifkan sama sekali.

Penafian: jangan tulis pengujian seperti ini - ini hanya untuk membuktikan bahwa loop sebenarnya "penghapusan" terjadi di 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;
    }
}

Hasilnya menunjukkan bahwa bergantung pada bagian mana dari yang JITdiaktifkan, metode menjadi lebih cepat (jauh lebih cepat sehingga terlihat seperti "tidak melakukan apa-apa" - penghapusan loop, yang tampaknya terjadi di C2 Compiler- yang merupakan level maksimum):

 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 
Eugene
sumber
13

Seperti yang telah ditunjukkan, kompiler JIT (just-in-time) dapat mengoptimalkan loop kosong untuk menghapus iterasi yang tidak perlu. Tapi bagaimana caranya?

Sebenarnya, ada dua kompiler JIT: C1 & C2 . Pertama, kode dikompilasi dengan C1. C1 mengumpulkan statistik dan membantu JVM menemukan bahwa dalam 100% kasus loop kosong kami tidak mengubah apa pun dan tidak berguna. Dalam situasi ini C2 memasuki panggung. Ketika kode sangat sering dipanggil, itu dapat dioptimalkan dan dikompilasi dengan C2 menggunakan statistik yang dikumpulkan.

Sebagai contoh, saya akan menguji cuplikan kode berikutnya (JDK saya disetel ke slowdebug build 9-internal ):

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

Dengan opsi baris perintah berikut:

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

Dan ada versi berbeda dari metode run saya , dikompilasi dengan C1 dan C2 secara tepat. Bagi saya, varian terakhir (C2) terlihat seperti ini:

...

; 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

...

Ini sedikit berantakan, tetapi jika Anda melihat lebih dekat, Anda mungkin memperhatikan bahwa tidak ada putaran panjang di sini. Ada 3 blok: B1, B2 dan B3 dan langkah-langkah pelaksanaannya bisaB1 -> B2 -> B3 atau B1 -> B3. Dimana Freq: 1- frekuensi perkiraan yang dinormalisasi dari eksekusi blok.

Oleksandr Pyrohov
sumber
8

Anda mengukur waktu yang dibutuhkan untuk mendeteksi loop tidak melakukan apa-apa, mengkompilasi kode di thread latar belakang dan menghilangkan kode.

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);
}

Jika Anda menjalankan ini dengan -XX:+PrintCompilationAnda dapat melihat kode telah dikompilasi di latar belakang ke kompiler level 3 atau C1 dan setelah beberapa loop ke level 4 dari C4.

    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

Jika Anda mengubah loop untuk menggunakan longitu tidak bisa dioptimalkan.

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

alih-alih Anda mendapatkan

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
Peter Lawrey
sumber
Aneh ... mengapa longpenghitung mencegah pengoptimalan yang sama terjadi?
Ryan Amos
@RyanAmos, pengoptimalan hanya diterapkan ke jumlah loop primitif umum jika tipe intnote char dan short secara efektif sama pada level kode byte.
Peter Lawrey
-1

Anda mempertimbangkan waktu mulai dan selesai dalam nanodetik dan Anda membaginya dengan 10 ^ 6 untuk menghitung latensi

long d = (finish - start) / 1000000

seharusnya 10^9karena 1detik = 10^9nanodetik.

DHARMENDRA SINGH
sumber
Apa yang Anda sarankan tidak relevan dengan poin saya. Yang saya ingin tahu adalah berapa lama waktu yang dibutuhkan, dan tidak masalah apakah durasi ini dicetak / ditampilkan dalam mili-detik atau detik.
twimo