Skip to content

필사 모드: JIT 컴파일 완전 가이드 2025: V8, JVM HotSpot, .NET, Tiered Compilation, 성능 분석

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

TL;DR

- **JIT = 인터프리터의 속도 + 컴파일러의 유연성**

- **Tiered Compilation**: 빠른 시작 + 점진적 최적화

- **V8 (Chrome, Node.js)**: Ignition (인터프리터) → Maglev → TurboFan

- **JVM HotSpot**: 인터프리터 → C1 → C2 (또는 Graal)

- **.NET**: ReadyToRun → Tier 0 → Tier 1

- **Inline Caching, Speculation, Deoptimization**: JIT의 핵심 기법

1. 컴파일 모델

1.1 3가지 접근

**1. 인터프리터**:

- 코드를 직접 실행

- 빠른 시작

- 느린 실행

**2. AOT (Ahead-Of-Time)**:

- 미리 기계어로 컴파일

- 빠른 실행

- 느린 시작 (컴파일 시간)

- 동적 최적화 X

**3. JIT (Just-In-Time)**:

- 런타임에 컴파일

- **둘의 장점 결합**

1.2 비교

| | 인터프리터 | AOT | JIT |

|---|---|---|---|

| **시작** | 빠름 | 느림 | 빠름 |

| **실행** | 느림 | 빠름 | 빠름 (warm-up 후) |

| **최적화** | 없음 | 정적 | **동적** |

| **메모리** | 적음 | 적음 | 많음 |

| **예** | Python (CPython) | C, Rust | Java, JS, C# |

1.3 JIT의 장점

**1. 프로파일 기반 최적화**:

- 어떤 코드가 자주 실행되는지 측정

- "hot" 코드만 최적화

**2. 가정 기반 최적화**:

- "이 변수는 항상 정수다"라고 가정

- 틀리면 deoptimize

**3. 인라이닝**:

- 함수 호출을 본문으로 대체

- 함수 호출 오버헤드 제거

**4. 동적 정보 활용**:

- 런타임 타입 정보

- 분기 예측 데이터

2. JIT가 작동하는 방식

2.1 기본 흐름

[Source Code]

↓ Parse

[AST]

↓ Compile to Bytecode

[Bytecode]

↓ Execute (Interpreter)

[Profile (어떤 함수가 hot?)]

↓ JIT Compile (hot 함수만)

[Native Code]

↓ Execute (Fast)

[Deoptimize if assumption wrong]

[Bytecode (다시)]

2.2 Hot Spot 감지

**Method-level**:

- 함수 호출 횟수 카운트

- 임계값 (예: 10,000) 넘으면 컴파일

**Loop-level**:

- 루프 반복 횟수 카운트

- "OSR (On-Stack Replacement)"

2.3 OSR — On-Stack Replacement

void process() {

for (int i = 0; i < 1_000_000_000; i++) {

// 매우 hot

}

}

문제: 함수가 한 번만 호출되어도 루프가 hot.

해결: **OSR** — 실행 중인 인터프리터를 컴파일된 코드로 교체.

[Interpreter at iteration 100,000]

↓ JIT compile

[Native code]

↓ OSR: 인터프리터 → 네이티브

[Iteration 100,001 in native code]

2.4 컴파일 임계값

| 시스템 | Method 임계값 | OSR 임계값 |

|---|---|---|

| **HotSpot C1** | 1,500 | 13,500 |

| **HotSpot C2** | 10,000 | 14,000 |

| **V8 Maglev** | ~1,000 | |

| **V8 TurboFan** | ~10,000 | |

너무 낮음: 컴파일 오버헤드.

너무 높음: 인터프리터에서 너무 오래.

3. V8 (Chrome, Node.js)

3.1 V8의 컴파일 파이프라인

[JavaScript]

↓ Parse

[AST]

↓ Bytecode

[Ignition (Interpreter)]

↓ Profile

[Sparkplug (baseline JIT)]

↓ Hot

[Maglev (mid-tier)]

↓ Hotter

[TurboFan (top-tier optimizer)]

3.2 Ignition — Bytecode Interpreter

**2016 도입**.

**역할**:

