✍️ 필사 모드: 컴파일러와 현대 언어 런타임 — LLVM, JIT, GC, V8 TurboFan/Maglev, Inline Caching, Escape Analysis, Rust Monomorphization 완벽 가이드 (2025)
한국어왜 컴파일러/런타임을 알아야 하는가
2025년 현실:
- V8, JVM, Go runtime, CPython, LLVM — 매일 쓰는 도구들.
- 이들의 내부 최적화가 당신 앱 성능의 70%를 결정.
- "이 코드 왜 느리지?"의 답이 컴파일러 결정에 숨어 있는 경우 다수.
- 언어 선택(Go vs Rust vs Python vs TS)은 런타임 특성 선택이다.
- AI 시대에 수백만 QPS LLM 인퍼런스 런타임은 컴파일러 최적화 싸움.
이 글은 "내 코드가 실행되기까지 무엇이 일어나는가"를 추적한다.
Part 1 — 컴파일러 vs 인터프리터 vs JIT — 스펙트럼
전통 구분
- 컴파일러: 전체 코드를 기계어로 미리 번역 (AOT, Ahead-of-Time).
- 인터프리터: 소스를 한 줄씩 즉시 실행.
- JIT: 실행 중 자주 쓰이는 부분을 기계어로 컴파일.
현실은 하이브리드
- JVM: 인터프리터 + C1 + C2 JIT + AOT(GraalVM).
- V8: Ignition(인터프리터) + Sparkplug + Maglev + TurboFan(JIT).
- CPython: 순수 바이트코드 인터프리터 → 3.13 Specializing Adaptive Interpreter.
- .NET: 바이트코드 + RyuJIT + AOT(NativeAOT).
"언어는 컴파일러인가 인터프리터인가"는 의미 없는 질문. **"어떤 레이어로 구성되어 있는가"**가 실질적 질문.
Part 2 — LLVM의 지배
왜 LLVM이 표준이 되었나
2000년 Chris Lattner의 박사 프로젝트. 2024년 현재:
- 언어 프론트엔드가 LLVM IR을 생성하면 이후 최적화·코드생성은 LLVM에 맡김.
- "LLVM을 쓴 언어": Rust, Swift, Julia, Zig, Crystal, Kotlin/Native, Mojo, Chapel.
- Apple Silicon 마이그레이션의 핵심 도구.
- GPU 코드(CUDA/HIP)도 LLVM 경유.
LLVM IR
define i32 @add(i32 %a, i32 %b) {
%sum = add i32 %a, %b
ret i32 %sum
}
언어 중립 중간 표현. 최적화 패스 수백 개가 이 위에서 돈다.
- Constant Folding
- Dead Code Elimination
- Loop Invariant Code Motion
- Inlining
- Vectorization
- Instruction Combining
MLIR (2019, Chris Lattner 재등장)
"다단계 IR." LLVM IR 한 단계가 아니라, 도메인별 중간 표현을 여러 레벨로.
왜 필요한가: 머신러닝 프레임워크(TensorFlow/PyTorch)가 고수준 그래프 → 저수준 연산까지 여러 추상 레벨을 관리해야 한다. LLVM IR만으로는 표현력이 부족.
2024년 MLIR 채택:
- Mojo (Modular AI) — Python 슈퍼셋, MLIR 기반.
- IREE — ML 컴파일러.
- Triton (OpenAI) — GPU 커널 언어.
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):
- 같은 속성 구조의 객체들이 같은 hidden class 공유.
obj.x는 "이 hidden class의 offset N"으로 컴파일.
Inline Cache (IC):
- 속성 접근 결과를 호출 지점에 캐시.
- 같은 hidden class 객체가 반복 호출되면 캐시 히트 → 네이티브 속도.
- 형태 바뀌면 IC miss → polymorphic → megamorphic으로 전이.
실무 교훈: 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가 잘 되는 코드는 훨씬 빠르다.
- JVM C2: 강력한 escape analysis.
- Go 컴파일러:
go build -gcflags="-m"으로 결과 확인 가능. - V8 TurboFan: 제한적.
Tiered Compilation
JVM: C1(빠른 JIT) → C2(공격적 JIT) → (OpenJDK 17+) GraalVM.
-XX:+PrintCompilation으로 관찰.- C2는 여전히 업계에서 가장 강력한 JIT 중 하나.
LuaJIT: 트레이스 기반 JIT. Mike Pall의 걸작. 2020년대에도 여전히 최고의 동적 언어 JIT로 인용됨.
Part 4 — Garbage Collector 계보
Mark & Sweep (1960)
- 루트부터 도달 가능 객체 마킹.
- 미마킹된 것 쓸어버림.
- 단점: Stop-the-world, 단편화.
Copying GC
- 두 영역 분할, 살아있는 객체를 한쪽으로 복사.
- 단편화 없음.
- 메모리 2배 필요.
Generational GC
- 대부분의 객체가 어리게 죽는다는 관찰(Weak Generational Hypothesis).
- Young/Old 영역 분리, 젊은 영역을 자주 수집.
Concurrent & Incremental GC
GC를 애플리케이션과 동시에 또는 조각내서 실행 → STW 최소화.
G1 GC (JDK 9+ 기본)
- Region 기반(2048개 힙 영역).
- 예측 가능한 pause time target.
- 대부분의 서버 워크로드에 기본.
ZGC (JDK 11+, 2023 Production-Ready)
- Colored Pointers — 객체 이동을 참조 자체의 색 비트로 관리.
- Sub-millisecond pause 보장(수십 TB 힙에서도).
- 2023년 Generational ZGC로 처리량까지 개선.
Shenandoah (RedHat, JDK 12+)
- 동시 압축.
- ZGC와 경쟁.
Go의 GC
- 동시 mark & sweep.
- 3색 추상 알고리즘 + write barrier.
- Stop-the-world 1ms 이하 목표(Go 1.5+에서 달성).
- Generational 아님 — 대신 "escape analysis로 스택에 많이 올림".
CPython
- Reference Counting 주 + 순환 참조용 Cycle Collector.
- 장점: 결정적 해제.
- 단점: 모든 할당/해제에 카운터 조작, GIL 필요.
V8
- Orinoco — 동시·증분·병렬.
- 젊은 세대(new space): Copying.
- 늙은 세대(old space): Mark-Compact.
GC 선택 철학
| GC | pause | 처리량 | 메모리 오버헤드 |
|---|---|---|---|
| Parallel (JDK, 기본 X) | 긴 | 높다 | 낮다 |
| G1 | 중 | 중 | 중 |
| ZGC | 극저 | 중 | 중 |
| Shenandoah | 극저 | 중 | 중 |
| Go | 저 | 중 | 저-중 |
| CPython RC | 거의 0 | 낮다(카운터) | 낮다 |
Part 5 — Go 스케줄러
G-M-P 모델
- G: goroutine
- M: OS thread
- P: processor (논리 CPU, 보통 GOMAXPROCS)
각 P는 로컬 G 큐 보유. 비어있으면 다른 P에서 work-stealing.
특징
- 협력적 선점 + Go 1.14+ 비동기 선점 (시간 초과 시 시그널로 중단).
- Syscall 진입 시 M이 블록되어도 P는 다른 M에 즉시 할당 → 다른 goroutine 계속.
- Netpoller로 I/O는 epoll/kqueue 통합.
단점
- NUMA 인지 스케줄링 제한적.
- GOMAXPROCS 자동 감지가 cgroups 인지 제한 — 컨테이너에서 수동 설정 권장.
automaxprocs라이브러리가 Uber에서 해결.
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
"추상화를 사용한 코드가 직접 작성한 코드보다 느리지 않다."
- Iterator 체인이 for-loop과 동일 어셈블리.
- async/await가 state machine으로 완전히 풀림.
- Trait 객체(dyn Trait)는 비용 있지만, 정적 디스패치는 0.
Rust의 Borrow Checker
런타임 비용 0으로 메모리 안전성 달성. 컴파일 타임에 소유권·수명을 검증.
2024-2025 Rust 발전
- async trait 네이티브 (1.75).
- Parallel frontend (Nightly) — 컴파일 속도 개선.
- Cranelift 백엔드 — 디버그 빌드에서 LLVM 대체 가능.
- cargo-nextest — 테스트 속도 개선.
Part 7 — Python 3.13의 혁명
Specializing Adaptive Interpreter (PEP 659, 3.11+)
CPython이 바이트코드에 형태별 특화된 명령을 런타임에 끼워넣기.
LOAD_ATTR → LOAD_ATTR_INSTANCE_VALUE (dict 기반 객체)
→ LOAD_ATTR_SLOT (__slots__)
→ LOAD_ATTR_MODULE
→ ...
V8의 Inline Cache를 CPython에 가져온 것.
3.13의 추가 변화
- Experimental JIT (copy-and-patch 기법). 실험적 플래그.
- Free-Threading (PEP 703) — GIL 없는 실행. 별도 빌드 옵션.
- incremental GC.
PyPy
- 트레이스 JIT. 수년간 CPython보다 4-10배 빠름.
- 호환성 이슈 때문에 대중 채택은 제한적.
- HPy (2020+) — C 확장 API의 이식성 개선 노력.
Part 8 — JavaScript 런타임의 지형
V8 (Chrome, Node.js, Deno)
- 2008년 등장. Lars Bak.
- 표준 JS 최적화의 기준.
JavaScriptCore (Safari/WebKit)
- 4계층 JIT(LLInt → Baseline → DFG → FTL).
- B3 JIT 컴파일러 자체 개발.
- 메모리 효율이 V8보다 낫다는 평.
SpiderMonkey (Firefox)
- IonMonkey JIT.
- WebAssembly 구현 품질 높음.
Bun의 JSC 선택
Bun은 V8 대신 JSC. Node보다 빠른 벤치 수치의 일부는 이 선택 덕분.
런타임 API 차이
- Node.js: fs, net, http, CommonJS+ESM.
- Deno: Web API 우선 + permissions + TypeScript 네이티브.
- Bun: Node API + Web API + 빠른 번들러/테스트러너 내장.
- Workerd (Cloudflare): V8 isolate, 제한된 Node 호환.
Part 9 — WebAssembly 런타임
이전 글(WASM)에서 이미 다뤘지만 런타임 관점에서 한 번 더:
| 런타임 | 특징 | 어디서 쓰나 |
|---|---|---|
| V8 + Wasm | 브라우저 표준 | 웹 |
| Wasmtime | Bytecode Alliance | 서버 WASI |
| Wasmer | 다양한 백엔드 | 임베디드 |
| WasmEdge | CNCF | 엣지 컴퓨팅 |
| Wasmer + Cranelift | 빠른 컴파일 | 개발 |
| Wasmer + LLVM | 최적 코드 | 프로덕션 |
Part 10 — AOT vs JIT의 트레이드오프
AOT (Rust, Go, Swift, GraalVM Native Image)
장점:
- 빠른 시작.
- 예측 가능한 성능.
- 런타임 오버헤드 적음.
단점:
- 동적 최적화 불가(타입 피드백 못 씀).
- 바이너리 크기.
- 컴파일 시간.
JIT (V8, JVM C2)
장점:
- 동적 타입/다형성 최적화.
- 런타임 정보 활용.
단점:
- 워밍업 시간(초기 느림).
- 메모리 오버헤드.
- 예측 불가능한 pause.
GraalVM — 둘 사이 다리
- Native Image — Java 코드를 AOT 컴파일.
- 시작 50ms, 메모리 90% 절감.
- Spring Native, Quarkus, Micronaut — 서버리스·컨테이너에 적합.
- 단점: Reflection·Dynamic Class Loading 제약.
Part 11 — 성능 분석 워크플로
CPU 프로파일링
- Flame Graph로 전체 그림.
- 핫스팟 함수 식별.
- 해당 함수의 어셈블리 확인(
perf annotate, 또는 Compiler Explorer). - JIT 출력 보려면: Node
--print-opt-code, V8 logging.
메모리 프로파일링
- Chrome DevTools Heap Snapshot (V8).
- JFR + Mission Control (JVM).
- pprof (Go).
- memray (Python).
트레이스
- Linux perf + flame graph.
- Parca, Pyroscope continuous profiling.
- async-profiler (JVM) — safepoint 이슈 없는 JVM 프로파일러.
Part 12 — 체크리스트 (12항목)
- 런타임 버전 최신 LTS — V8, JVM, Go, Python 모두 꾸준히 개선.
- GC 튜닝은 벤치 후 — 프리매처 최적화는 나쁨.
- JS 객체 형태 안정 — Hidden Class 안정화.
- Go는 escape analysis 의식 —
-gcflags="-m"으로 확인. - Rust는
#[inline]/PGO — 수동 힌트가 종종 필요. - JVM은 JFR 기본 — 프로덕션 상시 프로파일.
- CPython은 3.13+ 검토 — 특화 인터프리터 성능 개선.
- Container 내 CPU 인식 — GOMAXPROCS, -XX:ActiveProcessorCount.
- Startup 민감 앱은 AOT 고려 — Native Image, Go AOT.
- JIT 워밍업 측정 — 벤치마크 초기값은 버려라.
- Allocation hot path 최소화 — 대부분 성능 문제의 원인.
- Compiler Explorer (godbolt.org) — 어셈블리 직접 확인 습관.
Part 13 — 10대 안티패턴
- "언어가 빨라서 빠르다" — 런타임 설정·구조가 더 크다.
- 벤치를
time ./app로 1번 측정 — 분산·워밍업 무시. - JIT 워밍업 무시 —
-Xcomp만 보고 판단. - 마이크로벤치에 JMH 없이
System.nanoTime()— 오차 수십배. - Generic 남용(monomorphization 폭발) — 바이너리 수십 MB.
- Python에서 tight loop을 pure Python으로 — NumPy/Cython/Numba 고려.
- Node.js에서 CPU 바운드를 main 이벤트루프 — worker_threads 필수.
- JVM에 Xms=Xmx 동일 없이 프로덕션 운영 — 힙 재할당 비용.
- GC 로그 미수집 — 프로덕션 장애 분석 불가.
- 컨테이너 기본 메모리로 JVM — OOM Kill 당함.
-XX:+UseContainerSupport기본이지만 확인.
Part 14 — 학습 자료
- 책: Crafting Interpreters (Robert Nystrom) — 무료 공개. 가장 친절한 인터프리터 책.
- 책: Engineering a Compiler (Cooper & Torczon).
- 책: Modern Compiler Implementation in ML/Java/C (Andrew Appel).
- 책: The Garbage Collection Handbook (Jones, Hosking, Moss).
- 블로그: V8 blog, Chrome V8 Engineer talks on YouTube.
- 도구: godbolt.org — 어셈블리 실험 천국.
- 강의: Stanford CS143 Compilers (공개).
마치며 — 언어는 런타임이다
"어떤 언어를 쓸까"라는 질문은 종종 **"어떤 런타임 특성을 원하는가"**의 문제다.
- 지연이 극도로 낮아야 한다 → Rust, Go AOT, GraalVM Native.
- 개발 생산성이 최우선 → Python, TypeScript, Kotlin.
- 다형성 최적화 필요 → JVM C2, V8 TurboFan.
- 시작 시간 중요 → AOT.
모든 런타임은 절충이다. 같은 언어도 설정·버전·GC 선택으로 성능 프로필이 달라진다. 엔지니어의 무기는 "나는 V8의 Hidden Class를 깨뜨리지 않는다", "나는 Go의 escape analysis를 확인한다" 같은 런타임에 대한 구체적 이해다.
LLM이 코드를 잘 쓰는 시대에, **"왜 이 코드가 빠르거나 느린가"**를 설명할 수 있는 엔지니어의 가치는 더 커진다. 그 답은 대부분 컴파일러와 런타임에 있다.
다음 글 예고 — "AI 엔지니어링 실전" — LLM API 아키텍처, RAG, 에이전트, 파인튜닝, 벡터 DB, 평가, 프로덕션 운영
14편의 컴퓨터 시스템 여정 끝에, 다음은 그 모든 시스템 위에 서 있는 AI 애플리케이션이다.
- LLM API 호출의 실전 — 재시도, 타임아웃, 스트리밍, 비용
- RAG 아키텍처 — 단순 조회부터 Hybrid Search까지
- 에이전트 디자인 패턴 — ReAct, Plan-and-Execute, Tool Use
- 파인튜닝 언제, 언제 하지 말까 — LoRA, DPO, RLHF 비교
- 벡터 DB 실전 — pgvector vs Qdrant vs Pinecone, 언제 무엇
- LLM 평가(Eval) — 정확도 측정의 진짜 어려움
- 프롬프트 엔지니어링의 과학 — Structured Output, Few-shot, Chain-of-Thought
- LLM Observability — OpenTelemetry GenAI, LangSmith, LangFuse
- 비용 최적화 — 모델 선택, 캐싱, Prompt Compression
- 보안 — Prompt Injection, Data Leakage
"AI 프로덕트를 실제로 만드는 법." 다음 글에서.
현재 단락 (1/241)
2025년 현실: