program story

40 억 회 반복 Java 루프가 2ms 만 걸리는 이유는 무엇입니까?

inputbox 2020. 7. 25. 10:55
반응형

40 억 회 반복 Java 루프가 2ms 만 걸리는 이유는 무엇입니까?


2.7GHz Intel Core i7이 설치된 랩톱에서 다음 Java 코드를 실행하고 있습니다. 2 ^ 32 반복으로 루프를 완료하는 데 걸리는 시간을 측정하려고했는데 대략 1.48 초 (4 / 2.7 = 1.48)로 예상되었습니다.

그러나 실제로 1.48 초 대신 2 밀리 초만 걸립니다. 이것이 아래의 JVM 최적화의 결과인지 궁금합니다.

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

여기에는 두 가지 가능성 중 하나가 있습니다.

  1. 컴파일러는 루프가 중복되고 아무 것도하지 않으므로 루프를 최적화하지 않았다는 것을 깨달았습니다.

  2. JIT (Just-In-Time Compiler)는 루프가 중복되고 아무 것도하지 않는다는 것을 깨달아 최적화했습니다.

최신 컴파일러는 매우 지능적입니다. 그들은 코드가 쓸모없는 것을 볼 수 있습니다. 빈 루프를 GodBolt 에 넣고 출력을 본 다음 -O2최적화 를 켜 십시오. 출력이

main():
    xor eax, eax
    ret

Java에서 대부분의 최적화는 JIT에 의해 수행됩니다. C / C ++와 같은 다른 언어에서는 대부분의 최적화가 첫 번째 컴파일러에서 수행됩니다.


JIT 컴파일러에 의해 최적화 된 것처럼 보입니다. 끄면 ( -Djava.compiler=NONE) 코드가 훨씬 느리게 실행됩니다.

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

OP의 코드를 안에 넣었습니다 class MyClass.


나는 분명히 명백하게 말할 것입니다-이것이 발생하는 JVM 최적화이며 루프는 단순히 제거 될 것입니다. 다음은 활성화 / 활성화 할 때만 차이 JIT무엇인지 보여주는 작은 테스트입니다 C1 Compiler.

면책 조항 : 다음과 같이 테스트를 작성하지 마십시오. 이것은 실제 루프 "제거"가 다음에서 발생 함을 증명하기위한 것입니다 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;
    }
}

결과 JIT는 활성화 된 부분에 따라 메소드가 더 빨라진다는 것을 보여줍니다 (너무 빠르면 루프 제거, C2 Compiler최대 레벨 인 것 같습니다 ).

 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 

As already pointed out, JIT (just-in-time) compiler can optimize an empty loop in order to remove unnecessary iterations. But how?

Actually, there are two JIT compilers: C1 & C2. First, the code is compiled with the C1. C1 collects the statistics and helps the JVM to discover that in 100% cases our empty loop doesn't change anything and is useless. In this situation C2 enters the stage. When the code is get called very often, it can be optimized and compiled with the C2 using collected statistics.

As an example, I will test the next code snippet (my JDK is set to 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!");
    }
}

With the following command line options:

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

And there are different versions of my run method, compiled with the C1 and C2 appropriately. For me, the final variant (C2) looks something like this:

...

; 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

...

It is a little bit messy, but If you look closely, you may notice that there is no long running loop here. There are 3 blocks: B1, B2 and B3 and the execution steps can be B1 -> B2 -> B3 or B1 -> B3. Where Freq: 1 - normalized estimated frequency of a block execution.


You are measuring the time it take to detect the loop doesn't do anything, compile the code in a background thread and eliminate the code.

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

If you run this with -XX:+PrintCompilation you can see the code has been compiled in the background to level 3 or C1 compiler and after a few loops to level 4 of 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

If you change the loop to use a long it doesn't get as optimised.

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

instead you get

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

You consider start and finish time in nanosecond and you divide by 10^6 for calculate the latency

long d = (finish - start) / 1000000

it should be 10^9 because 1 second = 10^9 nanosecond.

참고URL : https://stackoverflow.com/questions/47957337/why-does-a-4-billion-iteration-java-loop-take-only-2-ms

반응형