Gemini_Generated_Image_7t33uf7t33uf7t33.jpeg

JVM 환경에서 애플리케이션이 컴파일되어 실행되는 과정에서 JIT 컴파일러는 자바 컴파일러에 의해 변환된 자바 바이트 코드(.class)를 일정 시점에 네이티브 코드로 변환하여 코드 캐시에 캐싱한 이후 인터프리터 대신 네이티브 코드를 직접 실행하도록 한다. 그 덕에 명령어를 하나씩 실행하는 인터프리팅 방식보다 빠른 속도를 가질 수 있다.

물론, JIT 컴파일러의 컴파일 비용도 있기 때문에 모든 바이트코드를 컴파일하진 않고, JVM 내부적으로 해당 메서드의 수행 빈도를 확인 후 일정 정도를 넘을 때 컴파일을 수행한다.

만약 JVM 의 동작형태나 구성요소에 대해 더 궁금하다면 다음 포스팅을 참고하도록 하자. (링크)

JIT컴파일러는 단순 네이티브 코드 변환만을 수행하지 않는다. 변환시에 다양한 최적화 기법을 통해 Java 코드를 더 빠르게 실행할 수 있도록 하며, 정적 컴파일러가 수행하는 최적화뿐 아니라 동적 프로파일링 정보를 활용한 최적화도 포함된다.

이번 포스팅에서는 JIT 컴파일러가 수행하는 주요 코드 최적화 기법에 대해 알아보도록 한다.

1. Inlining(인라이닝)


개념

메서드 호출 시 발생하는 오버헤드(스택 조작, 호출/복귀 비용등)를 줄이기 위해, 호출하는 위치에 해당 메서드의 본문을 직접 삽입하는 최적화 기법으로 인라이닝 자체로도 성능 향상을 주지만, 동시에 다른 최적화의 기반을 만들어주기에 게이트웨이(gateway) 최적화 라고 부르기도 한다.

우선, 어떻게 최적화가 이뤄지는지 다음 간단한 예제 코드를 통해 확인해보자.

인라이닝 최적화 예제 코드

// 최적화 전
int add(int a, int b) { return a + b; }
int sum = 0;
for (int i = 0; i < N; i++) {
    sum += add(x, y);
}

// 최적화 후 (인라이닝 적용)
int sum = 0;
for (int i = 0; i < N; i++) {
    // add(x, y)의 본문을 직접 삽입 (a+b 수행)
    sum += (x + y);
}

⇒ 최적화 전에는 루프 내에서 매번 add(x, y) 메서드를 호출하지만, 인라이닝 이후에는 메서드 호출 없이 직접 연산을 수행한다.

이렇게 호출부분의 내용이 직접 들어가며 호출 관련 오버헤드가 제거되고 루프 내 연산이 단순화되는 것을 확인할 수 있다.