- AST를 bytecode로 컴파일

- 빠른 시작 (메모리 효율)

- 인라인 캐싱

**왜 인터프리터?**:

- 모바일에서 메모리 절약

- 시작 시간 단축

3.3 Sparkplug — Baseline JIT

**2021 도입**.

**역할**:

- Bytecode를 즉시 기계어로 (최적화 X)

- Ignition보다 5-15% 빠름

- 매우 빠른 컴파일 (인터프리터 직접 변환)

3.4 Maglev — Mid-tier Optimizer

**2023 도입**.

**역할**:

- TurboFan 전 단계

- Sparkplug보다 빠름

- TurboFan보다 컴파일 빠름

- Tier 사이 균형

3.5 TurboFan — Top-tier Optimizer

**2017 도입** (Crankshaft 대체).

**역할**:

- 가장 최적화된 코드 생성

- Sea-of-Nodes IR

- 인라이닝, 분기 예측, escape analysis

- SIMD 활용

**컴파일 비용**: 매우 큼 (~ms 단위), hot 함수만.

3.6 Inline Caching

JavaScript의 동적 디스패치 최적화.

function getX(obj) {

return obj.x // obj의 형태를 모름

}

// Hidden Class 학습

getX({x: 1, y: 2}) // Class A

getX({x: 3, y: 4}) // Class A (재사용)

getX({x: 5, y: 6, z: 7}) // Class B (다른 형태)

V8이 학습:

- 첫 호출: monomorphic (Class A만)

- 두 번째: 여전히 monomorphic

- 세 번째: polymorphic (Class A, B)

- 5+: megamorphic (느림)

**Monomorphic이 가장 빠름**. 같은 형태 객체를 일관되게 사용.

3.7 Hidden Classes

JavaScript는 동적 타입이지만, V8 내부에서는 **숨은 클래스**를 사용:

const obj1 = {} // Class C0

obj1.x = 1 // Class C1 (x 필드 추가)

obj1.y = 2 // Class C2

const obj2 = {} // Class C0

obj2.x = 1 // Class C1 (재사용!)

obj2.y = 2 // Class C2 (재사용!)

**같은 순서로 필드 추가** → 같은 hidden class → 빠름.

**다른 순서면**:

const obj3 = {}

obj3.y = 1 // Class D1 (다른 경로)

obj3.x = 2 // Class D2

// obj1, obj2와 다른 클래스 → 느림

**팁**: 객체 생성 시 모든 필드를 한 번에:

function makePoint(x, y) {

return { x, y } // 모든 필드 같은 순서

}

4. JVM HotSpot

4.1 컴파일 파이프라인

[Java source]

↓ javac

[Bytecode (.class)]

↓ JVM

[Interpreter]

↓ Hot (1,500 호출)

[C1 (Client Compiler)]

↓ Very Hot (10,000 호출)

[C2 (Server Compiler)]

↓ 또는

[Graal]

4.2 C1 (Client Compiler)

**역할**:

- 빠른 컴파일

- 보통의 최적화

- 빠른 시작에 적합

**사용**: 짧은 프로그램, 데스크톱 앱.

4.3 C2 (Server Compiler)

**역할**:

- 매우 공격적 최적화

- 느린 컴파일

- 긴 실행 프로그램에 적합

**최적화**:

- 인라이닝

- 루프 풀기 (loop unrolling)

- Escape analysis

- Branch prediction

- Dead code elimination

- Constant folding

4.4 Tiered Compilation (JDK 8+)

**기본**: 인터프리터 → C1 → C2.

**장점**:

- 빠른 시작 (인터프리터 + C1)

- 최종적으로 최고 성능 (C2)

활성화 (기본)

java -XX:+TieredCompilation MyApp

C2만 (고성능)

java -XX:-TieredCompilation -server MyApp

4.5 Graal — 새로운 컴파일러

**Oracle이 만든 차세대 JIT**. C2 대체.

**장점**:

- Java로 작성 (C2는 C++)

- 더 나은 최적화 (특정 워크로드)

- 다른 언어 지원 (GraalVM)

java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler MyApp

4.6 Escape Analysis

