From 05b3f6b2caed5813f4b87f3ec628976cf2211258 Mon Sep 17 00:00:00 2001 From: Min Namgung Date: Wed, 21 Feb 2024 23:32:46 +0900 Subject: [PATCH] =?UTF-8?q?Chapter=2010=20-=20=EB=8B=A8=ED=98=95=EC=84=B1?= =?UTF-8?q?=20=EB=94=94=EC=8A=A4=ED=8C=A8=EC=B9=98=20&=20=EC=98=A8-?= =?UTF-8?q?=EC=8A=A4=ED=83=9D=20=EC=B9=98=ED=99=98=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\353\202\250\352\266\201\353\257\274.md" | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 "ch10/\353\202\250\352\266\201\353\257\274.md" diff --git "a/ch10/\353\202\250\352\266\201\353\257\274.md" "b/ch10/\353\202\250\352\266\201\353\257\274.md" new file mode 100644 index 0000000..3ea0bf0 --- /dev/null +++ "b/ch10/\353\202\250\352\266\201\353\257\274.md" @@ -0,0 +1,211 @@ +--- +marp: true +paginate: true +--- + +# 단형성 디스패치와 온-스택 치환 알아보기 + +*Mono-Morphic Dispatch & On-Stack Replacement* + +⎯ 남궁민 + +--- + +## 헷갈리는 개념들을 이해해보자 + +챕터 10에서 다루는 핫스팟 JIT 컴파일러의 최신 최적화 기법들 + +- 인라이닝 +- 루프 펼치기 +- 탈출 분석 +- 락 생략/확장 +- 단일형 디스패치 +- 인트린직 +- 온-스택 치환 + +--- + +- ~~인라이닝~~ - 👌 +- ~~루프 펼치기~~ - 👌 +- ~~탈출 분석~~ - 👌 +- ~~락 생략/확장~~ - 👌 +- 단일형 디스패치 - *조금 헷갈리는데...* +- ~~인트린직~~ - 👌 +- 온-스택 치환 - *엥 뭐지?* + +--- + +## 단형성 디스패치 최적화 + +*'사람이 작성한 코드를 보면
십중팔구 각 호출부마다 딱 한 가지 런타임 타입이 수신자 객체 타입이 된다'* + +⇒ 메소드를 최초로 호출한 객체의 런타임 타입을 알아내면
그 이후 모든 호출도 동일한 타입일 가능성이 크다 + +--- + +### '다형성' 코드 예시 + +```java +public int runMegamorphic() { + Shape currentShape = null; + + switch (random.nextInt(3)) { + case 0: + currentShape = triangle; + break; + case 1: + currentShape = square; + break; + case 2: + currentShape = octagon; + break; + } +} +``` + +--- + +### 컴파일러가 단형성 디스패치 최적화를 하는 이유 + +- 다형성의 매우 약한 보장 + - "object는 A, B, C 중에 하나이다." +- 단형성 디스패치의 동작 방식은 하드웨어의 동작 방식과 가깝다 +- 자바 같은 객체지향 언어는 **서브타입**과 **다형성**을 지원하는데, virtual invoke에 대한 하드웨어 지원은 없으므로 **메소드 디스패치**는 당연히 대가가 따른다 +- 다형성 디스패치는 런타임 전에는 객체가 생성되지 않아서 어떤 메소드를 호출해야 할지 알 수가 없다 + +--- + +### 정적 메소드, 추상 클래스 구현체, 인터페이스 구현체 + +- 정적 메소드 > 동적 추상 클래스 구현체 > 동적 인터페이스 구현체 순으로 성능이 빠르다 +- 정적 메소드는 **정적 바인딩**을 통해 메소드 디스패치를 생략하고, 어떤 메소드를 호출할지 바로 알 수 있기 때문 +- 동적 추상 클래스 구현체의 경우, 가상 메소드를 호출할 때 null check 비용 발생 +- 동적 인터페이스 구현체의 경우, 가상 메소드를 호출할 때 null check + 타입 체크 비용 발생 + +--- + +### 구현체별 벤치마크 결과 + +```sh +Benchmark (count) (p1) (p2) (p3) Mode Cnt Score Error Units +Three.dynamic_Abstract_Ref 10000 1 1 1 avgt 50 138.611 ± 2.524 us/op +Three.dynamic_Interface_Ref 10000 1 1 1 avgt 50 155.505 ± 3.027 us/op +Three.static_ID_ifElse 10000 1 1 1 avgt 50 99.868 ± 2.026 us/op +Three.static_ID_switch 10000 1 1 1 avgt 50 98.905 ± 0.600 us/op + +Three.dynamic_Abstract_Ref 10000 18 1 1 avgt 50 79.674 ± 2.423 us/op +Three.dynamic_Interface_Ref 10000 18 1 1 avgt 50 90.016 ± 0.940 us/op +Three.static_ID_ifElse 10000 18 1 1 avgt 50 57.578 ± 1.079 us/op +Three.static_ID_switch 10000 18 1 1 avgt 50 55.561 ± 0.169 us/op + +Three.dynamic_Abstract_Ref 10000 38 1 1 avgt 50 58.465 ± 0.160 us/op +Three.dynamic_Interface_Ref 10000 38 1 1 avgt 50 58.335 ± 0.209 us/op +Three.static_ID_ifElse 10000 38 1 1 avgt 50 51.692 ± 1.070 us/op +Three.static_ID_switch 10000 38 1 1 avgt 50 51.860 ± 0.156 us/op +``` + +--- + +### 단형성 디스패치 최적화 방식 + +- 정적 분석을 통해 다중 호출 타겟이 발생되었을 때 + - 컴파일러는 동적 타입 프로필을 활용한 추측성 최적화를 통해 정확한 receiver 타겟을 찾아낼 수 있다 +- C2 컴파일러는 단형성 및 이형성의 경우에 일반적으로 이 작업을 수행, 다형성의 경우에도 확실한 추정이 가능하면 수행 +- C1 컴파일러는 단형성의 경우에만 이 작업을 수행 + +--- + +### `instanceof` 연산자 사용하기 + +```java +if (currentShape instanceof Triangle) { + return ((Triangle) currentShape).getSides(); +} else { + return currentShape.getSides(); +} +``` + +- 다형성 디스패치를 이형성 디스패치로 변환해서 최적화할 수 있다 + +--- + +### 코드를 작성할 때 다형성을 포기하라는 얘기는 아니다 + +- 대부분의 경우 메소드 디스패치에 대한 성능 오버헤드는 무시할 수 있을 정도로 작음 +- 객체지향 언어에서 인터페이스를 작성하고 이에 대한 구현체를 작성하는 것은 매우 중요한 추상화 메커니즘 +- 다만 메소드 디스패치 성능이 중요할 만한 소수의 예외 케이스들이 있을 수 있다 +- 그래도 일반적인 상황에서 다형성에 대한 성능 걱정은 의미가 없다 + +--- + +## 온-스택 치환 + +### 온-스택 치환이 뭐야? + +- RET(Return Address): 함수 호출 이후 복귀 지점을 스택에 저장하는 명령어 +- OSR(On-Stack Replacement): 함수 호출 이후 복귀 지점을 변경하는 명령어 +- OSR은 길게 실행 중인 "핫" 메소드를 중간에 컴파일해서 최적화된 코드로 교체하는 기술 + +--- + +### OSR 단계 + +1. 런타임에서 "핫" 메소드를 '현재 변수값', '프로그램 카운터'를 통해 식별 +2. 메소드를 recompile +3. 컴파일러는 스택 활성화 프레임을 생성하고, 이전 버전에서 추출한 값들로 업데이트 +4. 이전의 스택 활성화 프레임을 새로운 버전으로 교체하고 새 버전에서 실행 + +--- + +### OSR 최적화 확인 예제 + +```java +public class OSRSample { + public static void main(String[] args) { + for (int i = 0; i < 100000; i++) { + System.out.println("hello ->" + i); + } + } +} +``` + +```sh +java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=osrsample.log OSRSample.java +``` + +--- + +```bash + +``` + +- `bytes` : 메소드 크기 +- `compiled_kind='osr'` : OSR 기법을 사용했음을 나타냄 +- `compiler='c1'` : C1 컴파일러를 사용했음을 나타냄 +- `count` : 컴파일러에 의해 메소드가 호출된 횟수 +- `backedge_count` : 루프가 포함된 메소드를 감지하는 용도, 인터프리터에 의해 숫자가 증가됨 +- `iicount` : 인터프리터가 메소드를 호출한 횟수 + +--- + +### OSR의 또다른 작동 방식 + +- C2 컴파일러의 추측성 최적화가 실패할 수 있음 + - 예시: 다형성 동적 메소드 호출을 정적 호출로 최적화했는데, 수신자 객체가 틀렸을 경우 +- 이런 경우 최적화된 스택 프레임을 다시 최적화되지 않은 스택 프레임으로 교체한다 + +--- + +## References + +- [The Black Magic of (Java) Method Dispatch](https://shipilev.net/blog/2015/black-magic-method-dispatch/) +- [What's up with monomorphism?](https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html) +- [최적화 기법 on-stack replacement](https://die4taoam.tistory.com/52) +- [Java -On Stack Replacement (OSR)](https://thangavel-blog.medium.com/java-on-stack-replacement-osr-b527ab3fff8c) +- [Differences between Just in Time compilation and On Stack Replacement](https://stackoverflow.com/questions/9105505/differences-between-just-in-time-compilation-and-on-stack-replacement)