Tại sao một vòng lặp Java 4 tỷ lần lặp lại chỉ mất 2 ms?


113

Tôi đang chạy mã Java sau trên máy tính xách tay có Intel Core i7 2,7 GHz. Tôi dự định để nó đo khoảng thời gian để hoàn thành một vòng lặp với 2 ^ 32 lần lặp, tôi dự kiến ​​khoảng 1,48 giây (4 / 2,7 = 1,48).

Nhưng thực tế nó chỉ mất 2 mili giây, thay vì 1,48 giây. Tôi tự hỏi liệu đây có phải là kết quả của bất kỳ tối ưu hóa JVM nào bên dưới không?

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
Vâng, vâng. Bởi vì phần thân vòng lặp không có tác dụng phụ, trình biên dịch khá vui vẻ loại bỏ nó. Kiểm tra mã byte javap -vđể xem.
Elliott Frisch

36
Bạn sẽ không thấy điều đó trở lại trong mã byte. javacthực hiện rất ít tối ưu hóa thực tế và để hầu hết nó cho trình biên dịch JIT.
Jorn Vernee

4
'Tôi đang tự hỏi liệu đây có phải là kết quả của bất kỳ tối ưu hóa JVM nào bên dưới không?' - Bạn nghĩ sao? Nó có thể là gì khác nếu không phải là tối ưu hóa JVM?
apangin

7
Câu trả lời cho câu hỏi này về cơ bản được chứa trong stackoverflow.com/a/25323548/3182664 . Nó cũng chứa cụm kết quả (mã máy) mà JIT tạo ra cho các trường hợp như vậy, cho thấy rằng vòng lặp được JIT tối ưu hóa hoàn toàn . (Câu hỏi tại stackoverflow.com/q/25326377/3182664 cho thấy rằng có thể lâu hơn một chút nếu vòng lặp không thực hiện 4 tỷ phép toán mà 4 tỷ trừ đi một ;-)). Tôi gần như coi câu hỏi này là một bản sao của câu hỏi kia - bạn có phản đối không?
Marco 13

7
Bạn giả sử bộ xử lý sẽ thực hiện một lần lặp trên mỗi Hz. Đó là một giả định sâu rộng. Các bộ vi xử lý ngày nay thực hiện tất cả các loại tối ưu hóa, như @Rahul đã đề cập và trừ khi bạn biết nhiều hơn về cách hoạt động của Core i7, bạn không thể giả định như vậy.
Tsahi Asher

Câu trả lời:


106

Có một trong hai khả năng xảy ra ở đây:

  1. Trình biên dịch nhận ra rằng vòng lặp là thừa và không làm gì cả nên nó đã tối ưu hóa nó đi.

  2. JIT (trình biên dịch kịp thời) nhận ra rằng vòng lặp là thừa và không làm gì cả, vì vậy nó đã tối ưu hóa nó.

Các trình biên dịch hiện đại rất thông minh; họ có thể thấy khi nào mã là vô dụng. Hãy thử đặt một vòng lặp trống vào GodBolt và xem đầu ra, sau đó bật tính năng -O2tối ưu hóa, bạn sẽ thấy rằng đầu ra là một thứ gì đó dọc theo dòng

main():
    xor eax, eax
    ret

Tôi muốn làm rõ điều gì đó, trong Java hầu hết các tối ưu hóa được thực hiện bởi JIT. Trong một số ngôn ngữ khác (như C / C ++), hầu hết các tối ưu hóa được thực hiện bởi trình biên dịch đầu tiên.


Trình biên dịch có được phép tối ưu hóa như vậy không? Tôi không biết chắc chắn về Java, nhưng các trình biên dịch .NET thường nên tránh điều này để cho phép JIT thực hiện tối ưu hóa tốt nhất cho nền tảng.
IllidanS4 muốn Monica lại

1
@ IllidanS4 Nói chung, điều này phụ thuộc vào tiêu chuẩn ngôn ngữ. Nếu trình biên dịch có thể thực hiện tối ưu hóa có nghĩa là mã, được diễn giải theo tiêu chuẩn, có tác dụng tương tự, thì có. Tuy nhiên, có rất nhiều điều tinh tế mà người ta phải xem xét, ví dụ: có một số phép biến đổi đối với các phép tính dấu phẩy động có thể dẫn đến khả năng xảy ra quá trình thừa / thiếu, vì vậy mọi tối ưu hóa đều phải được thực hiện cẩn thận.
user1997744

9
@ IllidanS4 nên làm thế nào để môi trường thời gian chạy có thể tối ưu hóa tốt hơn? Ít nhất nó phải phân tích mã không thể nhanh hơn việc xóa mã trong quá trình biên dịch.
Gerhardh

2
@Gerhardh Tôi không nói về trường hợp chính xác này, khi thời gian chạy không thể thực hiện công việc tốt hơn trong việc loại bỏ các phần mã thừa, nhưng tất nhiên có thể có một số trường hợp lý do này đúng. Và bởi vì có thể có các trình biên dịch khác cho JRE từ các ngôn ngữ khác, thời gian chạy cũng nên thực hiện những tối ưu hóa này, vì vậy có khả năng không có lý do gì để chúng được thực hiện bởi cả thời gian chạy và trình biên dịch.
IllidanS4 muốn Monica trở lại

