실행되지 않은 코드가 주석 처리되면 Java 프로그램이 느리게 실행됩니다.
Java 프로그램 중 하나에서 이상한 동작을 관찰했습니다. 나는 행동을 복제 할 수있는 동안 가능한 한 코드를 강화하려고 노력했습니다. 아래에 전체 코드를 입력하십시오.
public class StrangeBehaviour {
static boolean recursionFlag = true;
public static void main(String[] args) {
long startTime = System.nanoTime();
for (int i = 0; i < 10000; i ++) {
functionA(6, 0);
}
long endTime = System.nanoTime();
System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
}
static boolean functionA(int recursionDepth, int recursionSwitch) {
if (recursionDepth == 0) { return true; }
return functionB(recursionDepth, recursionSwitch);
}
static boolean functionB(int recursionDepth, int recursionSwitch) {
for (int i = 0; i < 16; i++) {
if (StrangeBehaviour.recursionFlag) {
if (recursionSwitch == 0) {
if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
} else {
if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
}
} else {
// This block is never entered into.
// Yet commenting out one of the lines below makes the program run slower!
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
}
}
return false;
}
}
가지 기능 현관이 두 functionA()
있으며 functionB()
서로를 재귀 적으로 호출합니다. 두 함수 모두 recursionDepth
재귀 종료를 제어 하는 매개 변수를 사용합니다. 변경되지 않은 상태 로 최대 한 번 functionA()
호출 functionB()
합니다 recursionDepth
. functionB()
로 functionA()
16 번 호출합니다 recursionDepth - 1
. 재귀 때 종료 functionA()
로 호출 recursionDepth
의 0
.
functionB()
여러 System.out.println()
호출 이있는 코드 블록이 있습니다. 항목은 하지 않는 boolean recursionFlag
설정되고 true
프로그램 실행 중에 변경되지 않는 변수에 의해 제어가 블록은 입력되지 않습니다. 그러나 println()
호출 중 하나라도 주석 처리 하면 프로그램이 느리게 실행됩니다. 내 컴퓨터에서 실행 시간은 모든 println()
호출 이있는 경우 <0.2 초이고 호출 중 하나가 주석 처리 된 경우> 2 초입니다.
이 동작의 원인은 무엇입니까? 내 유일한 추측은 코드 블록의 길이 (또는 함수 호출 수 등)와 관련된 일련의 변수에 의해 트리거되는 순진한 컴파일러 최적화가 것입니다. 이것에 대한 더 많은 많은 것들을 주시면 감사하겠습니다!
편집 : JDK 1.8을 사용하고 있습니다.
완전한 대답은 k5_와 Tony의 대답의 조합입니다.
OP가 게시 한 코드는 벤치 마크를 수행하기 전에 HotSpot을 트리거하는 워밍업 루프를 생략합니다. 따라서 인쇄 문이 고유 할 때 10 배 (내 컴퓨터에서) 속도가 빨라지고, 핫스팟에서 바이트 코드를 CPU 명령으로 실행하는 데 된 시간과 CPU 명령의 실제 실행이 결합됩니다.
타이밍 루프 앞에 별도의 워밍업 루프를 추가하면 print 문을 사용하면 2.5 배의 속도 만 향상됩니다.
이는 HotSpot / JIT가 설명이 방법이 인라인 될 때 더 오래 걸리고 (Tony) 코드 실행이 더 오래 확실하다는 것을 나타냅니다.
public static void main(String[] args) {
// Added the following warmup loop before the timing loop
for (int i = 0; i < 50000; i++) {
functionA(6, 0);
}
long startTime = System.nanoTime();
for (int i = 0; i < 50000; i++) {
functionA(6, 0);
}
long endTime = System.nanoTime();
System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
}
주석 처리 된 코드는 인라인 처리 방법에 영향을줍니다. functionB가 길어 지거나 커지면 (더 많은 바이트 코드 급)
따라서 @ J3D1은 VMOptions를 사용하여 functionB ()에 대한 인라인을 수동으로 끌 수 있습니다.
-XX:CompileCommand=dontinline,com.jd.benchmarking.StrangeBehaviour::functionB
방송보다 더 짧은 함수로 지연을 제거합니다.
vm 옵션을 사용하면 인라인을 표시 할 수 있습니다. -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
더 큰 버전은 인라인 기능이 아닙니다 .B
@ 8 StrangeBehaviour::functionB (326 bytes) callee is too large
@ 21 StrangeBehaviour::functionA (12 bytes)
@ 8 StrangeBehaviour::functionB (326 bytes) callee is too large
@ 35 StrangeBehaviour::functionA (12 bytes)
@ 8 StrangeBehaviour::functionB (326 bytes) callee is too large
더 짧은 버전 버전은 functionB를 인라인 시도하여 몇 번 더 시도합니다.
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 21 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 35 StrangeBehaviour::functionA (12 bytes) recursive inlining is too deep
@ 35 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 21 StrangeBehaviour::functionA (12 bytes) recursive inlining is too deep
@ 35 StrangeBehaviour::functionA (12 bytes) recursive inlining is too deep
@ 21 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 35 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) recursive inlining is too deep
@ 35 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 21 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) recursive inlining is too deep
@ 35 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) recursive inlining is too deep
대부분 추측하지만 더 많은 인라인이있는 바이트 코드는 분기 예측 및 캐싱에 문제를 해결합니다.
@ k5_와 함께 있는데, 함수를 인라인할지 여부를 결정하는 임계 값이있는 것입니다. 그리고 JIT 컴파일러가 그것을 인라인하기로 결정하면 -XX:+PrintCompilation
쇼 와 같이 많은 작업과 시간이 필요합니다 .
task-id
158 32 3 so_test.StrangeBehaviour::functionB (326 bytes) made not entrant
159 35 3 java.lang.String::<init> (82 bytes)
160 36 s 1 java.util.Vector::size (5 bytes)
1878 37 % 3 so_test.StrangeBehaviour::main @ 6 (65 bytes)
1898 38 3 so_test.StrangeBehaviour::main (65 bytes)
2665 39 3 java.util.regex.Pattern::has (15 bytes)
2667 40 3 sun.misc.FDBigInteger::mult (64 bytes)
2668 41 3 sun.misc.FDBigInteger::<init> (30 bytes)
2668 42 3 sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
2.51 seconds elapsed.
상단은 주석이없는 정보이고, 다음은 메소드 크기를 326 바이트에서 318 바이트로 줄이는 주석이 있습니다. 그리고 출력의 열 1에 있는 작업 ID 가 후자의 경우 훨씬 더 커서 시간이 더 많이 존재하는 것을 알 수 있습니다.
task-id
126 35 4 so_test.StrangeBehaviour::functionA (12 bytes)
130 33 3 so_test.StrangeBehaviour::functionA (12 bytes) made not entrant
131 36 s 1 java.util.Vector::size (5 bytes)
14078 37 % 3 so_test.StrangeBehaviour::main @ 6 (65 bytes)
14296 38 3 so_test.StrangeBehaviour::main (65 bytes)
14296 39 % 4 so_test.StrangeBehaviour::functionB @ 2 (318 bytes)
14300 40 4 so_test.StrangeBehaviour::functionB (318 bytes)
14304 34 3 so_test.StrangeBehaviour::functionB (318 bytes) made not entrant
14628 41 3 java.util.regex.Pattern::has (15 bytes)
14631 42 3 sun.misc.FDBigInteger::mult (64 bytes)
14632 43 3 sun.misc.FDBigInteger::<init> (30 bytes)
14632 44 3 sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
14.50 seconds elapsed.
코드를 다음과 같이 변경 (두 줄을 추가하고 인쇄 줄을 commnet), 코드 크기가 326 바이트로 변경되고 이제 더 빨리 실행되는 것을 볼 수 있습니다.
if (StrangeBehaviour.recursionFlag) {
int a = 1;
int b = 1;
if (recursionSwitch == 0) {
if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
} else {
if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
}
} else {
// This block is never entered into.
// Yet commenting out one of the lines below makes the program run slower!
System.out.println("...");
System.out.println("...");
System.out.println("...");
//System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
}
새로운 시간 및 JIT 컴파일러 정보 :
140 34 3 so_test.StrangeBehaviour::functionB (326 bytes) made not entrant
145 36 3 java.lang.String::<init> (82 bytes)
148 37 s 1 java.util.Vector::size (5 bytes)
162 38 4 so_test.StrangeBehaviour::functionA (12 bytes)
163 33 3 so_test.StrangeBehaviour::functionA (12 bytes) made not entrant
1916 39 % 3 so_test.StrangeBehaviour::main @ 6 (65 bytes)
1936 40 3 so_test.StrangeBehaviour::main (65 bytes)
2686 41 3 java.util.regex.Pattern::has (15 bytes)
2689 42 3 sun.misc.FDBigInteger::mult (64 bytes)
2690 43 3 sun.misc.FDBigInteger::<init> (30 bytes)
2690 44 3 sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
2.55 seconds elapsed.
결론적으로 :
- 메소드 크기가 일부 제한을 초과하면 JIT는이 함수를 인라인하지 않습니다.
- 그리고 임계 값 아래로 축소되는 줄을 주석 처리하면 JIT는 인라인하기로 결정합니다.
- 이 함수를 인라인하면 많은 JIT 작업이 발생하여 프로그램 속도가 느려집니다.
업데이트 :
에 Accroding 내 최신 시험 ,이 질문에 대한 대답은 너무 쉽게되지 않습니다 :
내 코드 샘플에서 알 수 있듯이 일반 인라인 최적화는
- 프로그램 가속화
- 많은 컴파일러 작업 비용이 들지 않습니다 (내 테스트에서는 인라인이 발생할 때 작업 비용이 적게 듭니다).
그러나이 문제에서 코드는 많은 JIT 작업을 유발하고 JIT의 버그로 보이는 프로그램을 느리게 만듭니다. 그리고 그것이 왜 JIT의 많은 작업을 유발하는지는 아직 명확하지 않습니다.
'ProgramingTip' 카테고리의 다른 글
Android-CoordinatorLayout에서 사용하면 바닥 글이 화면 외 스크롤됩니다. (0) | 2020.12.25 |
---|---|
Android에서 ClipData의 "label"매개 변수는 정확히 무엇입니까? (0) | 2020.12.25 |
리터럴을 만드는 방법 : 클래스 (0) | 2020.12.25 |
std :: string을 UTF-8 텍스트 파일에 쓰는 방법 (0) | 2020.12.25 |
JavaScript : 좋은 부분 -`new`를 전혀 사용하지 않는 방법 (0) | 2020.12.25 |