LabHub

블로그

컴파일러와 현대 언어 런타임 — LLVM, JIT, GC, V8 TurboFan/Maglev, Inline Caching, Escape Analysis, Rust Monomorphization 완벽 가이드 (2025)

한국어English日本語

왜 컴파일러/런타임을 알아야 하는가

2025년 현실:

이 글은 "내 코드가 실행되기까지 무엇이 일어나는가"를 추적한다.

Part 1 — 컴파일러 vs 인터프리터 vs JIT — 스펙트럼

전통 구분

현실은 하이브리드

"언어는 컴파일러인가 인터프리터인가"는 의미 없는 질문. "어떤 레이어로 구성되어 있는가"가 실질적 질문.

Part 2 — LLVM의 지배

왜 LLVM이 표준이 되었나

2000년 Chris Lattner의 박사 프로젝트. 2024년 현재:

LLVM IR

define i32 @add(i32 %a, i32 %b) {
  %sum = add i32 %a, %b
  ret i32 %sum
}

언어 중립 중간 표현. 최적화 패스 수백 개가 이 위에서 돈다.

MLIR (2019, Chris Lattner 재등장)

"다단계 IR." LLVM IR 한 단계가 아니라, 도메인별 중간 표현을 여러 레벨로.

왜 필요한가: 머신러닝 프레임워크(TensorFlow/PyTorch)가 고수준 그래프 → 저수준 연산까지 여러 추상 레벨을 관리해야 한다. LLVM IR만으로는 표현력이 부족.

2024년 MLIR 채택:

Part 3 — JIT 컴파일러의 대가들

V8의 4계층

2024년 기준:

바이트코드 (Ignition 인터프리터)
   ()
Sparkplug — 바이트코드를 1:1 기계어로(비최적화, 빠른 생성)
   (더 핫)
Maglev — 중간 최적화 JIT (2023 도입)
   (매우 핫)
TurboFan — 최대 최적화 (Sea of Nodes, 오래 걸림)

각 단계는 수집한 타입 피드백을 활용해 다음 단계에서 더 공격적 최적화.

역최적화(Deoptimization): 가정이 틀리면 하위 단계로 돌아감. 동적 언어 최적화의 핵심 매커니즘.

Hidden Class + Inline Caching

JavaScript 객체는 동적. obj.x의 주소가 고정이 아님. V8의 해결:

Hidden Class (aka Shape, Map):

Inline Cache (IC):

실무 교훈: JS 객체 형태를 안정적으로 유지하라.

// 나쁨: 조건부 속성 추가
const p = {};
if (cond) p.x = 1;
if (cond2) p.y = 2;

// 좋음: 모든 속성을 생성 시점에 선언
const p = { x: cond ? 1 : undefined, y: cond2 ? 2 : undefined };

Escape Analysis

"이 객체가 함수 밖으로 탈출하지 않는다면 → 스택 할당 or 스칼라 대체(scalar replacement)."

힙 할당은 비싸다. GC 압력. Escape analysis가 잘 되는 코드는 훨씬 빠르다.

Tiered Compilation

JVM: C1(빠른 JIT) → C2(공격적 JIT) → (OpenJDK 17+) GraalVM.

LuaJIT: 트레이스 기반 JIT. Mike Pall의 걸작. 2020년대에도 여전히 최고의 동적 언어 JIT로 인용됨.

Part 4 — Garbage Collector 계보

Mark & Sweep (1960)

Copying GC

Generational GC

Concurrent & Incremental GC

GC를 애플리케이션과 동시에 또는 조각내서 실행 → STW 최소화.

G1 GC (JDK 9+ 기본)

ZGC (JDK 11+, 2023 Production-Ready)

Shenandoah (RedHat, JDK 12+)

Go의 GC

CPython

V8

GC 선택 철학

GCpause처리량메모리 오버헤드
Parallel (JDK, 기본 X)높다낮다
G1
ZGC극저
Shenandoah극저
Go저-중
CPython RC거의 0낮다(카운터)낮다

Part 5 — Go 스케줄러

G-M-P 모델

각 P는 로컬 G 큐 보유. 비어있으면 다른 P에서 work-stealing.

특징

단점

Part 6 — Rust와 AOT의 힘

Monomorphization

fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }

let x = max(1i32, 2i32);    // max::<i32> 생성
let y = max(1.0f64, 2.0);   // max::<f64> 별도 생성

각 타입마다 전용 기계어 생성 → 호출 오버헤드 0, 최적 인라이닝. 단점: 바이너리 크기 증가(code bloat).

Zero-Cost Abstractions

"추상화를 사용한 코드가 직접 작성한 코드보다 느리지 않다."

Rust의 Borrow Checker

런타임 비용 0으로 메모리 안전성 달성. 컴파일 타임에 소유권·수명을 검증.

2024-2025 Rust 발전

Part 7 — Python 3.13의 혁명

Specializing Adaptive Interpreter (PEP 659, 3.11+)

CPython이 바이트코드에 형태별 특화된 명령을 런타임에 끼워넣기.

