diff --git a/minsu/ch10/README.md b/minsu/ch10/README.md new file mode 100644 index 0000000..2afbfcc --- /dev/null +++ b/minsu/ch10/README.md @@ -0,0 +1,236 @@ +# 10. JIT 컴파일의 세계로 + +# 10.1 JITWatch란? +[JITWatch](https://github.com/AdoptOpenJDK/jitwatch/) +- 애플리케이션 실행 중에 핫스팟이 실제로 바이트코드에 무슨 일을 했는지 이해하는데 도움이 된다. +- 객관적인 비교에 필요한 측정값을 제공한다. +- 실행 중인 자바 애플리케이션이 생성한 핫스팟 컴파일 상세 로그를 파싱/분석해서 그 결과를 자바 FX GUI 형태로 보여준다. + +다음 플래그를 추가해야한다. +``` +-XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation +``` + + +## 디버그 JVM과 hsdis +- 디버그 JVM은 운영 JVM보다 더 상세한 디버깅 정보를 추출하려고 제작한 가상 머신 +- 디버그 JVM을 통해서 JIT 서브시스템의 통계칭 등의 상세 디버깅 정보를 얻을 수 있다. +- JIT 컴파일러가 생성한 역 에섬블된 네이티브 코드를 살펴보려면 hedis 같은 역에섬블리 바이너리가 있어야 한다. + +# 10.2 JIT 컴파일 개요 +- VM이 데이터를 어떻게 수집하는지, 실행 프로그램에 어떤 최적화를 수행하는지 잘 알고 있어야 툴에서 컴파일드 코드를 보면서 올바르게 해석할 수 있다. +- 핫스팟은 PGO를 이용해 JIT 컴파일 여부를 판단한다. + +컴파일러는 컴파일할 코드의 내부 표현형을 빌드하며, 이 내부 표현형을 토대로 코드를 한껏 컴파일한다. +핫스팟 JIT 컴파일러는 다양한 최신 컴파일러 최적화 기법을 동원한다. +- 인라이닝 +- 루프 펼치기 +- 탈출 분석 +- 락 생략/확장 +- 단일형 디스패치 +- 인트린직 +- 온-스택 치환 + +> 중요한 점은, 이러한 최적화 기법은 런타임 정보와 지원 여부에 따라 다소 (완전히) 달라질 수 있다. +> +> 가령 CI은 추측성 최적화를 안 하므로 실행 성격이 어떨지 확실하지 않은 가정하에 최적화를 하지 않는다. + +# 10.3 인라이닝 +호출된 메서드의 호출한 지점에 복사하는 것이다. +메서드 호출 시 다음과 같은 오버헤드를 제거할 수 있다. +- 전달할 매개변수 세팅 +- 호출할 메서드를 정확하게 룩업 +- 새 호출 프레임에 맞는 런타임 자료 구조(지역 변수 및 평가 스택 등) 생성 +- 새 메서드로 제어권 이송 +- 호출부에 결과 반환 (결과값이 있는 경우) + +인라이닝은 JIT 컴파일러가 제일 먼저 적용하는 최적화라서 관문 최적화라고도 하며, 다른 최적화 범위를 확장시키는 역할도 한다. +- 탈출 분석 +- 죽은 코드 제거 +- 루프 펼치기 +- 락 생략 + +## 10.3.1 인라이닝 제어 +때로는 VM 차원에서 인라이닝 서브시스템에 제한을 걸어야하는 경우도 있다. +- JIT 컴파일러가 메서드를 최적화하는데 소비하는 시간 +- 생성된 네이티브 코드크기 + + +핫스팟은 다음 항목을 따지면서 어떤 메서드를 인라이닝할 지 결정한다. +- 인라이닝할 메서드의 바이트코드 크기 +- 현재 호출 체인에서 인라이닝할 메서드의 깊이 +- 메서드를 컴파일한 버전이 코드 캐시에서 차지하는 공간 + +## 10.3.2 인라이닝 서브시스템 튜닝 +스위치 설명 + +- ```XX:MaxInlineSize=``` 메서드를 이 크기 이하로 인라이닝 +- ```XX:FreqInlineSize=``` 핫 메서드를 이 크기 이하로 인라이닝 +- ```XX:InlineSmallCode=``` 최종 단계 컴파일이 이미 존재할 경우 메서드를 인라이닝하지 않는다. +- ```XX:MaxInlineLevel=``` 이 수준보다 더 깊이 호출 프레임을 인라이닝하지 않는다. + +# 10.4 루프 펼치기 +루프 내부의 메서드 호출을 전부 인라이닝하면, 컴파일러는 루프를 한번 순회할 때마다 비용과 크기를 더 분명하게 알 수 있다. +백브랜치(back branch)가 일어나면 그때마다 CPU는 유입된 명령어 파이프라인을 덤프하기 때문에 성능상 바람직하지 않는다. +일반적으로 루프 바디가 짧을수록 백 브랜치 비용은 상대적으로 높기 때문에 다음의 기준으로 루프 펼치기 여부를 결정한다. + +- 루프 카운터 변수 유형(대부분 객체 아닌 int나 long형 사용) +- 루프 보폭(loop stride, 한번 순회할 때마다 루프 카운터 값이 얼마나 바뀌는 지) +- 루프 내부의 탈출 지점 개수(return 또는 break) + +성능이 얼마나 차이가 나는지 JMH 벤치마크로 비교할 수 있다. +```Java + +package optjava.jmh; + +import org.openjdk.jmh.annotations.*; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class LoopUnrollingCounter { + private static Final int MAX = 1_000_000; + private long[] data = new long[MAX]; + + @Setup + public void createData() { + java.util.Random random = new java.util.Random(); + for (int i = 0; i < MAX; i++) { + data[i] = random.nextLong(); + } + } + + @Benchmark + public long intStride1() { + long sum = 0; + for (int i = 0; i < MAX; i++) { + sum += data[i]; + } + return sum; + } + + @Benchmark + public long longStride1() { + long sum = 0; + for (long l = 0; l < MAX; l++) { + sum += data[(int) l]; + } + return sum; + } +} +``` + +벤치마크 결과 +| Benchmark | Mode | Cnt | Score | Error | Units | +|------------------------------------------|-------|-----|------------|---------|---------| +| UnitsLoopUnrollingCounter.intStride1 | thrpt | 200 | 2423.818 ± | 2.547 | ops/s | +| LoopUnrollingCounter.longStride1 | thrpt | 200 | 1469.833 ± | 0.721 | ops/s | + +int형 카운터 루프의 처리량이 약 64% 더 높다. +-> 어셈블리 수준까지 타고 내려가보면 long형 카운터를 쓸 경우 루프 바디가 펼쳐지지 않고 루프 안에 세이브포인트 폴이 박히기 때문 + +## 10.4.1 루프 펼치기 정리 +핫스팟은 다양한 최적화 기법으로 루프 펼치기를 한다. + +- 카운터가 int, short, char 형일 경우 루프를 최적화한다. +- 루프 바디를 펼치고 세이프포인트 폴을 제거한다. +- 루프를 펼치면 백 브랜치 횟수가 줄고, 그만큼 분기 예측 비용이 적게 든다. +- 세이프포인트 폴을 제거하면 루프를 순회할 때마다 하는 일이 줄어든다. + +# 10.5 탈출 분석 +핫스팟은 어떤 메서드가 내부에서 수행한 작업을 그 메서드 경계 밖에서도 볼 수 있는지, 또는 부수 효과를 유발하지 않는지 범위 기반 분석을 통해서 판별한다. +이러한 기법을 탈출 분석이라고 하며 메서드 내부에서 할당된 객체를 메서드 범위 밖에서 바라볼 수 있는지를 알아보는 용도로 사용된다. + +일반적으로 세가지 유형으로 분류할 수 있다. +``` +typedef enum { + // 객체가 메서드/스레드를 탈출하지 않고 + // 호출 인수로 전달되지 않으며, + // 스칼라로 대체 가능하다. + NoEscape = 1, + + // 객체가 메서드/스레드를 탈출하지 않지만 + // 호출 인수로 전달되거나 레퍼런스로 참조되며, + // 호출 도중에는 탈출하지 않는다. + ArgEscape = 2, + + // 객체가 메서드/스레드를 탈출한다. + GlobalEscape = 3 +} +``` +## 10.5.1 힙 할당 제거 +핫스팟의 탈출 분석 최적화는 개발자가 객체 할당률을 신경 쓰지 않고도 자바 코드를 자연스레 작성할 수 있도록 설계되었다. + +스칼라 치환(scalar replacement)이라는 최적화를 적용해 객체 필드를 마치 처음부터 객체 필드가 아닌 지역 변수였던 것처럼 스칼라 값으로 바꾼다. +그 후 레지스터 할당기(register allocator)라는 핫스팟 컴포넌트에 의해 CPU 레지스터 속으로 배치한다. + +## 10.5.2 락과 탈출 분석 +핫스팟은 탈출 분석 및 관련 기법을 통해서 락 성능도 최적화한다. +- 비탈출 객체에 있는 락은 제거한다. (락 생략) +- 같은 락을 공유한, 락이 걸린 연속된 영역은 병합한다. (락 확장) +- 락을 해제하지 않고 같은 락을 반복 획득한 블록을 찾아낸다. (중첩 락) + +## 10.5.3 탈출 분석의 한계 +- 다른 최적화 기법들처럼 트레이드오프가 있다. +- 힙이 아니라도 다른 어딘가에는 할당을 해야 하는데, CPU 레지스터나 스택 공간은 상대적으로 희소한 리소스이다. +- 기본적으로 원소가 64개 이상인 배열은 핫스팟에서 탈출 분석의 혜택을 볼 수 없으며, 이 개수 제한은 다음 VM 스위치로 조정한다. + + +# 10.6 단형성 디스패치 +- 핫스팟 C2 컴파일러가 수행하는 추측성 최적화는 대부분 경험적 연구 결과를 토대로 하며, 단형성 디스패치 기법도 그런 부류 중 하나이다. +- 즉, 어떤 객체에 있는 메서드를 호출할 때, 그 메서드를 최초로 호출한 객체의 런타임 타입을 알아내면 그 이후의 모든 호출도 동일한 타입일 가능성이 크다. +- 이 추측을 통해서 호출부의 메서드 호출을 최적화할 수 있다. +- 일반 애플리케이션에서는 대부분이 단형적 호출이지만 이형성 디스패치나 다형성도 지원한다. + + +# 10.7 인트린직 +- 인트린직(intrinsics)은 JIT 서브시스템이 동적 생성하기 이전에 JVM이 이미 알고 있는 고도로 튜닝된 네이티브 메서드 구현체를 가리키는 용어이다. +- 주로 OS나 CPU 아키텍처의 특정 기능을 응용하는, 성능이 필수적인 코어 메서드에서 쓰이며, 플랫폼에 따라 지원되는 경우도 있고 안되는 경우도 있다. + +인트리직화한 메서드 +| 메서드 | 설명 | +|--------------------------------------------|--------------------------------------------------------------| +| `java.lang.System.arraycopy()` | CPU의 벡터 지원으로 배열을 빠르게 복사한다. | +| `java.lang.System.currentTimeMillis()` | 대부분의 OS가 제공하는 구현체가 빠르다. | +| `java.lang.Math.min` | 일부 CPU에서 분기 없이 연산 가능하다. | +| 기타 `java.lang.Math` 메서드 | 일부 CPU에서 직접 명령어를 지원한다. | +| 암호화 함수 (예: AES) | 하드웨어로 가속하면 성능이 매우 좋아진다. | +| `.ad` 확장자를 가진 파일 (OpenJDK 핫스팟) | 이것은 인트린직 템플릿이다. | + +인트린직의 핵심 중 하나는 정말 자주 쓰이는 작업에 한해서만 성능에 큰 영향을 미칠 수 있다. + + +# 10.8 온-스택 치환 +- main() 메서드와 같이 컴파일을 일으킬 정도로 호출 빈도가 높지는 않지만 메서드 내부에 핫 루프가 포함된 경우가 있는데, 핫스팟은 이런 코드를 온-스택 치환(OSR)을 이용해서 최적화한다. +- 인터프리터가 루프 백 브랜치 횟수를 세어보고 특정 한계치를 초과하면 루프를 컴파일한 후 치촨해서 실행한다. + + +# 10.9 세이프포인트 복습 +JVM에 세이프포인트가 걸리는 조건을 종합 정리해보면 다음과 같다. + +- 메서드를 역최적화 +- 힙 덤프를 생성 +- 바이어스 락을 취소 +- 클래스를 재정의 + +핫스팟에서는 다음 지점에 세이프포인트 체크 코드를 넣는다. +- 루프 백 브랜치 지점 +- 메서드 반환 지점 + +따라서 경우에 따라 스레드가 세이프포인터에 도달하려면 어느 정도 시간이 소요될 수 있다. +또한 컴파일러는 세이프포인트를 폴링하면서 체크하는 비용을 감수할지, 다른 스레드도 모두 세이프포인테 닿을 때까지 대기하는 긴 세이프포인트를 회피할 지에 대해 고민하게 된다. + + +# 10.10 코어 라이브러리 메서드 +JDK 코어 라이브러리 크기가 JIT 컴파일에 어떤 영향을 주는지 살펴봐야한다. + +## 10.10.1 인라이닝하기 적합한 메서드 크기 상한 +인라이닝 여부는 메서드의 바이트 코드 크기로 결정되므로, 클래스 파일을 정적 분석하면 인라이닝을 하기에 지나치게 큰 메서드를 솎아낼 수 있다. + +- 도메인에 특정한 메서드로 성능 개선한다. +- 메서드를 작게하는 것도 인라이닝 가짓수를 늘여서 장점을 가진다. + +## 10.10.2 컴파일하기 적합한 메서드 크기 상한 +- 핫스팟은 메서드 크기가 어느 이상 초과하면 컴파일되지 않는 한계치(8000바이트)가 있다. +- 운영계 JVM에서는 이 수치를 바꿀 수 없지만, 디버그 JVM에서는 ```--XX:HugeMethodLimit=```스위치로 컴파일 가능한 메서드 바이트코드의 최대 크기를 설정할 ㅜ 있다.