11장 - 백엔드 컴파일러 최적화
11.1 들어가며
바이트코드를 프로그래밍 언어의 중간 표현이라고 생각하면, 컴파일러가 클래스 파일을 로컬 환경에 맞는 네이티브 코드로 변환하는 과정을 전체 컴파일 과정의 백엔드로 간주할 수 있다.
AOT 컴파일러든 JIT 컴파일러든 자바 가상 머신에서 필수는 아니다. "자바 가상 머신 명세" 는 어떤 컴파일러를 제공해야 한다고 규정하지 않는다. 컴파일러를 구현하는 방법이나 제약도 안내하지 않는다 하지만 백엔드 컴파일러의 컴파일 성능과 최적화 품질은 상용 가상 머신의 우수성을 측정하는 핵심 지표다.
11.2 JIT 컴파일러
현재 가상 머신의 쌍두마차인 핫스팟과 OpenJ9 는 자바 프로그램을 먼저 인터프리터로 해석해 실행한다. 그런 다음 아주 자주 실행되는 메서드나 코드 블록이 발견되면 해당 코드를 네이티브 코드로 컴파일하고 다양한 최적화를 적용해 실행 효율을 높인다. 이런 코드 블록을 핫수팟 코드, 핫코드라고 부르며 런타임에 이 작업을 수행하는 백엔드 컴파일러를 JIT 컴파일러라고 한다.
11.2.1 인터프리터와 컴파일러
현재 가상 머신의 쌍두마차인 핫스팟과 OpenJ9 는 인터프리터와 컴파일러를 함께 사용한다. 각각은 고유한 장점이 있다.
프로그램을 빠르게 시작해야 할 때는 인터프리터가 먼저 나서서 컴파일 없이 곧바로 실행할 수 있다. 프로그램이 시작된 후에는 시간이 흐를수록 컴파일러의 역할이 커진다. 점점 더 많은 코드를 네이티브 코드로 컴파일해 실행 효율을 높이는 것이다. 또한 메모리가 부족한 환경에서는 인터프리터 방식으로 메모리를 절약할 수 있다. 그래서 일부 임베디드 시스템과 대부분의 자바 카드 제품이라면 인터프리터만 쓰인다.
인터프리터는 적극적으로 최적화하는 컴파일러의 비상구 역할도 한다. 컴파일러가 최적화를 적극적으로 하다 보면 이따금 잘못된 선택을 하기도 한다. 대부분의 경우에는 성능이 개성되지만 특수한 상황에서는 올바른 결과를 내지 못하는 최적화를 사용하는 것이다. 그래서 적극적인 최적화 가정이 무너지는 경우 최적화를 최소화하고 다시 인터프리어에 실행을 맡길 수 있다.
이처럼 자바 가상 머신의 전체 실행 구조에선 인터프리터와 컴파일러는 항상 서로 협력하여 프로그램을 실행한다.
핫스팟 가상 머신에는 JIT 컴파일러가 2개 또는 3개 내장되어 있다. 그 중 2개는 오래전부터 존재했으며, 각각 클라이언트 컴파일러(C1), 서버 컴파일러(C2) 라는 이름으로 불린다. 세번째 JIT 컴파일러는 JDK 10 과 함께 등장한 그랄 컴파일러로, 장기적으로 C2 를 대체할 예정이었으나, JDK 16 부터 표준 JDK 에서 배재된 채 그랄 VM 이라는 오라클의 별도 프로젝트에서 개발되고 있다.
JIT 컴파일러가 바이트코드를 네이티브 코드로 컴파일하면 시간이 걸리며, 일반적으로 최적화를 더 많이 할수록 컴파일도 오래 걸린다. 한편 더 많이 최적화된 코드로 컴파일하기 위해 인터프리터가 성능 모니터링 정보를 수집할 수 있으며, 이 작업 역시 실행 시간에 영향을 준다.
프로그램 시작 응답 속도와 운영 효율 사이에서 최상의 균형을 맞추기 위해서 핫스팟 가상 머신은 컴파일 서브시스템에 계층형 컴파일을 추가했다. 계층형 컴파일 개념은 오래된 것이지만 JDK 6에 와서야 처음 제공되기 시작했다. 그리고 마침내 JDK 7 때 서버 모드 가상 머신의 기본 컴파일 전략으로 승격했다. 계층형 컴파일은 컴파일과 최적화 규모와 소요 시간에 따라 다음과 같이 여러 단계의 수준으로 수행된다.
계층 0 : 인터프리터가 프로그램을 순수하게 해석 실행한다. 성능 모니터링 기능은 켜지 않는다.
계층 1 : 클라이언트 컴파일러를 사용하여 바이트코드를 네이티브 코드로 컴파일하고 실행한다. 이때 간단하고 안정적인 최적화만 수행하며, 성능 모니터링은 하지 않는다.
계층 2 : 클라이언트 컴파일러를 사용한다. 메서드 및 반환 횟수 통계 등 몇 가지 성능 모니터링만 수행한다.
계층 3 : 여전히 클라이언트 컴파일러를 사용한다. 분기 점프와 가상 메서드 호출 버전 등 모든 성능 모니터링 정보를 수집한다.
계층 4 : 서버 컴파일러를 사용한다. 서버 컴파일러는 성능 모니터링 정보를 활용하여 더 오래 걸리는 최적화까지 수행한다. 이때 신뢰도가 낮은 공격적인 최적화를 수행하기도 한다.
가상 머신 버전과 실행 매개 변수에 따라 계층의 종류와 구체적인 동작 방식은 달라질 수 있다.
계층형 컴파일이 도입된 뒤로는 인터프리터, 클라이언트 컴파일러, 서버 컴파일러가 협력해 동작하면서 핫 코드가 여러 번 컴파일 될 수 있다. 빠르게 컴파일해야 할 때는 클라이언트 컴파일러를 사용하고, 성능을 더 높여야 할 때는 서버 컴파일러를 사용한다. 서버 컴파일러가 매우 복잡한 최적화 알고리즘을 수행할 때는 우선 클라이언트 컴파일러로 간단한 최적화 후, 복잡한 최적화는 느긋하게 마무리하는 방식도 적용 가능하다.
Last updated