Split View: 컴파일러와 현대 언어 런타임 — LLVM, JIT, GC, V8 TurboFan/Maglev, Inline Caching, Escape Analysis, Rust Monomorphization 완벽 가이드 (2025)
컴파일러와 현대 언어 런타임 — 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 프로덕트를 실제로 만드는 법." 다음 글에서.
Compilers and Modern Language Runtimes — LLVM, JIT, GC, V8 TurboFan/Maglev, Inline Caching, Escape Analysis, Rust Monomorphization Complete Guide (2025)
Why You Should Know Compilers and Runtimes
Reality in 2025:
- V8, JVM, Go runtime, CPython, LLVM — tools you use daily.
- Their internal optimizations decide 70% of your app's performance.
- "Why is this code slow?" answers often hide in compiler decisions.
- Picking a language (Go vs Rust vs Python vs TS) is picking a runtime profile.
- In the AI era, million-QPS LLM inference runtimes are a compiler-optimization battle.
This post traces "what happens until my code runs."
Part 1 — Compiler vs Interpreter vs JIT — A Spectrum
Classical split
- Compiler: translate to machine code ahead-of-time (AOT).
- Interpreter: execute source one line at a time.
- JIT: compile hot parts to machine code during execution.
Reality is hybrid
- JVM: interpreter + C1 + C2 JIT + AOT (GraalVM).
- V8: Ignition (interpreter) + Sparkplug + Maglev + TurboFan (JIT).
- CPython: pure bytecode interpreter → 3.13 Specializing Adaptive Interpreter.
- .NET: bytecode + RyuJIT + AOT (NativeAOT).
"Is this language compiled or interpreted?" is the wrong question. "What tiers does its runtime have?" is the real one.
Part 2 — LLVM's Dominance
Why LLVM became the standard
Chris Lattner's 2000 PhD project. In 2024:
- Language frontends emit LLVM IR; optimization and codegen are LLVM's job.
- Languages using LLVM: Rust, Swift, Julia, Zig, Crystal, Kotlin/Native, Mojo, Chapel.
- Core tool for Apple Silicon migration.
- GPU code (CUDA/HIP) goes through LLVM.
LLVM IR
define i32 @add(i32 %a, i32 %b) {
%sum = add i32 %a, %b
ret i32 %sum
}
Language-neutral IR. Hundreds of optimization passes run on it:
- Constant Folding
- Dead Code Elimination
- Loop Invariant Code Motion
- Inlining
- Vectorization
- Instruction Combining
MLIR (2019, Chris Lattner returns)
"Multi-level IR." Not one LLVM IR, but domain-specific IRs at multiple levels.
Why needed: ML frameworks (TensorFlow/PyTorch) manage high-level graphs down to low-level ops across multiple abstractions. LLVM IR alone lacks the expressiveness.
MLIR adoption in 2024:
- Mojo (Modular AI) — Python superset, MLIR-based.
- IREE — ML compiler.
- Triton (OpenAI) — GPU kernel language.
Part 3 — JIT Masters
V8's four tiers
As of 2024:
Bytecode (Ignition interpreter)
↓ (hot)
Sparkplug — 1:1 bytecode-to-machine (non-optimizing, fast codegen)
↓ (hotter)
Maglev — mid-tier optimizing JIT (2023)
↓ (very hot)
TurboFan — peak optimization (Sea of Nodes, slow)
Each tier uses type feedback collected below to optimize more aggressively above.
Deoptimization: if assumptions break, fall back. Core mechanism of dynamic-language optimization.
Hidden Class + Inline Caching
JS objects are dynamic; obj.x has no fixed address. V8's answer:
Hidden Class (aka Shape, Map):
- Objects with identical property structure share a hidden class.
obj.xcompiles to "offset N in this hidden class."
Inline Cache (IC):
- Cache the property-access result at the call site.
- Repeated access on the same hidden class → cache hit → native speed.
- Shape changes → IC miss → polymorphic → megamorphic.
Lesson: keep JS object shapes stable.
// Bad: conditional property addition
const p = {};
if (cond) p.x = 1;
if (cond2) p.y = 2;
// Good: declare all properties at construction
const p = { x: cond ? 1 : undefined, y: cond2 ? 2 : undefined };
Escape Analysis
"If this object doesn't escape the function → stack-allocate or scalar-replace."
Heap allocation is expensive; GC pressure. Code friendly to escape analysis is much faster.
- JVM C2: strong escape analysis.
- Go compiler: check with
go build -gcflags="-m". - V8 TurboFan: limited.
Tiered Compilation
JVM: C1 (fast JIT) → C2 (aggressive JIT) → (OpenJDK 17+) GraalVM.
- Observe with
-XX:+PrintCompilation. - C2 is still among the strongest JITs in production.
LuaJIT: trace-based JIT. Mike Pall's masterpiece. Still cited as the best dynamic-language JIT in the 2020s.
Part 4 — GC Lineage
Mark & Sweep (1960)
- Mark reachable objects from roots.
- Sweep the unmarked.
- Cons: stop-the-world, fragmentation.
Copying GC
- Split into two spaces; copy live objects to the other.
- No fragmentation; 2x memory.
Generational GC
- Most objects die young (Weak Generational Hypothesis).
- Young/Old split; collect young often.
Concurrent and Incremental GC
Run GC concurrently or in small chunks to minimize STW.
G1 GC (default in JDK 9+)
- Region-based (~2048 heap regions).
- Predictable pause-time targets.
- Default for most server workloads.
ZGC (JDK 11+, Production-Ready in 2023)
- Colored Pointers — track object moves via color bits in references.
- Sub-millisecond pauses even on tens of TB heaps.
- 2023 Generational ZGC improves throughput too.
Shenandoah (RedHat, JDK 12+)
- Concurrent compaction.
- Competitor to ZGC.
Go's GC
- Concurrent mark & sweep.
- Tri-color abstraction + write barrier.
- STW target below 1ms (achieved since Go 1.5).
- Not generational — uses escape analysis to put more on the stack.
CPython
- Primarily reference counting + a cycle collector for cycles.
- Pros: deterministic release.
- Cons: counter ops on every alloc/dealloc; needs GIL.
V8
- Orinoco — concurrent, incremental, parallel.
- Young generation (new space): copying.
- Old generation (old space): mark-compact.
Choosing a GC
| GC | pause | throughput | memory overhead |
|---|---|---|---|
| Parallel (JDK, not default) | long | high | low |
| G1 | mid | mid | mid |
| ZGC | ultra-low | mid | mid |
| Shenandoah | ultra-low | mid | mid |
| Go | low | mid | low-mid |
| CPython RC | near 0 | low (counters) | low |
Part 5 — Go Scheduler
G-M-P model
- G: goroutine
- M: OS thread
- P: processor (logical CPU, usually GOMAXPROCS)
Each P has a local G queue. If empty, work-steals from another P.
Traits
- Cooperative preemption + Go 1.14+ asynchronous preemption (signal-based timeout).
- On syscall: even if M blocks, P is reassigned to another M immediately → other goroutines keep running.
- Netpoller integrates I/O with epoll/kqueue.
Limitations
- Limited NUMA-aware scheduling.
- GOMAXPROCS auto-detect is cgroups-limited — set manually in containers.
- Uber's
automaxprocslibrary fixes this.
Part 6 — Rust and the Power of AOT
Monomorphization
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
let x = max(1i32, 2i32); // generates max::<i32>
let y = max(1.0f64, 2.0); // generates max::<f64>
Dedicated machine code per type → zero call overhead, optimal inlining. Downside: binary bloat.
Zero-Cost Abstractions
"Code using abstractions is no slower than hand-written equivalent."
- Iterator chains compile to the same assembly as a for-loop.
- async/await fully unrolls into a state machine.
- Trait objects (
dyn Trait) cost, but static dispatch is zero.
Rust's Borrow Checker
Zero-runtime-cost memory safety. Compile-time ownership/lifetime verification.
Rust 2024-2025
- Native async trait (1.75).
- Parallel frontend (Nightly) — faster compiles.
- Cranelift backend — replaces LLVM in debug builds.
cargo-nextest— faster tests.
Part 7 — Python 3.13's Revolution
Specializing Adaptive Interpreter (PEP 659, 3.11+)
CPython injects shape-specialized bytecode instructions at runtime.
LOAD_ATTR → LOAD_ATTR_INSTANCE_VALUE (dict-based object)
→ LOAD_ATTR_SLOT (__slots__)
→ LOAD_ATTR_MODULE
→ ...
V8's Inline Cache, brought to CPython.
3.13 additions
- Experimental JIT (copy-and-patch). Experimental flag.
- Free-Threading (PEP 703) — GIL-less execution. Separate build option.
- Incremental GC.
PyPy
- Tracing JIT. 4-10x faster than CPython for years.
- Compatibility issues limit mainstream adoption.
- HPy (2020+) — portable C-extension API effort.
Part 8 — JavaScript Runtime Landscape
V8 (Chrome, Node.js, Deno)
- Released 2008. Lars Bak.
- The reference point for JS optimization.
JavaScriptCore (Safari/WebKit)
- 4-tier JIT (LLInt → Baseline → DFG → FTL).
- Custom B3 JIT compiler.
- Reputed to be more memory-efficient than V8.
SpiderMonkey (Firefox)
- IonMonkey JIT.
- Strong WebAssembly implementation.
Bun picks JSC
Bun uses JSC instead of V8. Part of its beating-Node benchmarks stems from this choice.
Runtime API differences
- Node.js: fs, net, http, CommonJS+ESM.
- Deno: Web API first + permissions + native TypeScript.
- Bun: Node API + Web API + built-in bundler/test runner.
- Workerd (Cloudflare): V8 isolates, limited Node compat.
Part 9 — WebAssembly Runtimes
Covered previously, but from a runtime angle:
| Runtime | Trait | Where |
|---|---|---|
| V8 + Wasm | Browser standard | Web |
| Wasmtime | Bytecode Alliance | Server WASI |
| Wasmer | Multiple backends | Embedded |
| WasmEdge | CNCF | Edge |
| Wasmer + Cranelift | Fast compile | Dev |
| Wasmer + LLVM | Best code | Prod |
Part 10 — AOT vs JIT Trade-offs
AOT (Rust, Go, Swift, GraalVM Native Image)
Pros:
- Fast startup.
- Predictable performance.
- Low runtime overhead.
Cons:
- No dynamic optimization (no type feedback).
- Binary size.
- Compile time.
JIT (V8, JVM C2)
Pros:
- Dynamic typing/polymorphism optimization.
- Uses runtime information.
Cons:
- Warmup (slow at first).
- Memory overhead.
- Unpredictable pauses.
GraalVM — the bridge
- Native Image — AOT-compile Java.
- 50ms startup, 90% memory reduction.
- Spring Native, Quarkus, Micronaut — serverless/container-friendly.
- Cons: reflection and dynamic class loading constrained.
Part 11 — Performance Analysis Workflow
CPU profiling
- Flame graph for the big picture.
- Identify hotspot functions.
- Inspect their assembly (
perf annotateor Compiler Explorer). - For JIT output: Node
--print-opt-code, V8 logging.
Memory profiling
- Chrome DevTools Heap Snapshot (V8).
- JFR + Mission Control (JVM).
- pprof (Go).
- memray (Python).
Tracing
- Linux perf + flame graph.
- Parca, Pyroscope — continuous profiling.
- async-profiler (JVM) — safepoint-free JVM profiler.
Part 12 — Checklist (12 items)
- Latest LTS runtime — V8, JVM, Go, Python all keep improving.
- GC tune only after benchmarking — premature optimization is bad.
- Keep JS object shape stable — stabilize Hidden Class.
- Mind Go escape analysis — check with
-gcflags="-m". - Rust:
#[inline]/PGO — manual hints often needed. - JVM: JFR always on — continuous prod profiling.
- Consider CPython 3.13+ — specialized interpreter gains.
- Container CPU awareness — GOMAXPROCS,
-XX:ActiveProcessorCount. - Startup-sensitive apps → AOT — Native Image, Go AOT.
- Measure JIT warmup — discard initial benchmark numbers.
- Minimize allocation hot paths — most perf issues are here.
- Compiler Explorer (godbolt.org) — habitual assembly inspection.
Part 13 — 10 Anti-patterns
- "This language is fast" — runtime config/structure matters more.
- Benchmark with
time ./apponce — ignoring variance/warmup. - Ignoring JIT warmup — judging from
-Xcomponly. - Microbenchmarking with
System.nanoTime()instead of JMH — tens of times off. - Generic abuse (monomorphization explosion) — tens of MB binaries.
- Tight loops in pure Python — consider NumPy/Cython/Numba.
- CPU-bound work on Node main event loop —
worker_threadsrequired. - JVM prod without Xms=Xmx — heap-resize cost.
- No GC logs — impossible to triage prod incidents.
- JVM on container default memory — gets OOM-killed.
-XX:+UseContainerSupportis default but verify.
Part 14 — Learning Resources
- Book: Crafting Interpreters (Robert Nystrom) — free online. Kindest intro.
- Book: Engineering a Compiler (Cooper & Torczon).
- Book: Modern Compiler Implementation in ML/Java/C (Andrew Appel).
- Book: The Garbage Collection Handbook (Jones, Hosking, Moss).
- Blog: V8 blog; Chrome V8 engineer talks on YouTube.
- Tool: godbolt.org — assembly playground.
- Course: Stanford CS143 Compilers (open).
Closing — Language Is Runtime
"Which language?" is often really "which runtime profile?"
- Ultra-low latency → Rust, Go AOT, GraalVM Native.
- Dev productivity first → Python, TypeScript, Kotlin.
- Polymorphism optimization → JVM C2, V8 TurboFan.
- Startup matters → AOT.
Every runtime is a trade-off. The same language shifts performance profile by config, version, GC choice. An engineer's weapon is concrete runtime knowledge: "I don't break V8's Hidden Class," "I verify Go's escape analysis."
In the LLM-writes-code era, engineers who can explain why code is fast or slow grow more valuable. The answer mostly lives in the compiler and the runtime.
Next — "AI Engineering in Practice" — LLM API architecture, RAG, agents, fine-tuning, vector DBs, evaluation, production ops
After 14 systems posts, the next sits on top of them all: AI applications.
- LLM API calls in practice — retries, timeouts, streaming, cost
- RAG architecture — from basic retrieval to Hybrid Search
- Agent design patterns — ReAct, Plan-and-Execute, Tool Use
- When (and when not) to fine-tune — LoRA, DPO, RLHF
- Vector DBs — pgvector vs Qdrant vs Pinecone
- LLM evaluation — the real difficulty of measuring accuracy
- Prompt engineering science — Structured Output, Few-shot, Chain-of-Thought
- LLM observability — OpenTelemetry GenAI, LangSmith, LangFuse
- Cost optimization — model choice, caching, Prompt Compression
- Security — Prompt Injection, Data Leakage
"How to actually ship an AI product." Next post.