6
@ IllidanS4 bất kỳ tối ưu hóa thời gian chạy nào không thể mất ít hơn 0 thời gian. Việc ngăn trình biên dịch xóa mã sẽ không có ý nghĩa gì.
Gerhardh

55

Có vẻ như nó đã được tối ưu hóa bởi trình biên dịch JIT. Khi tôi tắt nó ( -Djava.compiler=NONE), mã chạy chậm hơn nhiều:

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

Tôi đặt mã của OP bên trong class MyClass.


2
Kỳ dị. Khi tôi chạy mã cả hai phương diện, nó nhanh hơn mà không có cờ, nhưng chỉ bởi một yếu tố của 10, và thêm hoặc loại bỏ zeros để số lần lặp trong vòng lặp cũng ảnh hưởng đến thời gian chạy bởi các yếu tố của mười, có và không có cờ. Vì vậy, (đối với tôi) vòng lặp dường như không được tối ưu hóa hoàn toàn, bằng cách nào đó, chỉ được thực hiện nhanh hơn 10 lần. (Oracle Java 8-151)
tobias_k

@tobias_k nó phụ thuộc vào giai đoạn của JIT mà vòng lặp đang trải qua, tôi đoán stackoverflow.com/a/47972226/1059372
Eugene

21

Tôi chỉ sẽ nói rõ ràng - rằng đây là một tối ưu hóa JVM xảy ra, vòng lặp sẽ đơn giản bị loại bỏ. Đây là một bài kiểm tra nhỏ cho thấy sự khác biệt rất lớnJIT khi chỉ bật / kích hoạt C1 Compilervà vô hiệu hóa.

Tuyên bố từ chối trách nhiệm: không viết các bài kiểm tra như thế này - điều này chỉ để chứng minh rằng vòng lặp thực sự "loại bỏ" xảy ra trong 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;
    }
}

Kết quả cho thấy rằng tùy thuộc vào phần nào của phần mềm JITđược kích hoạt, phương thức sẽ nhanh hơn (nhanh hơn nhiều đến mức có vẻ như nó "không có gì" - loại bỏ vòng lặp, dường như đang xảy ra ở C2 Compiler- là mức tối đa):

 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

Như đã chỉ ra, trình biên dịch JIT (just-in-time) có thể tối ưu hóa một vòng lặp trống để loại bỏ các lần lặp không cần thiết. Nhưng bằng cách nào?

Trên thực tế, có hai trình biên dịch JIT: C1 & C2 . Đầu tiên, mã được biên dịch với C1. C1 thu thập các số liệu thống kê và giúp JVM phát hiện ra rằng trong 100% trường hợp, vòng lặp trống của chúng tôi không thay đổi bất cứ điều gì và vô dụng. Trong tình huống này, C2 bước vào sân khấu. Khi mã được gọi rất thường xuyên, nó có thể được tối ưu hóa và biên dịch với C2 bằng cách sử dụng số liệu thống kê được thu thập.

Ví dụ: tôi sẽ kiểm tra đoạn mã tiếp theo (JDK của tôi được đặt thành 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!");
    }
}

Với các tùy chọn dòng lệnh sau:

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

Và có nhiều phiên bản khác nhau của phương pháp chạy của tôi , được biên dịch với C1 và C2 một cách thích hợp. Đối với tôi, biến thể cuối cùng (C2) trông giống như sau:

...

; 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

...

Nó hơi lộn xộn một chút, nhưng nếu bạn nhìn kỹ, bạn có thể nhận thấy rằng không có vòng lặp chạy dài ở đây. Có 3 khối: B1, B2 và B3 và các bước thực hiện có thể là B1 -> B2 -> B3hoặc B1 -> B3. Where Freq: 1- tần suất ước tính chuẩn hóa của một khối thực thi.


8

Bạn đang đo thời gian cần thiết để phát hiện vòng lặp không làm gì cả, hãy biên dịch mã trong một chuỗi nền và loại bỏ mã.

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

Nếu bạn chạy điều này với -XX:+PrintCompilationbạn, bạn có thể thấy mã đã được biên dịch trong nền đến trình biên dịch cấp 3 hoặc C1 và sau một vài vòng lặp đến cấp 4 của 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

Nếu bạn thay đổi vòng lặp để sử dụng longnó, nó không được tối ưu hóa.

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

thay vào đó bạn nhận được

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

Thật kỳ lạ ... tại sao một bộ longđếm lại ngăn chặn việc tối ưu hóa tương tự xảy ra?
Ryan Amos

@RyanAmos tối ưu hóa chỉ được áp dụng cho số vòng lặp nguyên thủy phổ biến nếu kiểu intghi chú char và short thực sự giống nhau ở cấp mã byte.
Peter Lawrey

-1

Bạn coi thời gian bắt đầu và kết thúc tính bằng nano giây và bạn chia cho 10 ^ 6 để tính độ trễ

long d = (finish - start) / 1000000

nó phải là 10^91giây = 10^9nano giây.


Những gì bạn đề xuất không liên quan đến quan điểm của tôi. Điều tôi băn khoăn là thời lượng này mất bao lâu, và không quan trọng thời lượng này được in / biểu diễn dưới dạng mili giây hay giây.
twimo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.