ProgramingTip

실행되지 않은 코드가 주석 처리되면 Java 프로그램이 느리게 실행됩니다.

bestdevel 2020. 12. 25. 10:30
반응형

실행되지 않은 코드가 주석 처리되면 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()로 호출 recursionDepth0.

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.StrangeBeh‌​aviour::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의 많은 작업을 유발하는지는 아직 명확하지 않습니다.

참조 URL : https://stackoverflow.com/questions/41410743/java-program-runs-slower-when-code-that-is-never-executed-is-commented-out

반응형