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


113

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

69
그래. 루프 본문에는 부작용이 없기 때문에 컴파일러는이를 제거합니다. javap -v볼 수 있도록 바이트 코드를 조사 하십시오.
Elliott Frisch 2017

36
바이트 코드에서 다시 볼 수 없습니다. javac실제 최적화는 거의 수행하지 않으며 대부분의 경우 JIT 컴파일러에 맡깁니다.
요른 Vernee

4
'저는 이것이 JVM 최적화의 결과인지 궁금합니다.' - 어떻게 생각해? JVM 최적화가 아니라면 무엇이 될 수 있습니까?
apangin

7
이 질문에 대한 답변은 기본적으로 stackoverflow.com/a/25323548/3182664에 포함되어 있습니다. 또한 JIT가 이러한 경우에 생성하는 결과 어셈블리 (기계 코드)도 포함되어 루프가 JIT에 의해 완전히 최적화 되었음을 보여줍니다 . ( stackoverflow.com/q/25326377/3182664 의 질문 은 루프가 40 억 개의 작업을 수행하지 않고 40 억 에서 1을 빼면 시간이 조금 더 걸릴 수 있음을 보여줍니다 ;-)). 나는이 질문을 다른 질문의 중복으로 거의 생각할 것입니다.
Marco13

7
프로세서가 Hz 당 한 번의 반복을 수행한다고 가정합니다. 그것은 광범위한 가정입니다. 오늘날 프로세서는 @Rahul이 언급했듯이 모든 종류의 최적화를 수행하며 Core i7의 작동 방식에 대해 더 많이 알지 않는 한이를 가정 할 수 없습니다.
Tsahi 아셀

답변:


106

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

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

  2. JIT (just-in-time compiler)는 루프가 중복되고 아무것도하지 않는다는 것을 깨달았으므로이를 최적화했습니다.

최신 컴파일러는 매우 지능적입니다. 그들은 코드가 쓸모없는 때를 볼 수 있습니다. GodBolt에 빈 루프를 넣고 출력을 살펴본 다음 -O2최적화 를 켜면 출력이 다음과 같은 내용임을 알 수 있습니다.

main():
    xor eax, eax
    ret

Java에서 대부분의 최적화는 JIT에 의해 수행된다는 점을 명확히하고 싶습니다. 일부 다른 언어 (예 : C / C ++)에서는 대부분의 최적화가 첫 번째 컴파일러에 의해 수행됩니다.


컴파일러가 이러한 최적화를 수행 할 수 있습니까? Java에 대해서는 확실하지 않지만 .NET 컴파일러는 일반적으로 JIT가 플랫폼에 대해 최상의 최적화를 수행 할 수 있도록이를 피해야합니다.
IllidanS4는 Monica가 2017

1
@ IllidanS4 일반적으로 이것은 언어 표준에 따라 다릅니다. 컴파일러가 최적화를 수행 할 수 있다면 표준에 의해 해석되는 코드가 동일한 효과를 갖는다는 의미입니다. 하지만 고려해야 할 많은 미묘한 점이 있습니다. 예를 들어 오버플로 / 언더 플로가 발생할 가능성을 초래할 수있는 부동 소수점 계산에 대한 일부 변환이 있으므로 최적화를 신중하게 수행해야합니다.
user1997744 dec.

9
@ IllidanS4 런타임 환경이 어떻게 더 나은 최적화를 수행 할 수 있어야합니까? 최소한 컴파일 중에 코드를 제거하는 것보다 빠를 수없는 코드를 분석해야합니다.
Gerhardh

2
@Gerhardh 런타임이 코드의 중복 부분을 제거하는 데 더 나은 작업을 수행 할 수없는 정확한 경우에 대해 이야기하지 않았지만 물론이 이유가 올바른 경우가있을 수 있습니다. 그리고 다른 언어의 JRE 용 다른 컴파일러가있을 수 있기 때문에 런타임 도 이러한 최적화를 수행 해야 하므로 런타임과 컴파일러 모두에서 수행 할 이유가 없습니다.
IllidanS4는 Monica가

