it-swarm-ko.com

왜 2 * (i * i)가 Java에서 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 %보다 작을 것입니다.

781
Stefan

바이트 코드 순서에 약간의 차이가 있습니다.

2 * (i * i) :

     iconst_2
     iload0
     iload0
     imul
     imul
     iadd

vs 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 + ...]을 유지해야합니다. 더 많은 중간 결과가 보존되어야하기 때문입니다.

따라서 JIT는 첫 번째 경우에 대해보다 최적의 어셈블리 코드를 생성하므로 2 * (i * i)2 * i * i보다 빠릅니다.


그러나 물론 첫 번째 버전과 두 번째 버전이 좋은 것은 아닙니다. x86-64 CPU는 최소한 SSE2를 지원하므로 루프는 벡터화의 이점을 얻을 수 있습니다.

최적화의 문제입니다. 종종 그렇듯이 너무 공격적으로 풀리고 발에 쏠리면서 여러 다른 기회를 놓치게됩니다.

사실, 최신 x86-64 CPU는 명령어를 마이크로 옵스 (µops)로 세분화하고 레지스터 이름 변경, µ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 배 더 빠름.

1JIT 생성 어셈블리 출력을 얻으려면 디버그 JVM 가져 오기-XX:+PrintOptoAssembly

2C 버전은 -fwrapv 플래그로 컴파일되어 GCC가 부호있는 정수 오버플로를 2의 보완 랩으로 처리 할 수 ​​있습니다.

1127
rustyx

곱셈이 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
125
Runemoro

바이트 코드 : 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)) 포함 :

  • 푸시 스택
  • 스택에서 로컬 푸시
  • 스택에서 로컬 푸시
  • 스택 맨 위에 곱하기
  • 스택 맨 위에 곱하기

대괄호 (2 * i * i)없이 :

  • 푸시 스택
  • 스택에서 로컬 푸시
  • 스택 맨 위에 곱하기
  • 스택에서 로컬 푸시
  • 스택 맨 위에 곱하기

스택에 모든 내용을로드 한 다음 다시 작업하는 것은 스택에 놓고 조작하는 것보다 전환하는 것이 빠릅니다.

40
DSchmidt

Kasperd 허용 된 대답의 의견에 물었다 :

자바와 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에서 EDX는 RAX에서 RDX 범용 레지스터의 하위 부분입니다. 대답의 중요한 부분은 GCC 버전이 배포되지 않았다는 것입니다. 실제 기계 코드 루프 당 루프의 한 라운드 만 실행합니다. JVM 버전은 하나의 물리적 루프에 16 루프의 루프가 있지만 (rustyx 응답을 기반으로, 어셈블리를 재 해석하지 않았습니다). 이것은 루프 본문이 실제로 16 배 더 길기 때문에 더 많은 레지스터가 사용되는 이유 중 하나입니다.

34
Puzzled

호기심과 관련하여 질문의 환경과 직접적인 관련이 없지만 .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, 438 ms
  • 결과 : 119860736, 433 ms
  • 결과 : 119860736, 437 ms
  • 결과 : 119860736, 435 ms
  • 결과 : 119860736, 436 ms
  • 결과 : 119860736, 435 ms
  • 결과 : 119860736, 435 ms
  • 결과 : 119860736, 439 ms
  • 결과 : 119860736, 436 ms
  • 결과 : 119860736, 437 ms

2 * i * i

  • 결과 : 119860736, 417 ms
  • 결과 : 119860736, 417 ms
  • 결과 : 119860736, 417 ms
  • 결과 : 119860736, 418ms
  • 결과 : 119860736, 418ms
  • 결과 : 119860736, 417 ms
  • 결과 : 119860736, 418ms
  • 결과 : 119860736, 416 ms
  • 결과 : 119860736, 417 ms
  • 결과 : 119860736, 418ms
29
Ünsal Ersöz

비슷한 결과가 있습니다.

2 * (i * i): 0.458765943 s, n=119860736
2 * i * i: 0.580255126 s, n=119860736

두 루프가 같은 프로그램에 있거나 각각의 .Java 파일/.class에 별도의 실행에서 실행되는 경우SAMEresults가 있습니다.

마지막으로, 각각의 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

vs.

     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)
20
paulsm4

나는 기본 아키 타입을 사용하여 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

저는 바이트 코드에 대한 전문가는 아니지만 iload_2 전에 우리는 imulname__을 사용합니다. 차이점을 얻었을 것입니다. JVM이 i읽기를 두 번 최적화 (iname__은 이미 여기에 있으며 다시로드 할 필요가 없음) 할 수 있다고 가정 할 수 있습니다. 2*i*i 할 수 없습니다.

16
NoDataFound

부록 추가. 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

긴 이야기가 짧다 : HotSpot의 마이너 버전 번호조차도 JIT 구현 내 미묘한 차이가 눈에 띄는 영향을 미칠 수 있기 때문에.

13
GhostCat

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 버전 :

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

13
Oleksandr

추가하는 두 가지 방법은 약간 다른 바이트 코드를 생성합니다.

  17: iconst_2
  18: iload         4
  20: iload         4
  22: imul
  23: imul
  24: iadd

2 * (i * i) vs :

  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 컴파일러가 특정 코드 조각을 얼마나 잘 최적화 할 수 있는지에 달려 있으며 반드시 논리적 이유가있는 것은 아닙니다.

5
Jorn Vernee