-
CH10. JIT 컴파일의 세계로DevBook/Optimizing Java 2024. 6. 24. 00:29
JIT 컴파일은 JVM뿐만 아니라 많은 현대 프로그래밍 환경에 적용되어 온 기술이다.
JIT 컴파일러는 달라도 적용된 기술은 동일한 경우가 많다.
이번 장에서는 JITWatch라는 툴을 이용해 JVM의 내부 작동 원리를 시각화한다.
구체적인 JIT 최적화 알고리즘과 각각의 특성을 살펴보고 이런 기법들이 어떤 작용을 하는지 JITWatch로 관찰한다.
10.1 JITWatch란?
- JITWatch를 이용하면, 어플리케이션 실행 중에 핫스팟이 실제로 바이트코드에 무슨 일을 했는지 이해하는 데 도움이 됨
- 반드시 핫 패스(hot path)에 있는 컴파일 대상 메서드를 분석 대상으로 삼아야 함. 인터프리티드 메서드는 최적화 대상으로 적절하지 않음
- *핫 패스 : 프로그램 중 매우 빈번하게 호출/실행되는 코드 경로
- 반드시 핫 패스(hot path)에 있는 컴파일 대상 메서드를 분석 대상으로 삼아야 함. 인터프리티드 메서드는 최적화 대상으로 적절하지 않음
- JITWatch는 객관적인 비교에 필요한 측정값을 제공함
- JITWatch가 하는 일은, 실행 중인 자바 어플리케이션이 생성한 핫스팟 컴파일 상세 로그를 파싱/분석하여 그 결과를 자바FX GUI 형태로 보여주는 것임. 어플리케이션을 실행할 때 다음 플래그를 반드시 추가해야 JITWatch가 정상 작동함
- -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation
- 세 스위치를 켜면 JVM이 JITWatch에 입력할 로그를 생성하기 시작함
10.2 JIT 컴파일 개요
알아야 하는 이유
- VM이 데이터를 어떻게 수집하는지, 실행 프로그램에 어떤 최적화를 수행하는지 잘 알고 있어야 툴에서 컴파일드 코드를 보면서 올바르게 해석할 수 있다.
핫스팟의 JIT 컴파일 개요
1) 프로파일 기반 최적화(PGO)를 이용해 JIT 컴파일 여부 판단
- 내부적으로 핫스팟이 실행 프로그램 정보를 메서드 데이터 객체(Method Data Object, MDO)라는 구조체에 저장함
- MDO의 쓰임새는 바이트코드 인터프리터와 C1 컴파일러에서 JIT 컴파일러가 언제, 무슨 최적화를 할지 결정하는 데 필요한 정보를 기록하는 것
- 어떤 메서드가 호출되었고, 어느 분기문으로 갈라졌는지, 호출부에서는 무슨 타입이었는지 등의 정보
- MDO의 쓰임새는 바이트코드 인터프리터와 C1 컴파일러에서 JIT 컴파일러가 언제, 무슨 최적화를 할지 결정하는 데 필요한 정보를 기록하는 것
- 프로파일링된 프로퍼티의 '사용 빈도(hotness)'를 카운터에 계속 기록하고 기록한 값들은 프로파일링을 거치면서 차츰 사라짐
- 컴파일 큐 맨 앞에 이르렀을 때도 아직 핫한 메서드만 컴파일됨
2) 컴파일러별 세부 처리 절차 수행
- 프로파일링 데이터가 모이고 컴파일 결정을 내린 후에 컴파일러별 세부 처리 절차로 넘어감
- 컴파일러는 컴파일할 코드의 내부 표현형을 빌드함
- 구체적인 표현형은 사용하는 컴파일러(C1 or C2)에 따라 달라짐
- 컴파일러는 내부 표현형을 토대로 컴파일을 함
- 핫스팟 JIT 컴파일러는 다양한 최신 컴파일러 최적화 기법을 사용함
- 인라이닝, 루프 펼치기, 탈출 분석, 락 생략/확장, 단일형 디스패치, 인트린직, 온-스택 치환
- 주의) 이러한 최적화 기법은 런타임 정보와 지원 여부에 따라 달라짐
- 핫스팟 JIT 컴파일러는 다양한 최신 컴파일러 최적화 기법을 사용함
10.3 인라이닝
- 호출된 메서드(피호출부)의 콘텐츠를 호출한 지점(호출부)에 복사하는 것
- 메서드 호출 시 다음과 같은 오버헤드를 제거할 수 있음
- 전달할 매개변수 세팅
- 호출할 메서드를 정확하게 룩업
- 새 호출 프레임에 맞는 런타임 자료 구조(지역 변수 및 평가 스택 등) 생성
- 새 메서드로 제어권 이송
- 호출부에 결과 반환 (결과값이 있는 경우)
- JIT 컴파일러가 제일 먼저 적용하는 최적화여서 gateway optimization 라고도 함
- 메서드 경계를 없애고 연관된 코드를 한데 모아 놓기 때문
- 인라이닝 최적화를 제공하기 때문에 개발자는 잘 조직된, 재사용 가능한 코드를 작성할 수 있고 직접 마이크로 최적화를 할 필요가 없게 됨
- 마이크로 최적화 : 인라이닝처럼 명령문이나 연산 코드 단위로 최적화하는 것
- 핫스팟은 자동으로 통계치를 분석해 관련된 코드를 어느 시점에 하나로 모을지 결정함
예시)
> 인라이닝 최적화 전
int result = add(a, b); private int add(int x, int y) { return x + y; }
> 인라이닝 최적화 후
int result = a + b;
- add()의 바디는 호출부에 합쳐짐
10.3.1 인라이닝 제한
- VM 차원에서 인라이닝 서브시스템에 제한을 걸어야 할 경우도 있음
- 제약 조건이 하나도 없다면 컴파일러는 아주 깊은 호출 체인까지 파헤치며 인라이닝 할 것이기 때문
- VM에서 다음 항목을 조정해야 할 때
- JIT 컴파일러가 메서드를 최적화하는 데 소비하는 시간
- 생성된 네이티브 코드 크기 (즉, 코드 캐시 메모리 사용량)
- 핫스팟은 다음 항목을 따져보며 어떤 메서드를 인라이닝할지 결정함
- 인라이닝할 메서드의 바이트코드 크기
- 현재 호출 체인에서 인라이닝할 메서드의 깊이
- 메서드를 컴파일할 버전이 코드 캐시에서 차지하는 공간
10.3.2 인라이닝 서브시스템 튜닝
인라이닝 서브시스템의 작동 방식을 제어하는 기본적인 JVM 스위치 목록
- 중요 메서드가 인라이닝되지 않는 경우 환경에 따라 이런 메서드까지 인라이닝되도록 적절히 JVM 매개변수를 조정해야 할 수 있음
- -XX:MaxInlineSize나 -XX:FreqInlineSize 값 변경해보며 확인
- 매개변수를 바꿔가며 튜닝할 때에는 반드시 측정 데이터를 근거로 삼아야 함
10.4 루프 펼치기 (loop unrolling)
- 루프 내부의 메서드 호출을 전부 인라이닝하면, 컴파일러는 루프를 한번 순회할 때마다 비용이 얼마나 드는지, 반복 실행되는 코드는 크기가 얼마나 되는지 더 분명해짐
- 이 정보를 토대로 컴파일러는 매번 순회할 때마다 루프 처음으로 되돌아가는 횟수를 줄이기 위해 루프를 펼칠 수 있음
- 루프 바디가 짧을수록 백 브랜치 비용이 상대적으로 높기 때문에 핫스팟은 다음 기준에 따라 루프 펼치기 여부를 결정함
- 기준
- 루프 카운터 변수 유형(대부분 객체 아닌 int나 long형 사용)
- 루프 보폭(loop stride) : 한번 순회할 때마다 루프 카운터 값이 얼마나 바뀌는가
- 루프 내부의 탈출 지점 개수(return or break)
- *백 브랜치 : 한번 순회를 마치고 다시 루프문 처음으로 돌아가는(back) 것
- 백 브랜치가 일어나면 그때마다 CPU는 유입된 명령어 파이프라인을 덤프하기 때문에 성능상 바람직하지 않음
- 주의) 루프 펼치기는 핫스팟 버전별로 로직이 상이하고 아키텍처마다 많이 다름
- 기준
- 참고
10.4.1 루프 펼치기 정리
핫스팟은 다양한 최적화 기법으로 루프 펼치기를 함
- 카운터가 int, short, char형일 경우 루프를 최적화한다.
- 최적화 방식은 루프 바디를 펼치고 세이프포인트 폴을 제거하는 것이다. --> 루프를 펼쳤기 때문에 루프를 다 돌았는지 체크하지 않아도 되어 제거하는 것
- *세이프포인트 폴 : 세이프포인트 도달 여부를 폴링하여 체크하는 코드
- 세이프포인트는 내부 데이터 구조 변경을 모두 완료했다는 것을 실행중인 스레드가 아는 코드 상의 위치이다.
- 해당 위치는 JVM이 자바 코드를 실행중인 모든 스레드를 멈춰야하는지 확인하기 위한 이상적인 시간이다.
- 세이프포인트를 확인함으로써 JVM에게 GC와 같은 모든 어플리케이션 스레드 중단이 필요한 작업(메모리 레이아웃 변경, 내부 데이터 구조 변경 등)을 수행할 수 있는 기회를 제공한다.
- *세이프포인트 폴 : 세이프포인트 도달 여부를 폴링하여 체크하는 코드
- 루프를 펼치면 백 브랜치 횟수가 줄고 그만큼 분기 예측 비용도 덜 든다.
- 세이프포인트 폴을 제거하면 루프를 순회할 때마다 하는 일이 줄어든다.
주의) 루프 펼치기는 핫스팟 버전별로 로직이 상이하고 아키텍처마다 많이 다름
10.5 탈출 분석 (escape analysis)
- 핫스팟은 어떤 메서드가 내부에서 수행한 작업을 그 메서드 경계 밖에서도 볼 수 있는지, 또는 부수 효과를 유발하지는 않는지 범위 기반 분석(scope-based analysis)을 통해 판별함
- 이러한 기법을 탈출 분석이라고 하며, 메서드 내부에서 할당된 객체를 메서드 범위 밖에서도 바라볼 수 있는지 알아보는 용도로 쓰임
- 탈출 분석 최적화는 반드시 인라이닝 수행 이후 시도함
- 인라이닝 해서 피호출부 메서드 바디를 호출부에 복사하면 피호출부의 메서드 인수로 전달된 객체는 더 이상 탈출 객체로 표시되지 않기 때문
- 핫스팟은 탈출 분석 단계 도중, 잠재적으로 탈출한 객체를 3가지 유형으로 분류함
참고
https://blogs.oracle.com/javamagazine/post/escape-analysis-in-the-hotspot-jit-compiler
10.5.1 힙 할당 제거
- 탈출 분석의 목표는 힙 할당을 막을 수 있는지 추론하는 것
- 핫스팟의 탈출 분석 최적화는 개발자가 객체 할당률을 신경 쓰지 않고도 자바 코드를 작성할 수 있도록 설계됨
방식 --> 힙 할당을 막기 위한 방식
- 할당된 객체가 메서드를 탈출하지 않는다는 사실을 밝히면(NoEscape로 분류됨) VM은 스칼라 치환(scalar replacements) 최적화를 적용해 객체 필드를 마치 처음부터 객체 필드가 아닌 지역 변수였던 것처럼 스칼라 값으로 바꿈
- 그 후 레지스터 할당기(register allocator)라는 핫스팟 컴포넌트에 의해 CPU 레지스터 속으로 배치됨
- 여유 레지스터가 부족할 경우, 스칼라값을 현재 스택 프레임 위에 둘 수도 있음. 이를 스택 스필(stack spill)이라고 함
- *스택 스필
- 변수를 레지스터에 두는 가장 큰 이유는 속도지만, 컴퓨터 레지스터 개수는 한정되어 있어 모든 변수를 레지스터에 담을 수는 없음
- 이런 이유로 변수를 레지스터 -> 메모리로 옮기는 과정을 스필링(spilling), 반대로 메모리 -> 레지스터로 옮기는 과정을 필링(pilling)이라고 함
- *스택 스필
- 여유 레지스터가 부족할 경우, 스칼라값을 현재 스택 프레임 위에 둘 수도 있음. 이를 스택 스필(stack spill)이라고 함
예시)
- 위의 MyObj 인스턴스 foo는 메서드 범위를 벗어나지 않으므로 NoEscape로 분류됨
- 위의 MyObj 인스턴스는 extBar 메서드 인수로 전달되므로 ArgEscape로 분류됨
- 탈출 분석 직전, extBar()가 루프 바디 안으로 인라이닝되면 MyObj 인스턴스는 NoEscape로 다시 분류되어 힙 할당을 막을 수 있음
10.5.2 락과 탈출 분석
- 핫스팟은 탈출 분석 및 관련 기법을 통해 락 성능도 최적화함
- 단, 이 최적화는 synchronized를 사용한 인트린직 락(intrinsic lock)에만 해당됨
- *인트린직 락
- 자바에서 기본 제공되는 동기화 메커니즘으로, 'synchronized' 키워드를 사용해 구현됨
- JVM이 자동으로 관리함. 특정 코드 블록이나 메서드를 동기화하면 해당 객체의 인트린직 락이 자동으로 획득되고 해제됨
- *인트린직 락
- java.util.concurrent 패키지에 있는 락에는 적용되지 않음
- *java.util.concurrent 패키지에 있는 락
- 개발자가 락의 획득과 해제를 명시적으로 관리해야 함(lock, unlock 메서드 호출)
- *java.util.concurrent 패키지에 있는 락
- 단, 이 최적화는 synchronized를 사용한 인트린직 락(intrinsic lock)에만 해당됨
- 락 최적화의 핵심 정리
- 비탈출(nonescaping) 객체에 있는 락은 제거한다. - 락 생략(lock elision)
- 같은 락을 공유한, 락이 걸린 연속된 영역은 병합한다. - 락 확장(lock coarsening)
- 락을 해제하지 않고 같은 락을 반복 획득한 블록을 찾아낸다. - 중첩 락(nested lock)
방식 - 핫스팟은 락 확장/제거 시점을 자동으로 계산함
1) 락 확장
- 핫스팟은 동일한 객체에 연속적으로 락이 걸려 있을 경우, 락이 걸린 영역을 넓힐 수 있는지 체크함
- 락을 발견하면 반대 방향으로 거슬러 올라가 동일한 객체에 언락이 있는지 살펴보고, 만약 언락이 발견되면 두 락 영역을 더 큰 단일 영역으로 합할 수 있는지 살핌
- 락 확장 최적화는 기본 활성화되어 있지만, VM 스위치 -XX:-EliminateLocks로 해제할 수 있음
- 참고
2) 중첩 락 제거
- 핫스팟은 같은 객체에 걸린 중첩 락을 감지해 해당 스레드가 이미 그 락을 획득한 상태라면 내부 락을 제거함
- 중첩 락 최적화도 기본 활성화되어 있지만, VM 스위치 -XX:-EliminateNestedLocks로 끌 수 있음
10.5.3 탈출 분석의 한계
- 다른 최적화 기법들처럼 트레이드오프가 존재함
한계점 3가지
1) 힙 대신 저장하는 공간의 희소성
- 스칼라 치환에 의해 힙이 아닌 CPU 레지스터나 스택 공간에 저장함
- 해당 공간은 상대적으로 희소한 리소스임
2) 원소 개수가 많은 배열은 탈출 분석 대상이 아님
- 기본적으로 원소가 64개 이상인 배열은 핫스팟에서 탈출 분석의 혜택을 볼 수 없음
- 탈출 분석 최적화 여부는 사용한 배열 인덱스의 최대값이 아닌, 배열의 길이만으로 결정됨
- 배열 길이가 64를 초과하면 무조건 힙에 저장되고 이 코드의 할당률은 빠르게 상승할 수 있음
- 해당 개수 제한은 다음 VM 스위치로 조정함
- -XX:EliminateAllocationArraySizeLimit=<n>
3) 부분 탈출 분석(partial escape analysis)을 지원하지 않음
- 객체가 어느 분기점에서건 메서드 범위를 탈출하면(NoEscape이 아니게 되면) 힙에 객체를 할당하지 않는 최적화는 적용되지 않음
- 위의 분기문은 조건에 따라 둘 중 하나로 분기하므로 mightEscape 객체가 메서드를 탈출할 경우 반드시 ArgEscape로 분류됨
- 탈출 분석 최적화가 적용되지 않아 객체 할당률과 GC압을 가중시킬 수 있음
- 위 코드처럼 작성할 경우 비탈출 분기 조건 안의 객체(첫번째 분기문 내 객체)에는 탈출 분석 최적화가 적용될 수 있음
10.6 단형성 디스패치
- 핫스팟 C2 컴파일러가 수행하는 추측성 최적화는 대부분 경험적 연구 결과를 토대로 함
- 단형성 디스패치(monomorphic dispatch) 기법도 그런 부류 중 하나임
- 추측성 가정
- 어떤 객체에 있는 메서드를 호출할 때, 그 메서드를 최초로 호출한 객체의 런타임 타입을 알아내면 그 이후의 모든 호출도 동일한 타입일 가능성이 크다.
- 이 추측성 가정이 옳다면 해당 호출부의 메서드 호출을 최적화할 수 있음
- 항상 타입이 같다면 일단 호출 대상을 계산해서 invokevirtual 명령어를 퀵 타입 테스트(가드) 후 컴파일드 메서드 바디로 분기하는 코드로 치환하면 됨
- *invokevirtual 명령어
- JVM에서 메서드 호출할 때 사용하는 명령어
- 런타임에 실제 객체의 클래스 타입을 확인하고, 그 클래스에 정의된 메서드를 호출함 (다형성을 지원하기 위해 설계된 것)
- *퀵 타입 테스트(가드)와 컴파일드 메서드 바디로의 분기
- 1. 호출 대상 계산 : 객체의 메서드를 호출하려 할 때, 그 객체가 어떤 클래스의 인스턴스인지 알아내는 과정
- 2. 퀵 타입 테스트(가드) : 매우 빠르게 호출 대상 객체의 타입이 예상한 타입(항상 동일한 타입)과 일치하는지 확인
- 3. 컴파일드 메서드 바디로 분기 : 타입이 일치하면, invokevirtual 명령어를 사용하지 않고 직접 컴파일된 메서드 바디로 점프함
- *invokevirtual 명령어
- 즉, klass 포인터 및 vtable을 통해 가상 룩업을 하고 참조하는 일은 딱 한번만 수행하면 됨. 그 이후로 해당 호출부에서 메서드를 호출할 경우를 대비해 캐시하는 것임
- 항상 타입이 같다면 일단 호출 대상을 계산해서 invokevirtual 명령어를 퀵 타입 테스트(가드) 후 컴파일드 메서드 바디로 분기하는 코드로 치환하면 됨
- 핫스팟은 자주 쓰이지는 않지만 이형성 디스패치(bimorphic dispatch)라는 최적화도 지원함
- 서로 다른 두 타입을 단형성 디스패치와 같은 방법으로, 호출부마다 상이한 두 klass 워드를 캐시해 처리함
메서드 호출부 유형별 최적화 기법
- 단형성(monomorphic) 호출부 : 단형성 디스패치 기법 적용
- 이형성(bimorphic) 호출부 : 이형성 디스패치 기법 적용
- 다형성 (megamorphic) 호출부 : 원래 호출부에서 instanceof 체크를 하며 타입을 벗겨내 단형성 호출부 + 이형성 호출부로 구성 후 최적화 기법 적용
예시)
참고
https://shipilev.net/blog/2015/black-magic-method-dispatch/#_preface
10.7 인트린직
- 인트린직(intrinsics) JIT 서브시스템이 동적 생성하기 이전에 JVM이 이미 알고 있는, 고도로 튜닝된 네이티브 메서드 구현체를 가리키는 용어
- 주로 OS나 CPU 아키텍처의 특정 기능을 응용하는, 성능이 필수적인 코어 메서드에 쓰임
- 플랫폼에 따라 지원 여부가 달라짐
- JVM은 기동 직후 런타임에 자신을 실행한 하드웨어의 CPU를 꼼꼼히 살펴보고 사용 가능한 프로세서의 기능을 목록화함
- 즉, 어떤 식으로 최적화할지 코드 컴파일 타임에 결정할 필요없이 런타임까지 미룰 수 있음
- OpenJDK 핫스팟 소스 코드에서 확장자가 .ad(architecture dependent)인 파일이 바로 인트린직 템플릿임
- 자바9부터 메서드 앞에 @HotSpotIntrinsicCandidate 어노테이션을 붙여 인트린직을 사용할 수 있음을 나타냄
- x86_64 아키텍처용 템플릿은 hotspot/src/cpu/x86/vm/x86_64.ad 파일에 있음
*자바17 기준 아래 어노테이션 사용
@IntrinsicCandidate public static native long currentTimeMillis();
예시)
사용 대수(밑이 10인 로그값)를 계산할 때 자바에서는 java.lang.Math 패키지에 있는 다음 메서드를 사용함
public static double log10(double a)
x86_64 아키텍처에서 상용 대수는 두 부동소수점 장치(floating-point unit, FPU) 명령어로 계산할 수 있음
1. 상수 2의 상용 대수를 계산한다.
2. 1에서 계산된 값이 밑이 2인 주어진 인수의 대수와 곱한다.
다음 이 연산을 수행하는 인트린직 코드임
주의
- 새 인트린직을 추가할 때에는 JVM의 복잡도가 증가하는 것과 유용하게 잘 쓰는 것 사이에서 고민이 필요함
- *JVM의 복잡도 증가 : 인트린직 코드 추가를 위해 JVM 내부에 특별한 처리 로직이 필요하고 플랫폼별 최적화가 필요하기 때문
- 인트린직은 정말 자주 쓰이는 작업에 한해서만 성능에 큰 영향을 미칠 수 있음
10.8 온-스택 치환
- 컴파일을 일으킬 정도로 호출 빈도가 높지 않지만 메서드 내부에 핫 루프가 포함된 경우가 있음 (ex. 자바 프로그램의 main 메서드)
- 핫스팟은 이런 코드를 온-스택 치환(OSR)을 이용해 최적화함
- 인터프리터가 루프 백 브랜치 횟수를 세어보고 특정 한계치를 초과하면 루프를 컴파일한 후 치환해서 실행함
- *백 브랜치 : 루프 바디 끝에서 종료 조건을 체크하고 (아직 완료되지 않았으면) 루프 바디 처음으로 되돌아가는 것
- 인터프리터가 루프 백 브랜치 횟수를 세어보고 특정 한계치를 초과하면 루프를 컴파일한 후 치환해서 실행함
- 특징
- 컴파일러는 컴파일 이전의 루프 내에서 액세스하는 지역 변수와 락 등의 상태 변화가 컴파일 이후에도 반영되도록 보장해야 함
- 컴파일된 루프를 벗어난 후 실행을 재개하는 지점에서 모든 상태 변화가 가시적이어야 함
예시)
.java 코드
.class 바이트코드
33번 인덱스에서 goto 바이트코드는 13번 인덱스의 루프 체크로 흐름을 되돌림
핫스팟의 C1, C2 두 컴파일러 모두 OSR 컴파일을 수행함
JITWatch로 바이트코드와 소스 코드를 보면 어느 루프가 OSR 컴파일되었는지 확인 가능함
10.9 세이프포인트 복습
- 세이프포인트란?
- 특정 시점에 모든 자바 코드를 실행중인 스레드의 실행을 중단하고 JVM이 특정 작업을 수행할 수 있도록 하는 메커니즘
- 이 지점에서는 모든 스레드가 일정한 상태에 있으며, 가비지 컬렉션과 같은 작업을 안전하게 수행할 수 있음
- JVM에 세이프포인트가 걸리는 조건
- GC STW
- 메서드를 역최적화
- 힙 덤프 생성
- 바이어스 락(biased lock) 취소
- 클래스 재정의
- 컴파일드 코드에서 세이프포인트 체크 발급은 JIT 컴파일러가 담당하며, 핫스팟에서는 다음 지점에 세이프포인트 체크 코드를 넣음
- 루프 백 브랜치 지점
- 메서드 반환 지점
- *세이프포인트 체크 : JVM이 세이프포인트를 요청했는지 확인하고, 요청이 있다면 스레드를 중단시킴
- 따라서 경우에 따라 스레드가 세이프포인트에 도달하려면 어느 정도 시간이 소요될 수 있음
- 컴파일러는 세이프포인트를 폴링하며 체크하는 비용을 감수하느냐, 이미 세이프포인트에 닿은 스레드가 다른 스레드도 세이프포인트에 모두 닿을 때까지 대기해야 하는 긴 세이프포인트 시간(time-to-safepoint, TTSP)을 회피하느냐, 사이에서 고민할 것임
- -XX:+PrintGCApplicationStoppedTime, -XX:+PrintSafepointStatistics 설정 추가하면 세이프포인트 때문에 프로그램에소요된 총 시간 확인 가능
- 13장에서 자세히 다룰 예정
참고
https://blog.ragozin.info/2012/10/safepoints-in-hotspot-jvm.html
10.10 코어 라이브러리 메서드
- JDK 코어 라이브러리 크기가 JIT 컴파일러에 어떤 영향을 주는지 살펴보자
10.10.1 인라이닝하기 적합한 메서드 크기 상한
- 인라이닝 여부는 메서드의 바이트코드 크기로 결정됨
- JarScan 오픈소스 툴을 사용해 클래스 폴더 or JAR 파일 내부에서 바이트코드 크기가 주어진 한계치 이상인 메서드를 모두 찾아낼 수 있음
- 인라이닝 최적화를 위한 개선 방식
- 도메인에 특정한 메서드로 성능 개선
- ex) ASCII 캐릭터만 입력받는 경우 String의 toUpperCase()를 도메인에 특정한 메서드로 만들어(ASCII 전용 구현체를 만듦) 바이트코드 크기를 인라이닝 한계치 이하로 줄일 수 있음
- 메서드를 작게 구성
- 메서드를 작게 유지하면 다양한 인라이닝 트리를 구축해 핫 경로를 더욱 최적화할 수 있음
- 메서드가 커지면 인라이닝 크기 한계치를 금세 초과해 최적화 안 된 경로가 남아있게 됨
- 도메인에 특정한 메서드로 성능 개선
10.10.2 컴파일하기 적합한 메서드 크기 상한
- 핫스팟에는 메서드 크기가 어느 이상 초과하면 컴파일되지 않는 한계치(8,000 바이트)가 있음
- 운영계 JVM에서는 이 수치를 바꿀 수 없지만, 디버그 JVM에서는 -XX:HugeMethodLimit=<n> 스위치로 컴파일 가능한 메서드 바이트코드의 최대 크기를 설정할 수 있음
- 고려사항
- 핫스팟 한계치에 다다를 것 같으면 JarScan 같은 툴로 미리 한번 메서드 크기를 확인해보는 게 좋음
- JIT 컴파일러 설정값은 대부분 튜닝할 수 있지만, 항상 변경 전후 시스템을 벤치마크해야 함
- 코드 캐시 공간, JIT 컴파일러 큐 길이, GC압 등 예기치 않은 부수 효과가 발생할 수도 있기 때문
10.11 마치며
- -XX:+PrintCompilation 플래그와 9장에서 소개한 기법들을 활용하면 개별 메서드의 최적화 여부를 확인할 수 있음
- 최적화 기법을 알고 있으면 메서드 작성 시 이를 고려해 최적화된 형태로 실행되게 구성할 수 있음
'DevBook > Optimizing Java' 카테고리의 다른 글
14장. 고성능 로깅 및 메시징 (0) 2024.07.31 CH2. JVM 이야기 (0) 2024.04.14 CH1 성능과 최적화 (0) 2024.04.09 - JITWatch를 이용하면, 어플리케이션 실행 중에 핫스팟이 실제로 바이트코드에 무슨 일을 했는지 이해하는 데 도움이 됨