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);
}
여기에는 두 가지 가능성 중 하나가 있습니다.
컴파일러는 루프가 중복되고 아무 것도하지 않으므로 루프를 최적화하지 않았다는 것을 깨달았습니다.
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
'program story' 카테고리의 다른 글
레일즈 : 레일에서 find (id)가 예외를 발생시키는 이유는 무엇입니까? (0) | 2020.07.25 |
---|---|
virtualenv를 복제하는 방법 (0) | 2020.07.25 |
목록의 가능한 모든 순열을 생성하는 알고리즘? (0) | 2020.07.25 |
Pandas의 부울 색인 생성을위한 논리 연산자 (0) | 2020.07.25 |
ESI 및 EDI 레지스터의 목적은 무엇입니까? (0) | 2020.07.25 |