Java에서 2 * (i * i)가 2 * i * i보다 빠른 이유는 무엇입니까?
다음 Java 프로그램은 실행하는 데 평균 0.50 초에서 0.55 초가 걸립니다.
public static void main(String[] args) {
long startTime = System.nanoTime();
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += 2 * (i * i);
}
System.out.println((double) (System.nanoTime() - startTime) / 1000000000 + " s");
System.out.println("n = " + n);
}
내가 바꿀 경우 2 * (i * i)
에 2 * i * i
, 그것을 실행하는 0.60 사이에 0.65 초 걸립니다. 어째서?
프로그램의 각 버전을 15 번씩 번갈아 실행했습니다. 결과는 다음과 같습니다.
2*(i*i) | 2*i*i
----------+----------
0.5183738 | 0.6246434
0.5298337 | 0.6049722
0.5308647 | 0.6603363
0.5133458 | 0.6243328
0.5003011 | 0.6541802
0.5366181 | 0.6312638
0.515149 | 0.6241105
0.5237389 | 0.627815
0.5249942 | 0.6114252
0.5641624 | 0.6781033
0.538412 | 0.6393969
0.5466744 | 0.6608845
0.531159 | 0.6201077
0.5048032 | 0.6511559
0.5232789 | 0.6544526
의 가장 빠른 실행은 2 * i * i
의 가장 느린 실행보다 오래 걸렸습니다 2 * (i * i)
. 둘 다 효율적이라면 이런 일이 발생할 확률은 1 / 2 ^ 15 * 100 % = 0.00305 % 미만입니다.
바이트 코드의 순서에 약간의 차이가 있습니다.
2 * (i * i)
:
iconst_2
iload0
iload0
imul
imul
iadd
대 2 * i * i
:
iconst_2
iload0
imul
iload0
imul
iadd
첫눈에 이것은 차이를 만들지 않아야합니다. 두 번째 버전이 하나의 슬롯을 덜 사용하기 때문에 더 최적입니다.
따라서 우리는 하위 수준 (JIT) 1을 더 깊이 파헤쳐 야합니다 .
JIT는 작은 루프를 매우 공격적으로 펼치는 경향이 있음을 기억하십시오. 실제로 우리는 2 * (i * i)
케이스에 대해 16 배 풀림을 관찰합니다 .
030 B2: # B2 B3 <- B1 B2 Loop: B2-B2 inner main of N18 Freq: 1e+006
030 addl R11, RBP # int
033 movl RBP, R13 # spill
036 addl RBP, #14 # int
039 imull RBP, RBP # int
03c movl R9, R13 # spill
03f addl R9, #13 # int
043 imull R9, R9 # int
047 sall RBP, #1
049 sall R9, #1
04c movl R8, R13 # spill
04f addl R8, #15 # int
053 movl R10, R8 # spill
056 movdl XMM1, R8 # spill
05b imull R10, R8 # int
05f movl R8, R13 # spill
062 addl R8, #12 # int
066 imull R8, R8 # int
06a sall R10, #1
06d movl [rsp + #32], R10 # spill
072 sall R8, #1
075 movl RBX, R13 # spill
078 addl RBX, #11 # int
07b imull RBX, RBX # int
07e movl RCX, R13 # spill
081 addl RCX, #10 # int
084 imull RCX, RCX # int
087 sall RBX, #1
089 sall RCX, #1
08b movl RDX, R13 # spill
08e addl RDX, #8 # int
091 imull RDX, RDX # int
094 movl RDI, R13 # spill
097 addl RDI, #7 # int
09a imull RDI, RDI # int
09d sall RDX, #1
09f sall RDI, #1
0a1 movl RAX, R13 # spill
0a4 addl RAX, #6 # int
0a7 imull RAX, RAX # int
0aa movl RSI, R13 # spill
0ad addl RSI, #4 # int
0b0 imull RSI, RSI # int
0b3 sall RAX, #1
0b5 sall RSI, #1
0b7 movl R10, R13 # spill
0ba addl R10, #2 # int
0be imull R10, R10 # int
0c2 movl R14, R13 # spill
0c5 incl R14 # int
0c8 imull R14, R14 # int
0cc sall R10, #1
0cf sall R14, #1
0d2 addl R14, R11 # int
0d5 addl R14, R10 # int
0d8 movl R10, R13 # spill
0db addl R10, #3 # int
0df imull R10, R10 # int
0e3 movl R11, R13 # spill
0e6 addl R11, #5 # int
0ea imull R11, R11 # int
0ee sall R10, #1
0f1 addl R10, R14 # int
0f4 addl R10, RSI # int
0f7 sall R11, #1
0fa addl R11, R10 # int
0fd addl R11, RAX # int
100 addl R11, RDI # int
103 addl R11, RDX # int
106 movl R10, R13 # spill
109 addl R10, #9 # int
10d imull R10, R10 # int
111 sall R10, #1
114 addl R10, R11 # int
117 addl R10, RCX # int
11a addl R10, RBX # int
11d addl R10, R8 # int
120 addl R9, R10 # int
123 addl RBP, R9 # int
126 addl RBP, [RSP + #32 (32-bit)] # int
12a addl R13, #16 # int
12e movl R11, R13 # spill
131 imull R11, R13 # int
135 sall R11, #1
138 cmpl R13, #999999985
13f jl B2 # loop end P=1.000000 C=6554623.000000
스택에 "흘린"레지스터가 1 개 있음을 알 수 있습니다.
그리고 2 * i * i
버전 :
05a B3: # B2 B4 <- B1 B2 Loop: B3-B2 inner main of N18 Freq: 1e+006
05a addl RBX, R11 # int
05d movl [rsp + #32], RBX # spill
061 movl R11, R8 # spill
064 addl R11, #15 # int
068 movl [rsp + #36], R11 # spill
06d movl R11, R8 # spill
070 addl R11, #14 # int
074 movl R10, R9 # spill
077 addl R10, #16 # int
07b movdl XMM2, R10 # spill
080 movl RCX, R9 # spill
083 addl RCX, #14 # int
086 movdl XMM1, RCX # spill
08a movl R10, R9 # spill
08d addl R10, #12 # int
091 movdl XMM4, R10 # spill
096 movl RCX, R9 # spill
099 addl RCX, #10 # int
09c movdl XMM6, RCX # spill
0a0 movl RBX, R9 # spill
0a3 addl RBX, #8 # int
0a6 movl RCX, R9 # spill
0a9 addl RCX, #6 # int
0ac movl RDX, R9 # spill
0af addl RDX, #4 # int
0b2 addl R9, #2 # int
0b6 movl R10, R14 # spill
0b9 addl R10, #22 # int
0bd movdl XMM3, R10 # spill
0c2 movl RDI, R14 # spill
0c5 addl RDI, #20 # int
0c8 movl RAX, R14 # spill
0cb addl RAX, #32 # int
0ce movl RSI, R14 # spill
0d1 addl RSI, #18 # int
0d4 movl R13, R14 # spill
0d7 addl R13, #24 # int
0db movl R10, R14 # spill
0de addl R10, #26 # int
0e2 movl [rsp + #40], R10 # spill
0e7 movl RBP, R14 # spill
0ea addl RBP, #28 # int
0ed imull RBP, R11 # int
0f1 addl R14, #30 # int
0f5 imull R14, [RSP + #36 (32-bit)] # int
0fb movl R10, R8 # spill
0fe addl R10, #11 # int
102 movdl R11, XMM3 # spill
107 imull R11, R10 # int
10b movl [rsp + #44], R11 # spill
110 movl R10, R8 # spill
113 addl R10, #10 # int
117 imull RDI, R10 # int
11b movl R11, R8 # spill
11e addl R11, #8 # int
122 movdl R10, XMM2 # spill
127 imull R10, R11 # int
12b movl [rsp + #48], R10 # spill
130 movl R10, R8 # spill
133 addl R10, #7 # int
137 movdl R11, XMM1 # spill
13c imull R11, R10 # int
140 movl [rsp + #52], R11 # spill
145 movl R11, R8 # spill
148 addl R11, #6 # int
14c movdl R10, XMM4 # spill
151 imull R10, R11 # int
155 movl [rsp + #56], R10 # spill
15a movl R10, R8 # spill
15d addl R10, #5 # int
161 movdl R11, XMM6 # spill
166 imull R11, R10 # int
16a movl [rsp + #60], R11 # spill
16f movl R11, R8 # spill
172 addl R11, #4 # int
176 imull RBX, R11 # int
17a movl R11, R8 # spill
17d addl R11, #3 # int
181 imull RCX, R11 # int
185 movl R10, R8 # spill
188 addl R10, #2 # int
18c imull RDX, R10 # int
190 movl R11, R8 # spill
193 incl R11 # int
196 imull R9, R11 # int
19a addl R9, [RSP + #32 (32-bit)] # int
19f addl R9, RDX # int
1a2 addl R9, RCX # int
1a5 addl R9, RBX # int
1a8 addl R9, [RSP + #60 (32-bit)] # int
1ad addl R9, [RSP + #56 (32-bit)] # int
1b2 addl R9, [RSP + #52 (32-bit)] # int
1b7 addl R9, [RSP + #48 (32-bit)] # int
1bc movl R10, R8 # spill
1bf addl R10, #9 # int
1c3 imull R10, RSI # int
1c7 addl R10, R9 # int
1ca addl R10, RDI # int
1cd addl R10, [RSP + #44 (32-bit)] # int
1d2 movl R11, R8 # spill
1d5 addl R11, #12 # int
1d9 imull R13, R11 # int
1dd addl R13, R10 # int
1e0 movl R10, R8 # spill
1e3 addl R10, #13 # int
1e7 imull R10, [RSP + #40 (32-bit)] # int
1ed addl R10, R13 # int
1f0 addl RBP, R10 # int
1f3 addl R14, RBP # int
1f6 movl R10, R8 # spill
1f9 addl R10, #16 # int
1fd cmpl R10, #999999985
204 jl B2 # loop end P=1.000000 C=7419903.000000
여기서 우리는 [RSP + ...]
보존해야 할 더 많은 중간 결과로 인해 훨씬 더 많은 "유출"과 스택에 대한 더 많은 액세스를 관찰 합니다.
따라서 질문에 대한 대답은 간단 2 * (i * i)
하다보다 빠른 2 * i * i
JIT를이 첫 번째 경우에 대한 더 많은 최적의 어셈블리 코드를 생성하기 때문이다.
그러나 물론 첫 번째 버전도 두 번째 버전도 좋지 않다는 것은 분명합니다. x86-64 CPU는 최소한 SSE2를 지원하기 때문에 루프는 벡터화의 이점을 얻을 수 있습니다.
그래서 이것은 옵티마이 저의 문제입니다. 종종 그렇듯이 너무 공격적으로 펴서 발을 쏘아 올리면서 다양한 다른 기회를 놓치게됩니다.
실제로 최신 x86-64 CPU는 명령어를 마이크로 연산 (µop)으로 더 세분화하고 레지스터 이름 변경, µop 캐시 및 루프 버퍼와 같은 기능을 사용하여 루프 최적화는 최적의 성능을 위해 간단한 풀기보다 훨씬 더 세밀합니다. Agner Fog의 최적화 가이드에 따르면 :
평균 명령어 길이가 4 바이트 이상인 경우 µop 캐시로 인한 성능 향상은 상당히 클 수 있습니다. µop 캐시 사용을 최적화하는 다음 방법을 고려할 수 있습니다.
- 중요 루프가 µop 캐시에 들어갈만큼 충분히 작은 지 확인하십시오.
- 가장 중요한 루프 항목과 기능 항목을 32로 정렬합니다.
- 불필요한 루프 풀기를 피하십시오.
- 추가로드 시간이있는 지침은 피하십시오
. . .
로드 시간과 관련하여- 가장 빠른 L1D 적중도 4주기 , 추가 레지스터 및 µop 비용이 들기 때문에 메모리에 몇 번 액세스해도 타이트 루프에서 성능이 저하됩니다.
그러나 벡터화 기회로 돌아가서, 얼마나 빠른지 확인하기 위해 GCC로 유사한 C 애플리케이션을 컴파일 할 수 있습니다.이 애플리케이션은 이를 완전히 벡터화합니다 (AVX2가 표시되고 SSE2가 유사 함) 2 :
vmovdqa ymm0, YMMWORD PTR .LC0[rip]
vmovdqa ymm3, YMMWORD PTR .LC1[rip]
xor eax, eax
vpxor xmm2, xmm2, xmm2
.L2:
vpmulld ymm1, ymm0, ymm0
inc eax
vpaddd ymm0, ymm0, ymm3
vpslld ymm1, ymm1, 1
vpaddd ymm2, ymm2, ymm1
cmp eax, 125000000 ; 8 calculations per iteration
jne .L2
vmovdqa xmm0, xmm2
vextracti128 xmm2, ymm2, 1
vpaddd xmm2, xmm0, xmm2
vpsrldq xmm0, xmm2, 8
vpaddd xmm0, xmm2, xmm0
vpsrldq xmm1, xmm0, 4
vpaddd xmm0, xmm0, xmm1
vmovd eax, xmm0
vzeroupper
실행 시간 :
- SSE : 0.24 초 또는 2 배 빠름.
- AVX : 0.15 초 또는 3 배 빠름.
- AVX2 : 0.08 초 또는 5 배 빠름.
1 , JIT 생성 된 어셈블리 출력을 얻으려면 디버그 JVM를 얻을 수 및 실행-XX:+PrintOptoAssembly
2 C 버전은 -fwrapv
플래그 로 컴파일되어 GCC에서 부호있는 정수 오버플로를 2의 보수 랩 어라운드로 처리 할 수 있습니다.
곱셈이 2 * (i * i)
인 경우 JVM은 2
루프에서 곱셈을 제거 할 수 있으므로 동등하지만 더 효율적인 코드가 생성됩니다.
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += i * i;
}
n *= 2;
그러나 곱셈이 (2 * i) * i
이면 JVM은 상수에 의한 곱셈이 더 이상 덧셈 바로 직전에 없기 때문에이를 최적화하지 않습니다.
이것이 사실이라고 생각하는 몇 가지 이유는 다음과 같습니다.
if (n == 0) n = 1
루프 시작 부분에 문을 추가하면 곱셈을 분해해도 결과가 동일하다는 것을 더 이상 보장하지 않으므로 두 버전 모두 효율적입니다.- 최적화 된 버전 (2의 곱셈을 고려하여)은
2 * (i * i)
버전 만큼 빠릅니다.
이러한 결론을 도출하는 데 사용한 테스트 코드는 다음과 같습니다.
public static void main(String[] args) {
long fastVersion = 0;
long slowVersion = 0;
long optimizedVersion = 0;
long modifiedFastVersion = 0;
long modifiedSlowVersion = 0;
for (int i = 0; i < 10; i++) {
fastVersion += fastVersion();
slowVersion += slowVersion();
optimizedVersion += optimizedVersion();
modifiedFastVersion += modifiedFastVersion();
modifiedSlowVersion += modifiedSlowVersion();
}
System.out.println("Fast version: " + (double) fastVersion / 1000000000 + " s");
System.out.println("Slow version: " + (double) slowVersion / 1000000000 + " s");
System.out.println("Optimized version: " + (double) optimizedVersion / 1000000000 + " s");
System.out.println("Modified fast version: " + (double) modifiedFastVersion / 1000000000 + " s");
System.out.println("Modified slow version: " + (double) modifiedSlowVersion / 1000000000 + " s");
}
private static long fastVersion() {
long startTime = System.nanoTime();
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += 2 * (i * i);
}
return System.nanoTime() - startTime;
}
private static long slowVersion() {
long startTime = System.nanoTime();
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += 2 * i * i;
}
return System.nanoTime() - startTime;
}
private static long optimizedVersion() {
long startTime = System.nanoTime();
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += i * i;
}
n *= 2;
return System.nanoTime() - startTime;
}
private static long modifiedFastVersion() {
long startTime = System.nanoTime();
int n = 0;
for (int i = 0; i < 1000000000; i++) {
if (n == 0) n = 1;
n += 2 * (i * i);
}
return System.nanoTime() - startTime;
}
private static long modifiedSlowVersion() {
long startTime = System.nanoTime();
int n = 0;
for (int i = 0; i < 1000000000; i++) {
if (n == 0) n = 1;
n += 2 * i * i;
}
return System.nanoTime() - startTime;
}
결과는 다음과 같습니다.
Fast version: 5.7274411 s
Slow version: 7.6190804 s
Optimized version: 5.1348007 s
Modified fast version: 7.1492705 s
Modified slow version: 7.2952668 s
바이트 코드 : https://cs.nyu.edu/courses/fall00/V22.0201-001/jvm2.html 바이트 코드 뷰어 : https://github.com/Konloch/bytecode-viewer
내 JDK (Windows 10 64 비트, 1.8.0_65-b17)에서 재현하고 설명 할 수 있습니다.
public static void main(String[] args) {
int repeat = 10;
long A = 0;
long B = 0;
for (int i = 0; i < repeat; i++) {
A += test();
B += testB();
}
System.out.println(A / repeat + " ms");
System.out.println(B / repeat + " ms");
}
private static long test() {
int n = 0;
for (int i = 0; i < 1000; i++) {
n += multi(i);
}
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
n += multi(i);
}
long ms = (System.currentTimeMillis() - startTime);
System.out.println(ms + " ms A " + n);
return ms;
}
private static long testB() {
int n = 0;
for (int i = 0; i < 1000; i++) {
n += multiB(i);
}
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
n += multiB(i);
}
long ms = (System.currentTimeMillis() - startTime);
System.out.println(ms + " ms B " + n);
return ms;
}
private static int multiB(int i) {
return 2 * (i * i);
}
private static int multi(int i) {
return 2 * i * i;
}
산출:
...
405 ms A 785527736
327 ms B 785527736
404 ms A 785527736
329 ms B 785527736
404 ms A 785527736
328 ms B 785527736
404 ms A 785527736
328 ms B 785527736
410 ms
333 ms
왜? 바이트 코드는 다음과 같습니다.
private static multiB(int arg0) { // 2 * (i * i)
<localVar:index=0, name=i , desc=I, sig=null, start=L1, end=L2>
L1 {
iconst_2
iload0
iload0
imul
imul
ireturn
}
L2 {
}
}
private static multi(int arg0) { // 2 * i * i
<localVar:index=0, name=i , desc=I, sig=null, start=L1, end=L2>
L1 {
iconst_2
iload0
imul
iload0
imul
ireturn
}
L2 {
}
}
차이점은 다음과 같습니다. 대괄호 사용 ( 2 * (i * i)
) :
- const 스택 푸시
- 스택에 로컬 푸시
- 스택에 로컬 푸시
- 스택 상단을 곱하십시오
- 스택 상단을 곱하십시오
대괄호 제외 ( 2 * i * i
) :
- const 스택 푸시
- 스택에 로컬 푸시
- 스택 상단을 곱하십시오
- 스택에 로컬 푸시
- 스택 상단을 곱하십시오
스택에 모두로드 한 다음 다시 작업하는 것이 스택에 넣고 작업하는 것 사이를 전환하는 것보다 빠릅니다.
Kasperd 는 수락 된 답변에 대해 다음과 같이 질문했습니다.
Java 및 C 예제는 완전히 다른 레지스터 이름을 사용합니다. 두 예 모두 AMD64 ISA를 사용하고 있습니까?
xor edx, edx
xor eax, eax
.L2:
mov ecx, edx
imul ecx, edx
add edx, 1
lea eax, [rax+rcx*2]
cmp edx, 1000000000
jne .L2
나는 의견에 이것에 답할만큼 충분한 평판을 가지고 있지 않지만 이들은 동일한 ISA입니다. GCC 버전은 32 비트 정수 로직을 사용하고 JVM 컴파일 버전은 내부적으로 64 비트 정수 로직을 사용한다는 점을 지적 할 가치가 있습니다.
R8 ~ R15는 새로운 X86_64 레지스터 입니다. EAX to EDX는 RAX to RDX 범용 레지스터의 하단 부분입니다. 대답에서 중요한 부분은 GCC 버전이 풀리지 않는다는 것입니다. 단순히 실제 기계어 코드 루프 당 한 라운드의 루프를 실행합니다. JVM 버전에는 하나의 물리적 루프에서 16 라운드의 루프가 있지만 rustyx 답변을 기반으로 어셈블리를 재 해석하지 않았습니다. 이것은 루프 본문이 실제로 16 배 더 길기 때문에 더 많은 레지스터가 사용되는 이유 중 하나입니다.
질문의 환경과 직접적인 관련이 없지만 호기심을 위해 .NET Core 2.1, x64, 릴리스 모드에서 동일한 테스트를 수행했습니다.
여기에 흥미로운 결과가 있는데, 힘의 어두운면에서 유사한 음운 (다른 방법으로)이 일어나고 있음을 확인합니다. 암호:
static void Main(string[] args)
{
Stopwatch watch = new Stopwatch();
Console.WriteLine("2 * (i * i)");
for (int a = 0; a < 10; a++)
{
int n = 0;
watch.Restart();
for (int i = 0; i < 1000000000; i++)
{
n += 2 * (i * i);
}
watch.Stop();
Console.WriteLine($"result:{n}, {watch.ElapsedMilliseconds} ms");
}
Console.WriteLine();
Console.WriteLine("2 * i * i");
for (int a = 0; a < 10; a++)
{
int n = 0;
watch.Restart();
for (int i = 0; i < 1000000000; i++)
{
n += 2 * i * i;
}
watch.Stop();
Console.WriteLine($"result:{n}, {watch.ElapsedMilliseconds}ms");
}
}
결과:
2 * (i * i)
- 결과 : 119860736, 438ms
- 결과 : 119860736, 433ms
- 결과 : 119860736, 437ms
- 결과 : 119860736, 435ms
- 결과 : 119860736, 436ms
- 결과 : 119860736, 435ms
- 결과 : 119860736, 435ms
- 결과 : 119860736, 439ms
- 결과 : 119860736, 436ms
- 결과 : 119860736, 437ms
2 * 나는 * 나는
- 결과 : 119860736, 417ms
- 결과 : 119860736, 417ms
- 결과 : 119860736, 417ms
- 결과 : 119860736, 418ms
- 결과 : 119860736, 418ms
- 결과 : 119860736, 417ms
- 결과 : 119860736, 418ms
- 결과 : 119860736, 416ms
- 결과 : 119860736, 417ms
- 결과 : 119860736, 418ms
비슷한 결과를 얻었습니다.
2 * (i * i): 0.458765943 s, n=119860736
2 * i * i: 0.580255126 s, n=119860736
두 루프가 동일한 프로그램에 있거나 각각 별도의 실행에서 실행되는 별도의 .java 파일 /.class에있는 경우 동일한 결과를 얻었습니다 .
마지막으로 다음은 javap -c -v <.java>
각각 의 디 컴파일입니다.
3: ldc #3 // String 2 * (i * i):
5: invokevirtual #4 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: invokestatic #5 // Method java/lang/System.nanoTime:()J
8: invokestatic #5 // Method java/lang/System.nanoTime:()J
11: lstore_1
12: iconst_0
13: istore_3
14: iconst_0
15: istore 4
17: iload 4
19: ldc #6 // int 1000000000
21: if_icmpge 40
24: iload_3
25: iconst_2
26: iload 4
28: iload 4
30: imul
31: imul
32: iadd
33: istore_3
34: iinc 4, 1
37: goto 17
대
3: ldc #3 // String 2 * i * i:
5: invokevirtual #4 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: invokestatic #5 // Method java/lang/System.nanoTime:()J
11: lstore_1
12: iconst_0
13: istore_3
14: iconst_0
15: istore 4
17: iload 4
19: ldc #6 // int 1000000000
21: if_icmpge 40
24: iload_3
25: iconst_2
26: iload 4
28: imul
29: iload 4
31: imul
32: iadd
33: istore_3
34: iinc 4, 1
37: goto 17
참고로-
java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
기본 아키타 입을 사용하여 JMH를 시도했습니다. 또한 Runemoro의 설명 에 따라 최적화 된 버전을 추가했습니다 .
@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Fork(1)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
//@BenchmarkMode({ Mode.All })
@BenchmarkMode(Mode.AverageTime)
public class MyBenchmark {
@Param({ "100", "1000", "1000000000" })
private int size;
@Benchmark
public int two_square_i() {
int n = 0;
for (int i = 0; i < size; i++) {
n += 2 * (i * i);
}
return n;
}
@Benchmark
public int square_i_two() {
int n = 0;
for (int i = 0; i < size; i++) {
n += i * i;
}
return 2*n;
}
@Benchmark
public int two_i_() {
int n = 0;
for (int i = 0; i < size; i++) {
n += 2 * i * i;
}
return n;
}
}
결과는 다음과 같습니다.
Benchmark (size) Mode Samples Score Score error Units
o.s.MyBenchmark.square_i_two 100 avgt 10 58,062 1,410 ns/op
o.s.MyBenchmark.square_i_two 1000 avgt 10 547,393 12,851 ns/op
o.s.MyBenchmark.square_i_two 1000000000 avgt 10 540343681,267 16795210,324 ns/op
o.s.MyBenchmark.two_i_ 100 avgt 10 87,491 2,004 ns/op
o.s.MyBenchmark.two_i_ 1000 avgt 10 1015,388 30,313 ns/op
o.s.MyBenchmark.two_i_ 1000000000 avgt 10 967100076,600 24929570,556 ns/op
o.s.MyBenchmark.two_square_i 100 avgt 10 70,715 2,107 ns/op
o.s.MyBenchmark.two_square_i 1000 avgt 10 686,977 24,613 ns/op
o.s.MyBenchmark.two_square_i 1000000000 avgt 10 652736811,450 27015580,488 ns/op
내 PC에서 ( Core i7 860-내 스마트 폰에서 읽는 것 외에는 아무것도하지 않음) :
n += i*i
그럼n*2
먼저2 * (i * i)
두 번째입니다.
JVM은 분명히 인간과 같은 방식으로 최적화되지 않습니다 (Runemoro의 답변에 근거).
이제 바이트 코드를 읽습니다. javap -c -v ./target/classes/org/sample/MyBenchmark.class
- 2 * (i * i) (왼쪽)과 2 * i * i (오른쪽)의 차이점 : https://www.diffchecker.com/cvSFppWI
- 2 * (i * i)와 최적화 된 버전의 차이점 : https://www.diffchecker.com/I1XFu5dP
나는 바이트 코드에 대한 전문가 아니지만, 우리는 iload_2
전에 우리 imul
내가 읽는 JVM을 최적화한다고 가정 할 수 있습니다 : 당신이 차이를 얻을 곳은 아마 i
두 번이 ( i
이미 여기에, 다시로드 할 필요가 없다)가에 동안 2*i*i
'이 수 티.
Java 11을 사용한 흥미로운 관찰 과 다음 VM 옵션을 사용하여 루프 풀기 해제 :
-XX:LoopUnrollLimit=0
2 * (i * i)
표현식 이 포함 된 루프 는 더 간결한 기본 코드 1이됩니다 .
L0001: add eax,r11d
inc r8d
mov r11d,r8d
imul r11d,r8d
shl r11d,1h
cmp r8d,r10d
jl L0001
2 * i * i
버전 과 비교 :
L0001: add eax,r11d
mov r11d,r8d
shl r11d,1h
add r11d,2h
inc r8d
imul r11d,r8d
cmp r8d,r10d
jl L0001
자바 버전 :
java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)
벤치 마크 결과 :
Benchmark (size) Mode Cnt Score Error Units
LoopTest.fast 1000000000 avgt 5 694,868 ± 36,470 ms/op
LoopTest.slow 1000000000 avgt 5 769,840 ± 135,006 ms/op
벤치 마크 소스 코드 :
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
@Fork(1)
public class LoopTest {
@Param("1000000000") private int size;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(LoopTest.class.getSimpleName())
.jvmArgs("-XX:LoopUnrollLimit=0")
.build();
new Runner(opt).run();
}
@Benchmark
public int slow() {
int n = 0;
for (int i = 0; i < size; i++)
n += 2 * i * i;
return n;
}
@Benchmark
public int fast() {
int n = 0;
for (int i = 0; i < size; i++)
n += 2 * (i * i);
return n;
}
}
1-사용 된 VM 옵션 : -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:LoopUnrollLimit=0
더 많은 부록. IBM의 최신 Java 8 JVM을 사용하여 실험을 재현했습니다.
java version "1.8.0_191"
Java(TM) 2 Runtime Environment, Standard Edition (IBM build 1.8.0_191-b12 26_Oct_2018_18_45 Mac OS X x64(SR5 FP25))
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
그리고 이것은 매우 유사한 결과를 보여줍니다.
0.374653912 s
n = 119860736
0.447778698 s
n = 119860736
(2 * i * i를 사용한 두 번째 결과).
흥미롭게도 동일한 시스템에서 실행되지만 Oracle Java를 사용하는 경우 :
Java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
결과는 평균적으로 약간 느립니다.
0.414331815 s
n = 119860736
0.491430656 s
n = 119860736
간단히 말해서, JIT 구현 내의 미묘한 차이가 눈에 띄는 영향을 미칠 수 있으므로 HotSpot의 부 버전 번호도 중요합니다.
추가하는 두 가지 방법은 약간 다른 바이트 코드를 생성합니다.
17: iconst_2
18: iload 4
20: iload 4
22: imul
23: imul
24: iadd
대한 2 * (i * i)
대 :
17: iconst_2
18: iload 4
20: imul
21: iload 4
23: imul
24: iadd
들어 2 * i * i
.
다음과 같은 JMH 벤치 마크를 사용할 때 :
@Warmup(iterations = 5, batchSize = 1)
@Measurement(iterations = 5, batchSize = 1)
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class MyBenchmark {
@Benchmark
public int noBrackets() {
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += 2 * i * i;
}
return n;
}
@Benchmark
public int brackets() {
int n = 0;
for (int i = 0; i < 1000000000; i++) {
n += 2 * (i * i);
}
return n;
}
}
차이점은 분명합니다.
# JMH version: 1.21
# VM version: JDK 11, Java HotSpot(TM) 64-Bit Server VM, 11+28
# VM options: <none>
Benchmark (n) Mode Cnt Score Error Units
MyBenchmark.brackets 1000000000 avgt 5 380.889 ± 58.011 ms/op
MyBenchmark.noBrackets 1000000000 avgt 5 512.464 ± 11.098 ms/op
관찰 한 내용은 정확하며 벤치마킹 스타일의 이상이 아닙니다 (예 : 워밍업 없음, Java에서 올바른 마이크로 벤치 마크 작성 방법 참조 ).
Graal로 다시 실행 :
# JMH version: 1.21
# VM version: JDK 11, Java HotSpot(TM) 64-Bit Server VM, 11+28
# VM options: -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler
Benchmark (n) Mode Cnt Score Error Units
MyBenchmark.brackets 1000000000 avgt 5 335.100 ± 23.085 ms/op
MyBenchmark.noBrackets 1000000000 avgt 5 331.163 ± 50.670 ms/op
Graal이 전반적으로 더 나은 성능의 현대적인 컴파일러이기 때문에 결과가 훨씬 더 가깝다는 것을 알 수 있습니다.
따라서 이것은 실제로 JIT 컴파일러가 특정 코드 부분을 최적화 할 수있는 정도에 달려 있으며 반드시 논리적 이유가있는 것은 아닙니다.
참고 URL : https://stackoverflow.com/questions/53452713/why-is-2-ii-faster-than-2-ii-in-java
'Development Tip' 카테고리의 다른 글
파이썬에서 "명명 된 튜플"이란 무엇입니까? (0) | 2020.09.28 |
---|---|
sudo를 사용하여 쓰기 권한이없는 위치로 출력을 리디렉션하려면 어떻게해야합니까? (0) | 2020.09.28 |
이벤트 버블 링 및 캡처 란 무엇입니까? (0) | 2020.09.27 |
virtualenv와 함께 다른 Python 버전 사용 (0) | 2020.09.27 |
C ++ 17의 새로운 기능은 무엇입니까? (0) | 2020.09.27 |