public Point getPoint() {

Point p = new Point(1, 2); // 객체 생성

return p.x + p.y; // 객체가 함수 밖으로 안 나감

}

C2가 분석:

- `Point` 객체가 함수 밖으로 안 나감

- → **Heap에 할당 X, 스택에 할당** (매우 빠름)

- 또는 **scalar replacement** (필드만 사용)

**효과**: GC 부담 감소, 캐시 친화적.

4.7 Inlining

int square(int x) { return x * x; }

int sumSquares(int n) {

int sum = 0;

for (int i = 0; i < n; i++)

sum += square(i);

return sum;

}

C2가 인라이닝:

int sumSquares(int n) {

int sum = 0;

for (int i = 0; i < n; i++)

sum += i * i; // square 인라이닝

return sum;

}

**효과**:

- 함수 호출 오버헤드 제거

- 추가 최적화 가능 (loop fusion 등)

**한계**: 너무 큰 함수는 인라이닝 X (코드 크기 폭증 방지).

5. .NET CLR

5.1 컴파일 파이프라인

[C# source]

↓ Roslyn

[CIL (Common Intermediate Language)]

↓ CLR

[ReadyToRun (옵션, AOT)]

[Tier 0 (빠른 컴파일)]

↓ Hot

[Tier 1 (최적화)]

5.2 ReadyToRun (R2R)

**.NET Core 2.1+** AOT 컴파일.

**역할**:

- 시작 시간 단축

- IL을 미리 네이티브로

- 런타임에 추가 JIT 가능

dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true

**효과**:

- 시작 시간 50%+ 단축

- 메모리 절감

5.3 Tiered Compilation

**.NET Core 3.0+** 기본.

**Tier 0**: 빠른 컴파일, 최적화 적음.

**Tier 1**: 느린 컴파일, 공격적 최적화.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

int Square(int x) => x * x;

5.4 RyuJIT

**.NET Core**의 기본 JIT.

**개선** (.NET 8+):

- ARM64 지원

- 동적 PGO (Profile-Guided Optimization)

- 더 빠른 컴파일

5.5 NativeAOT

**.NET 7+**: 완전 AOT.

dotnet publish -p:PublishAot=true

**장점**:

- 매우 빠른 시작

- 작은 바이너리

- 컨테이너에 적합

- JIT 없음 → 메모리 적음

**단점**:

- 런타임 동적 최적화 X

- Reflection 제한

- 일부 라이브러리 호환 X

6. PGO — Profile-Guided Optimization

6.1 정적 PGO

1. **빌드 1**: 계측 코드 추가

2. **실행**: 프로파일 수집

3. **빌드 2**: 프로파일 사용해 최적화

Clang

clang -fprofile-generate -o app app.c

./app # profile.profdata 생성

clang -fprofile-use=profile.profdata -O2 -o app app.c

**효과**:

- 5-30% 성능 향상

- 분기 예측 개선

6.2 동적 PGO (.NET, JVM)

JIT가 런타임에 자동으로:

- 어떤 분기가 자주 taken인가?

- 어떤 함수가 hot인가?

- 어떤 객체 형태가 자주 보이나?

→ 다음 컴파일에 활용.

**장점**: 사용자 패턴에 자동 적응.

7. Tracing JIT

7.1 Method JIT vs Tracing JIT

**Method JIT** (HotSpot, V8):

- 함수 단위로 컴파일

- Java, JavaScript

**Tracing JIT** (PyPy, LuaJIT):

- 실행 경로(trace) 단위로 컴파일

- "이 루프의 이 분기"

- 매우 공격적 최적화

7.2 PyPy

Python 인터프리터 + tracing JIT.

**장점**:

- CPython보다 5-10배 빠름

- 100% 호환

**단점**:

- 시작 느림

- 메모리 많이 사용

- C 확장 호환 문제

7.3 LuaJIT

Lua의 tracing JIT.

**Mike Pall**의 작품. 종종 C와 비슷한 성능.

**사용**:

- WoW (Lua scripts)

- nginx (Lua module)

- OpenResty

7.4 Tracing의 어려움

- **Trace 폭증**: 가능한 경로가 너무 많으면

- **Trace 무효화**: 가정이 깨지면

- **메모리**: 많은 trace 저장

→ Method JIT가 더 일반적.

8. Speculation과 Deoptimization

8.1 Speculative Optimization

JIT는 가정을 합니다:

**예시**: V8

function add(a, b) {

return a + b

}

// 1000번 호출, 항상 정수

add(1, 2)

add(3, 4)

// V8: "a, b는 항상 정수다" → 정수 덧셈으로 최적화

**컴파일된 코드**:

; 정수 덧셈 (매우 빠름)

add eax, ebx

**가정 검증**: 매번 타입 체크.

8.2 가정이 깨지면?

add("hello", "world") // 문자열!

**문제**: 정수 덧셈 코드는 문자열 처리 못 함.

**해결**: **Deoptimization**.

[Native Code (정수 덧셈)]

↓ Type check 실패

[Deopt to Bytecode]

↓ Re-execute in interpreter

[String concatenation]

**비용**: deopt는 비쌈. 자주 발생하면 JIT가 포기.

8.3 Deopt 방지

**일관된 타입 사용**:

// 좋음 - 항상 정수

function add(a, b) {

return a + b

}

add(1, 2)

add(3, 4)

// 나쁨 - 타입 혼용

function process(x) {

if (typeof x === 'string') return x.toUpperCase()

return x * 2

}

8.4 Megamorphic 함수

function getName(obj) {

return obj.name

}

getName({name: "Alice"})

getName({name: "Bob", age: 30})

getName({name: "Charlie", age: 25, country: "KR"})

// 5+ 다른 hidden class → megamorphic

**Megamorphic = 느림**. V8의 inline cache가 polymorphic까지만 효율적.

9. 디버깅과 분석

9.1 V8

**Node.js**:

node --trace-opt --trace-deopt my-app.js

**출력**:

[marking 0x... for optimized recompilation]

[deoptimizing 0x... because of wrong map]

**Chrome DevTools**:

- Performance 탭

- "Bottom-Up" 분석

- Optimized vs not optimized

9.2 JVM

**JIT 컴파일 로그**:

java -XX:+PrintCompilation MyApp

**JITWatch**: GUI 도구.

**Async-profiler**: 샘플링 프로파일러.

./profiler.sh -d 30 -f profile.html <pid>

9.3 .NET

**dotnet-trace**:

dotnet-trace collect --process-id <pid>

**PerfView**: Windows 도구.

**BenchmarkDotNet**: 마이크로벤치마크.

[Benchmark]

public int Sum() {

int sum = 0;

for (int i = 0; i < 1000; i++) sum += i;

return sum;

}

10. 실전 — 빠른 코드 작성

10.1 V8 (JavaScript)

**Do**:

- ✅ 일관된 객체 형태 (모든 필드 같은 순서)

- ✅ 일관된 타입 (정수만, 문자열만)

- ✅ Hot 함수를 작게 (인라이닝 가능)

- ✅ Monomorphic 호출 사이트

**Don't**:

- ❌ 객체에 동적으로 필드 추가

- ❌ `delete` 키워드 (hidden class 무효화)

- ❌ try/catch in hot paths (V8은 try 안의 코드 최적화 X — 옛 버전)

- ❌ `arguments` 객체 (rest parameters 사용)

10.2 JVM

**Do**:

- ✅ 작은 메서드 (인라이닝)

- ✅ Final 필드/클래스 (devirtualization)

- ✅ Primitive 타입 우선

- ✅ Escape analysis 활용 (지역 객체)

**Don't**:

- ❌ 거대 메서드 (인라이닝 안 됨)

- ❌ 과도한 reflection (deoptimize)

- ❌ JIT warm-up 무시 (벤치마크에 중요)

10.3 .NET

**Do**:

- ✅ `[MethodImpl(MethodImplOptions.AggressiveInlining)]`

- ✅ `Span<T>` 사용 (heap 회피)

- ✅ struct 활용 (escape analysis)

- ✅ NativeAOT for short-lived

**Don't**:

- ❌ Boxing (object로 변환)

- ❌ LINQ in hot paths

- ❌ Reflection in hot paths

10.4 일반 원칙

1. **측정 먼저** — 추측하지 말기

2. **핫 루프에 집중** — Pareto 원칙

3. **JIT warm-up 고려** — 첫 1초는 느림

4. **벤치마크는 microbench가 아닌 실제 부하**

퀴즈

**답**: **인터프리터의 빠른 시작** + **컴파일러의 빠른 실행**. 처음에는 인터프리터로 빠르게 시작 → 자주 실행되는 "hot" 코드를 프로파일링 → JIT가 그 부분만 기계어로 컴파일. **AOT보다 우월한 점**: (1) 런타임 정보 활용 (실제 사용 패턴), (2) Speculative optimization (가정 기반), (3) Deoptimization (가정 틀리면 인터프리터로 돌아감). 단점: 메모리 사용량 증가, warm-up 필요.

**답**: JavaScript는 동적 타입이지만 V8 내부는 **숨은 클래스**로 객체 형태를 추적합니다. **같은 순서로 필드 추가** → 같은 hidden class → V8이 효율적으로 코드 생성. **다른 순서** → 다른 hidden class → 느림. **팁**: 객체 생성 시 모든 필드를 한 번에 (`{ x, y }`), `delete` 사용 X, 일관된 형태 유지. **Monomorphic > Polymorphic > Megamorphic** — 같은 형태로 호출하면 100배+ 빠를 수 있음.

**답**: **C1 (Client Compiler)**: 빠른 컴파일, 보통의 최적화. 짧은 프로그램이나 데스크톱 앱에 적합. **C2 (Server Compiler)**: 느린 컴파일, **공격적 최적화** (인라이닝, escape analysis, loop unrolling). 긴 실행 프로그램에 적합. **Tiered Compilation** (JDK 8+ 기본): 인터프리터 → C1 → C2 — 빠른 시작 + 최종 고성능. **Graal**은 C2를 대체할 차세대 컴파일러 (Java로 작성).

**답**: **Speculation**: JIT가 가정을 함 (예: "a, b는 항상 정수"). 가정 기반으로 매우 빠른 코드 생성. **Deoptimization**: 가정이 깨지면 인터프리터로 돌아감. 예: `add(1, 2)` 1000번 호출 → 정수 덧셈으로 최적화 → `add("hello", "world")` 호출 → 가정 깨짐 → deopt → 인터프리터에서 문자열 처리. **deopt는 비쌈**. 자주 발생하면 JIT가 그 함수를 포기. **일관된 타입 사용**이 핵심.

**답**: **NativeAOT 적합**: (1) **컨테이너/Lambda** — 빠른 시작 시간 필수, (2) **CLI 도구** — 짧은 실행, (3) **임베디드** — 작은 바이너리, (4) **JIT 메모리 부담 회피**. **장점**: 매우 빠른 시작, 작은 바이너리, JIT 없어 메모리 적음. **단점**: 런타임 동적 최적화 없음 (긴 실행 프로그램은 JIT가 더 빠를 수 있음), Reflection 제한, 일부 라이브러리 호환 안 됨. **.NET 7+, GraalVM Native Image**가 대표적. **장기 실행 서버는 JIT, 단기 실행은 AOT**.

참고 자료

- [V8 Blog](https://v8.dev/blog) — 공식 블로그

- [V8 Internals](https://v8.dev/docs)

- [JVM HotSpot](https://wiki.openjdk.org/display/HotSpot/Main)

- [GraalVM](https://www.graalvm.org/)

- [.NET Compilation](https://learn.microsoft.com/en-us/dotnet/standard/managed-execution-process)

- [LuaJIT](https://luajit.org/) — Mike Pall

- [PyPy](https://www.pypy.org/)

- [Tiered Compilation in JVM](https://docs.oracle.com/en/java/javase/17/vm/jvm-features.html)

- [JITWatch](https://github.com/AdoptOpenJDK/jitwatch)

- [Async-profiler](https://github.com/async-profiler/async-profiler)

- [BenchmarkDotNet](https://benchmarkdotnet.org/)

현재 단락 (1/403)

- **JIT = 인터프리터의 속도 + 컴파일러의 유연성**

작성 글자: 0원문 글자: 9,609작성 단락: 0/403