LOAD_ATTRLOAD_ATTR_INSTANCE_VALUE (dict 기반 객체)
LOAD_ATTR_SLOT (__slots__)
LOAD_ATTR_MODULE
...

V8의 Inline Cache를 CPython에 가져온 것.

3.13의 추가 변화

PyPy

Part 8 — JavaScript 런타임의 지형

V8 (Chrome, Node.js, Deno)

JavaScriptCore (Safari/WebKit)

SpiderMonkey (Firefox)

Bun의 JSC 선택

Bun은 V8 대신 JSC. Node보다 빠른 벤치 수치의 일부는 이 선택 덕분.

런타임 API 차이

Part 9 — WebAssembly 런타임

이전 글(WASM)에서 이미 다뤘지만 런타임 관점에서 한 번 더:

런타임특징어디서 쓰나
V8 + Wasm브라우저 표준
WasmtimeBytecode Alliance서버 WASI
Wasmer다양한 백엔드임베디드
WasmEdgeCNCF엣지 컴퓨팅
Wasmer + Cranelift빠른 컴파일개발
Wasmer + LLVM최적 코드프로덕션

Part 10 — AOT vs JIT의 트레이드오프

AOT (Rust, Go, Swift, GraalVM Native Image)

장점:

단점:

JIT (V8, JVM C2)

장점:

단점:

GraalVM — 둘 사이 다리

Part 11 — 성능 분석 워크플로

CPU 프로파일링

  1. Flame Graph로 전체 그림.
  2. 핫스팟 함수 식별.
  3. 해당 함수의 어셈블리 확인(perf annotate, 또는 Compiler Explorer).
  4. JIT 출력 보려면: Node --print-opt-code, V8 logging.

메모리 프로파일링

트레이스

Part 12 — 체크리스트 (12항목)

  1. 런타임 버전 최신 LTS — V8, JVM, Go, Python 모두 꾸준히 개선.
  2. GC 튜닝은 벤치 후 — 프리매처 최적화는 나쁨.
  3. JS 객체 형태 안정 — Hidden Class 안정화.
  4. Go는 escape analysis 의식-gcflags="-m"으로 확인.
  5. Rust는 #[inline]/PGO — 수동 힌트가 종종 필요.
  6. JVM은 JFR 기본 — 프로덕션 상시 프로파일.
  7. CPython은 3.13+ 검토 — 특화 인터프리터 성능 개선.
  8. Container 내 CPU 인식 — GOMAXPROCS, -XX:ActiveProcessorCount.
  9. Startup 민감 앱은 AOT 고려 — Native Image, Go AOT.
  10. JIT 워밍업 측정 — 벤치마크 초기값은 버려라.
  11. Allocation hot path 최소화 — 대부분 성능 문제의 원인.
  12. Compiler Explorer (godbolt.org) — 어셈블리 직접 확인 습관.

Part 13 — 10대 안티패턴

  1. "언어가 빨라서 빠르다" — 런타임 설정·구조가 더 크다.
  2. 벤치를 time ./app로 1번 측정 — 분산·워밍업 무시.
  3. JIT 워밍업 무시-Xcomp만 보고 판단.
  4. 마이크로벤치에 JMH 없이 System.nanoTime() — 오차 수십배.
  5. Generic 남용(monomorphization 폭발) — 바이너리 수십 MB.
  6. Python에서 tight loop을 pure Python으로 — NumPy/Cython/Numba 고려.
  7. Node.js에서 CPU 바운드를 main 이벤트루프 — worker_threads 필수.
  8. JVM에 Xms=Xmx 동일 없이 프로덕션 운영 — 힙 재할당 비용.
  9. GC 로그 미수집 — 프로덕션 장애 분석 불가.
  10. 컨테이너 기본 메모리로 JVM — OOM Kill 당함. -XX:+UseContainerSupport 기본이지만 확인.

Part 14 — 학습 자료

마치며 — 언어는 런타임이다

"어떤 언어를 쓸까"라는 질문은 종종 "어떤 런타임 특성을 원하는가"의 문제다.

모든 런타임은 절충이다. 같은 언어도 설정·버전·GC 선택으로 성능 프로필이 달라진다. 엔지니어의 무기는 "나는 V8의 Hidden Class를 깨뜨리지 않는다", "나는 Go의 escape analysis를 확인한다" 같은 런타임에 대한 구체적 이해다.

LLM이 코드를 잘 쓰는 시대에, "왜 이 코드가 빠르거나 느린가"를 설명할 수 있는 엔지니어의 가치는 더 커진다. 그 답은 대부분 컴파일러와 런타임에 있다.

다음 글 예고 — "AI 엔지니어링 실전" — LLM API 아키텍처, RAG, 에이전트, 파인튜닝, 벡터 DB, 평가, 프로덕션 운영

14편의 컴퓨터 시스템 여정 끝에, 다음은 그 모든 시스템 위에 서 있는 AI 애플리케이션이다.

"AI 프로덕트를 실제로 만드는 법." 다음 글에서.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다