Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

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

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 선택 철학

GCpause처리량메모리 오버헤드
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_ATTRLOAD_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브라우저 표준
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)

장점:

  • 동적 타입/다형성 최적화.
  • 런타임 정보 활용.

단점:

  • 워밍업 시간(초기 느림).
  • 메모리 오버헤드.
  • 예측 불가능한 pause.

GraalVM — 둘 사이 다리

  • Native Image — Java 코드를 AOT 컴파일.
  • 시작 50ms, 메모리 90% 절감.
  • Spring Native, Quarkus, Micronaut — 서버리스·컨테이너에 적합.
  • 단점: Reflection·Dynamic Class Loading 제약.

Part 11 — 성능 분석 워크플로

CPU 프로파일링

  1. Flame Graph로 전체 그림.
  2. 핫스팟 함수 식별.
  3. 해당 함수의 어셈블리 확인(perf annotate, 또는 Compiler Explorer).
  4. 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항목)

  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 — 학습 자료

  • 책: 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년 현실:

작성 글자: 0원문 글자: 8,367작성 단락: 0/241