6
@ IllidanS4 모든 런타임 최적화는 0보다 적은 시간이 소요될 수 없습니다. 컴파일러가 코드를 제거하지 못하도록하는 것은 의미가 없습니다.
Gerhardh

55

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

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

나는 OP의 코드를 class MyClass.


2
기묘한. 내가 코드를 두 가지를 실행할 때 입니다 빠른 플래그없이 만 10 배, 추가하거나 또한 루프의 반복 횟수에 0을 제거하기로하고,하지 않고 10의 요인에 의해 실행 시간에 영향을 미칩니다 깃발. 그래서 (저에게) 루프는 완전히 최적화되지 않은 것 같습니다. 어떻게 든 10 배 더 빨라졌습니다. (Oracle Java 8-151)
tobias_k

@tobias_k 루프가 통과하는 JIT의 단계에 따라 다릅니다. stackoverflow.com/a/47972226/1059372
Eugene

21

나는 단지 명백한 것을 말할 것이다. 이것은 일어나는 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 

13

이미 지적했듯이 JIT (just-in-time) 컴파일러는 불필요한 반복을 제거하기 위해 빈 루프를 최적화 할 수 있습니다. 하지만 어떻게?

실제로 두 가지 JIT 컴파일러가 있습니다. C1C2 . 첫째, 코드는 C1으로 컴파일됩니다. C1은 통계를 수집하고 JVM이 100 % 경우 빈 루프가 아무것도 변경하지 않고 쓸모 없다는 것을 발견하도록 도와줍니다. 이 상황에서 C2는 무대에 들어갑니다. 코드가 자주 호출되면 수집 된 통계를 사용하여 C2로 최적화 및 컴파일 할 수 있습니다.

예를 들어 다음 코드 스 니펫을 테스트하겠습니다 (내 JDK는 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!");
    }
}

다음 명령 줄 옵션 사용 :

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

그리고 C1과 C2로 적절하게 컴파일 된 다른 버전의 실행 방법이 있습니다. 저에게 최종 변형 (C2)은 다음과 같습니다.

...

; 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

...

조금 지저분하지만 자세히 살펴보면 여기에 오래 실행되는 루프가 없다는 것을 알 수 있습니다. B1, B2 및 B3의 3 개 블록이 있으며 실행 단계는 B1 -> B2 -> B3또는 일 수 있습니다 B1 -> B3. 어디서 Freq: 1-블록 실행의 정규화 된 예상 빈도.


8

루프가 아무것도하지 않는 것을 감지하고 백그라운드 스레드에서 코드를 컴파일하고 코드를 제거하는 데 걸리는 시간을 측정하고 있습니다.

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

이것을 실행 -XX:+PrintCompilation하면 코드가 백그라운드에서 레벨 3 또는 C1 컴파일러로 컴파일되고 몇 번의 루프 후에 C4의 레벨 4로 컴파일 된 것을 볼 수 있습니다.

    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

루프를 사용하여 변경하면 long최적화되지 않습니다.

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

대신 당신은 얻는다

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

이상합니다. long카운터가 동일한 최적화가 발생하는 것을 막는 이유는 무엇입니까?
Ryan Amos

@RyanAmos 최적화는 유형 intnote char 및 short가 바이트 코드 수준에서 효과적으로 동일한 경우 공통 기본 루프 수에만 적용됩니다 .
Peter Lawrey

-1

시작 및 완료 시간을 나노초 단위로 고려하고 대기 시간을 계산하기 위해 10 ^ 6으로 나눕니다.

long d = (finish - start) / 1000000

초 = 나노초 10^9이기 때문 이어야합니다 .110^9


당신이 제안한 것은 내 요점과 관련이 없습니다. 내가 궁금했던 것은 시간이 얼마나 걸리는지이며이 기간이 밀리 초 또는 초 단위로 인쇄 / 표현되는지 여부는 중요하지 않습니다.
twimo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.