Study · JDK · STEP 8 / 8 · 실행할 때

JIT는 자주 도는 코드만 골라, 실행 도중에 기계어로 갈아 끼운다

장면

같은 반복을 20구간 돌렸더니 첫 구간과 마지막 구간의 시간이 18배 가까이 달랐다. 코드는 하나도 안 바뀌었는데?

public class Warm3 {
    static int add(int a, int b) { return a + b; }

    public static void main(String[] args) {
        long sum = 0;
        for (int c = 1; c <= 20; c++) {                  // 구간 20개
            long t0 = System.nanoTime();
            for (int n = 0; n < 200_000; n++) {          // 구간마다 20만 번 (밑줄은 읽기 쉽게 끊는 표기)
                sum = add((int) (sum & 0xFFFF), n & 7);  // 계산 내용보다 add를 많이 부르는 것이 목적이다
            }
            System.out.println("chunk " + c + " us=" + (System.nanoTime() - t0) / 1000);
        }
    }
}

아래 재생기의 ▶를 누르면 한 스텝씩 진행된다. 금색 테두리가 방금 바뀐 곳이고, 캡션이 무슨 일이 일어났는지 말해 준다. 굵게 밑줄 친 말은 누르면 뜻이 나온다.

장면 · 자주 도는 코드 번역 0 / 20

add를 부르는 반복을 20만 번씩 20구간 돌린다. ▶를 누를 때마다 한 구간씩 막대가 쌓이고, 번역 로그가 함께 찍힌다. 괄호 안 「단계」는 번역 단계 번호다(0 인터프리터 · 3 C1 · 4 C2).

키보드 · ← → 한 스텝 · Space 재생/멈춤 · Home 처음

그래서 성능을 잴 때 처음 몇 구간(워밍업)의 숫자는 나중 구간과 다르게 본다.

어떻게 확인했나

  • 시간은 System.nanoTime으로 잰 한 번의 실행값이다. 컴퓨터마다 다르므로 비율과 순서만 읽는다.
  • 번역 시점은 -XX:+PrintCompilation으로 확인했다. 두 번 돌려 순서가 같았다.
  • PrintCompilation 로그 시각은 번역을 시작한 때다(compileBroker.cpp L2198–L2200). 끝난 때는 -XX:+LogCompilation으로 따로 두 번 쟀다 — 반복문의 C2 번역은 시작 약 4ms 뒤 끝났고, 같은 순간 C1 번역본이 폐기됐다(instanceKlass.cpp L3570–L3575). 장면의 로그는 그 전 실행의 것이라 시작 40ms → 폐기 43ms로 약 3ms다.
  • add가 반복문 번역본 안에 복사돼 들어간 것은 -XX:+PrintInlining 출력(@ 36 Warm3::add inline)으로 확인했다.