동일한 방법에 대한 두 번의 연속 호출이 실행을 위해 다른 시간을 산출하는 이유는 무엇입니까?

StackOverflow https://stackoverflow.com/questions/804620

문제

다음은 샘플 코드입니다.

public class TestIO{
public static void main(String[] str){
    TestIO t = new TestIO();
    t.fOne();
    t.fTwo();
    t.fOne();
    t.fTwo();
}


public void fOne(){
    long t1, t2;
    t1 = System.nanoTime();
    int i = 10;
    int j = 10;
    int k = j*i;
    System.out.println(k);
    t2 = System.nanoTime();
    System.out.println("Time taken by 'fOne' ... " + (t2-t1));
}

public void fTwo(){
    long t1, t2;
    t1 = System.nanoTime();
    int i = 10;
    int j = 10;
    int k = j*i;
    System.out.println(k);
    t2 = System.nanoTime();
    System.out.println("Time taken by 'fTwo' ... " + (t2-t1));
}

}

이것은 다음과 같은 출력을 제공합니다. 계속하려면 키를 누르십시오. . .

연속 호출보다 처음으로 동일한 방법을 실행하는 데 시간이 더 걸리는 이유는 무엇입니까?

나는 기부를 시도했다 -XX:CompileThreshold=1000000 명령 줄에는 차이가 없었습니다.

도움이 되었습니까?

해결책

몇 가지 이유가 있습니다. JIT (제 시간에만) 컴파일러가 실행되지 않았을 수 있습니다. JVM은 호출간에 다른 최적화를 할 수 있습니다. 경과 시간을 측정하고 있으므로 Java 이외의 다른 것이 컴퓨터에서 실행 중일 것입니다. 프로세서와 램 캐시는 아마도 후속 호출에서 "따뜻한"것일 수 있습니다.

메소드 실행 시간당 정확한 실행 시간을 얻으려면 수천 개 (수천 개)를 만들어야합니다.

다른 팁

언급 된 문제 안드레아스 그리고 JIT의 예측 불가능 성은 사실이지만 여전히 한 가지 더 문제는 다음과 같습니다. 클래스 로더:

첫 번째 전화 fOne 첫 번째 호출이기 때문에 후자와 근본적으로 다릅니다. System.out.println, 이는 클래스 로더가 디스크 또는 파일 시스템 캐시 (보통 캐시)에서 텍스트를 인쇄하는 데 필요한 모든 클래스를 얻을 때입니다. 매개 변수를 제공하십시오 -verbose:class 이 소규모 프로그램 중에 실제로 얼마나 많은 클래스가로드되었는지 확인하기 위해 JVM에게.

단위 테스트를 실행할 때 비슷한 동작을 발견했습니다. 큰 프레임 워크를 호출하는 첫 번째 테스트는 테스트 코드가 동일하지만 첫 번째 호출은 언제 이루어 지더라도 (C2Q6600에서 약 250ms의 Guice의 경우) 훨씬 더 오래 걸립니다. 수백 개의 클래스가 클래스 로더에 의해로드됩니다.

예제 프로그램이 너무 짧기 때문에 오버 헤드는 아마도 초기 JIT 최적화와 클래스 로딩 활동에서 비롯 될 수 있습니다. 쓰레기 수집가는 아마도 프로그램이 끝나기 전에 시작되지 않을 것입니다.


업데이트:

이제 나는 무엇이 무엇인지 알아낼 수있는 신뢰할 수있는 방법을 찾았습니다. 진짜 시간을내어. 클래스 로딩과 밀접한 관련이 있지만 아직 아무도 찾지 못했습니다. 기본 방법의 동적 연결!

코드를 다음과 같이 수정하여 테스트가 시작되고 종료 될 때 로그가 표시되도록 (빈 마커 클래스가로드 될 때) 로그가 표시됩니다.

    TestIO t = new TestIO();
    new TestMarker1();
    t.fOne();
    t.fTwo();
    t.fOne();
    t.fTwo();
    new TestMarker2();

권리와 함께 프로그램을 실행하기위한 명령 JVM 매개 변수 실제로 무슨 일이 일어나고 있는지 보여줍니다.

java -verbose:class -verbose:jni -verbose:gc -XX:+PrintCompilation TestIO

그리고 출력 :

* snip 493 lines *
[Loaded java.security.Principal from shared objects file]
[Loaded java.security.cert.Certificate from shared objects file]
[Dynamic-linking native method java.lang.ClassLoader.defineClass1 ... JNI]
[Loaded TestIO from file:/D:/DEVEL/Test/classes/]
  3       java.lang.String::indexOf (166 bytes)
[Loaded TestMarker1 from file:/D:/DEVEL/Test/classes/]
[Dynamic-linking native method java.io.FileOutputStream.writeBytes ... JNI]
100
Time taken by 'fOne' ... 155354
100
Time taken by 'fTwo' ... 23684
100
Time taken by 'fOne' ... 22672
100
Time taken by 'fTwo' ... 23954
[Loaded TestMarker2 from file:/D:/DEVEL/Test/classes/]
[Loaded java.util.AbstractList$Itr from shared objects file]
[Loaded java.util.IdentityHashMap$KeySet from shared objects file]
* snip 7 lines *

