HW부터 JavaScript까지 — V8 엔진 실행 구조
한 문장 요약
JS 코드 → V8이 파싱 → Ignition이 바이트코드 실행 → 반복 코드는 TurboFan이 JIT 컴파일 → 기계어 직접 실행 → CPU → 트랜지스터
전체 실행 스택
┌─────────────────────────────────────────────────┐
│ JavaScript 소스 코드 │
│ function add(a, b) { return a + b; } │
│ console.log(add(1, 2)); │
└──────────────────────┬──────────────────────────┘
│ V8 엔진 (C++로 작성된 바이너리)
▼
┌─────────────────────────────────────────────────┐
│ 1단계: 파싱 (Parser) │
│ │
│ 소스 코드 → 토큰 → AST │
│ │
│ AST 예시: │
│ FunctionDeclaration (add) │
│ ├─ params: [a, b] │
│ └─ body: ReturnStatement │
│ └─ BinaryExpression(+) │
│ ├─ Identifier(a) │
│ └─ Identifier(b) │
│ │
│ Lazy Parsing: 사용 안 되는 함수는 나중에 파싱 │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 2단계: Ignition (인터프리터) │
│ │
│ AST → 바이트코드 (빠른 시작, 낮은 메모리) │
│ │
│ add 함수 바이트코드: │
│ ┌─────────────────────────────────────────┐ │
│ │ Ldar a // a를 accumulator로 로드│ │
│ │ Add r0, [0] // r0(b)와 더하기 │ │
│ │ Return // accumulator 반환 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Ignition은 레지스터 기반 VM │
│ (Python의 스택 기반과 다름) │
│ │
│ 동시에 프로파일링 데이터 수집: │
│ - 함수 호출 횟수 │
│ - 인자 타입 정보 (항상 숫자? 문자열?) │
└──────────────────────┬──────────────────────────┘
│ 반복 실행 감지 (Hot Code)
▼
┌─────────────────────────────────────────────────┐
│ 3단계: TurboFan (JIT 컴파일러) │
│ │
│ 프로파일링 정보 활용 → 최적화된 기계어 생성 │
│ │
│ 최적화 예시 (add가 항상 숫자를 받으면): │
│ ┌─────────────────────────────────────────┐ │
│ │ // 가정: a, b는 항상 SMI(Small Integer)│ │
│ │ mov eax, [a] │ │
│ │ add eax, [b] ← 단 3줄 기계어 │ │
│ │ ret │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Speculative Optimization (추측 최적화): │
│ 타입이 바뀌면 → Deoptimize → Ignition으로 복귀 │
│ │
│ Hidden Class (V8 내부): │
│ ┌──────────────────────────────────────────┐ │
│ │ let obj = {x: 1} → Class A {x: offset0}│ │
│ │ obj.y = 2 → Class B {x:0, y:4} │ │
│ │ → 프로퍼티 접근을 오프셋으로 최적화 │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ V8 힙 메모리 구조 │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Young Generation (New Space) │ │
│ │ ┌─────────────┬────────────────────┐ │ │
│ │ │ From Space │ To Space │ │ │
│ │ │ (현재 활성) │ (GC 복사 대상) │ │ │
│ │ └─────────────┴────────────────────┘ │ │
│ │ → Minor GC (Scavenge): 단명 객체 처리 │ │
│ │ │ │
│ │ Old Generation │ │
│ │ - 오래 살아남은 객체 │ │
│ │ → Major GC (Mark-Compact) │ │
│ │ │ │
│ │ Large Object Space │ │
│ │ Code Space (JIT 기계어 저장) │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ OS 시스템 콜 │
│ console.log → write(1, buf, n) 시스템 콜 │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ CPU Fetch-Decode-Execute │
│ JIT된 기계어 → 네이티브 실행 (C 수준 성능) │
│ Ignition 바이트코드 → V8 C++ 코드 경유 실행 │
└──────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 트랜지스터 / 전기 신호 │
└─────────────────────────────────────────────────┘
JIT 컴파일의 2가지 경로
JavaScript 함수 실행
┌──────────────────────────────────────────────────┐
│ 경로 1: 처음 실행 또는 가끔 호출 │
│ │
│ Ignition 바이트코드 → 인터프리팅 │
│ 속도: 보통 (Python보다 빠름, 네이티브보다 느림) │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 경로 2: 반복 호출 (Hot Function) │
│ │
│ TurboFan JIT 컴파일 → 기계어 캐시 │
│ 속도: 네이티브 코드 수준 │
│ 조건: 타입이 일정해야 최적화 유지 │
└──────────────────────────────────────────────────┘
타입이 갑자기 바뀌면 (add에 문자열 전달):
최적화 해제(Deopt) → Ignition으로 폴백 → 재프로파일링
Hidden Class: 동적 타입을 정적처럼 최적화
// 이렇게 쓰면 최적화 유리 (프로퍼티 순서 일정)
function Point(x, y) {
this.x = x; // 항상 먼저
this.y = y; // 항상 나중
}
내부 Hidden Class 전이:
Point() → Class C0 {}
.x = 1 → Class C1 { x: offset 0 }
.y = 2 → Class C2 { x: offset 0, y: offset 4 }
// 이렇게 쓰면 최적화 불리 (프로퍼티 순서 다름)
let a = {}; a.x = 1; a.y = 2; // Class C2
let b = {}; b.y = 2; b.x = 1; // Class C3 (다른 클래스!)
→ 같은 구조인데도 다른 Hidden Class → 인라인 캐시 미스
핵심 요약
app.js
↓ V8 파서 (AST 생성)
AST
↓ Ignition (바이트코드 컴파일 + 프로파일링)
바이트코드 (레지스터 기반 VM)
↓ 반복 실행 감지
├─ 가끔 실행 → Ignition 인터프리팅 (C++ 코드 실행)
└─ 자주 실행 → TurboFan JIT 컴파일 → 기계어 직접 실행
↓
CPU Fetch-Decode-Execute
↓
트랜지스터 ON/OFF
핵심: JS는 인터프리터도 아니고 완전 컴파일도 아닌
JIT(Just-In-Time) 방식으로 실행된다.
뜨거운(Hot) 코드만 선택적으로 네이티브로 컴파일.