그리고 그 시차의 이유는 다음과 같습니다. [Dynamic-linking native method java.io.FileOutputStream.writeBytes ... JNI]

또한 JIT 컴파일러 가이 벤치 마크에 영향을 미치지 않는다는 것을 알 수 있습니다. 컴파일되는 방법은 세 가지 만 있습니다 (예 : java.lang.String::indexOf 위의 스 니펫에서) 그리고 그들은 모두 전에 발생합니다. fOne 메소드가 호출됩니다.

  1. 테스트 된 코드는 매우 사소합니다. 가장 비싼 행동은

     System.out.println(k);
    

    따라서 측정하는 것은 디버그 출력이 얼마나 빨리 기록되는지입니다. 이것은 크게 다르며 크기 등을 스크롤 해야하는 경우 화면의 디버그 창의 위치에 따라 다를 수 있습니다.

  2. JIT/핫스팟은 종종 사용되는 코피 패스를 점진적으로 최적화합니다.

  3. 프로세서는 예상되는 코피 패스를 최적화합니다. 더 자주 사용되는 경로는 더 빨리 실행됩니다.

  4. 샘플 크기가 너무 작습니다. 이러한 마이크로 렌치 마크는 일반적으로 워밍업 단계를 수행합니다. Java는 정말로 아무것도하지 않습니다.

Jitting 외에도 다른 요인이 다음과 같습니다.

  • System.out.println을 호출 할 때 프로세스의 출력 스트림 차단
  • 다른 프로세스에 의해 프로세스가 예약됩니다
  • 배경 스레드 작업을 수행하는 쓰레기 수집가

좋은 벤치 마크를 얻고 싶다면

  • 최소한 수천 번의 벤치마킹을 벤치마킹하고 평균 시간을 계산하는 코드를 실행하십시오.
  • 처음 여러 통화의 시간을 무시하십시오 (JITTING으로 인해)
  • 가능하면 GC를 비활성화하십시오. 코드가 많은 객체를 생성하는 경우 옵션이 아닐 수 있습니다.
  • 벤치마킹중인 코드에서 로깅 (println call)을 제거하십시오.

여러 플랫폼에는 벤치마킹 라이브러리가 있습니다. 또한 표준 편차 및 기타 통계를 계산할 수도 있습니다.

가장 가능성이 높은 범인은 jit (정시) 핫스팟 엔진. 기본적으로 처음으로 코드가 실행됩니다. 기계 코드는 JVM에 의해 "기억"후 향후 실행에 재사용됩니다.

첫 번째 실행 후 두 번째로 생성 된 코드가 이미 최적화 되었기 때문이라고 생각합니다.

제안 된 바와 같이, JIT는 범인이 될 수 있지만, 기계의 다른 프로세스가 그 순간에 리소스를 사용하는 경우 자원 대기 시간뿐만 아니라 대기 시간뿐만 아니라 I/O 대기 시간도 마찬가지입니다.

이 이야기의 도덕은 Micrbenchmarking이 특히 Java에게는 어려운 문제라는 것입니다. 왜이 일을하고 있는지 모르겠지만 문제를 위해 두 가지 접근 방식 중에서 선택하려는 경우 이런 식으로 측정하지 마십시오. 전략 설계 패턴을 사용하고 두 가지 다른 접근 방식으로 전체 프로그램을 실행하고 전체 시스템을 측정하십시오. 이로 인해 장기적으로 처리 시간이 거의 없어지고, 그 시점에서 전체 앱의 성능이 병목 현상이 얼마나 많은지에 대한 훨씬 더 현실적인 견해를 제공합니다 (힌트 : 아마도 생각보다 적을 것입니다.)

가장 아마도 대답은 초기화입니다. JIT는 최적화되기 전에 더 많은 사이클이 걸리므로 정답이 아닙니다. 그러나 처음에는 다음과 같습니다.

  • 수업을 찾고 있습니다 (캐시가되어 두 번째 조회가 필요하지 않음)
  • 로드 클래스 (메모리에 머무른 상태로로드)
  • 기본 라이브러리에서 추가 코드 받기 (기본 코드가 캐시 됨)
  • 마지막으로 CPU의 L1 캐시에서 실행하도록 코드를로드합니다. 그것은 당신의 의미에서 속도를 높이기위한 가장 제안 가능한 사례이며 동시에 벤치 마크 (마이크로 렌치 마크)가 많이 말하지 않는 이유입니다. 코드가 충분히 작 으면 루프의 두 번째 호출은 빠른 CPU 내부에서 완전히 실행할 수 있습니다. 실제 세계에서는 프로그램이 더 크고 L1 캐시의 재사용이 크지 않기 때문에 발생하지 않습니다.
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top