V8 엔진 내부 — Hidden Class, Inline Cache, JIT 최적화
V8은 Chrome과 Node.js에서 사용되는 자바스크립트 엔진입니다. 동적 타입 언어인 자바스크립트를 어떻게 빠르게 실행하는지 내부 원리를 이해하면, 성능 좋은 코드를 작성하는 데 큰 도움이 됩니다.
V8의 실행 파이프라인
소스 코드 → 파서(AST) → Ignition(인터프리터, 바이트코드)
↓ (핫 코드 감지)
TurboFan(JIT 컴파일러, 기계어)
- Ignition: 바이트코드 인터프리터로 빠르게 실행 시작
- TurboFan: 자주 실행되는 코드(핫 코드)를 최적화된 기계어로 컴파일
- Deoptimization: 최적화 가정이 깨지면 바이트코드로 폴백
Hidden Class (Maps)
V8은 동적 객체에 정적 언어처럼 구조(Shape)를 부여합니다.
// V8은 객체의 "형태"를 추적함
function Point(x, y) {
this.x = x; // Hidden Class C0 → C1 (x 추가)
this.y = y; // Hidden Class C1 → C2 (y 추가)
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
// p1과 p2는 같은 Hidden Class를 공유 → 최적화 가능!
// 같은 순서로 프로퍼티를 추가하면 같은 Hidden Class
const a = {};
a.x = 1;
a.y = 2;
const b = {};
b.x = 3;
b.y = 4;
// a와 b는 같은 Hidden Class
// 다른 순서 → 다른 Hidden Class (비효율)
const c = {};
c.y = 1; // 순서가 다름!
c.x = 2;
// c는 a, b와 다른 Hidden Class
성능 팁: Hidden Class 최적화
// 나쁜 예: 프로퍼티 추가 순서가 다름
function createUser(type) {
const user = {};
if (type === "admin") {
user.role = "admin";
user.name = "관리자";
} else {
user.name = "사용자"; // 순서가 다름!
user.role = "user";
}
return user;
}
// 좋은 예: 항상 같은 순서로 프로퍼티 초기화
function createUser(type) {
return {
name: type === "admin" ? "관리자" : "사용자",
role: type === "admin" ? "admin" : "user",
};
}
// 나쁜 예: 동적으로 프로퍼티 추가/삭제
const obj = { a: 1, b: 2 };
delete obj.b; // Hidden Class 변경 → 최적화 해제
// 좋은 예: 프로퍼티를 null로 설정
obj.b = null; // Hidden Class 유지
Inline Cache (IC)
프로퍼티 접근 위치를 캐시하여 반복 접근을 빠르게 합니다.
function getX(obj) {
return obj.x; // 이 접근 패턴을 캐시
}
// 같은 Hidden Class 객체를 반복 전달 → Monomorphic IC (가장 빠름)
const points = [
{ x: 1, y: 2 },
{ x: 3, y: 4 },
{ x: 5, y: 6 },
];
points.forEach(getX); // 모두 같은 형태 → 빠름
// 다른 Hidden Class 객체를 전달 → Megamorphic IC (느림)
getX({ x: 1 });
getX({ x: 1, y: 2 });
getX({ x: 1, y: 2, z: 3 });
getX({ a: 0, x: 1 }); // 형태가 다양 → 캐시 효과 감소
IC 상태
| 상태 | 설명 | 성능 |
|---|---|---|
| Monomorphic | 1가지 형태 | 가장 빠름 |
| Polymorphic | 2-4가지 형태 | 보통 |
| Megamorphic | 5가지 이상 | 느림 |
JIT 최적화와 Deoptimization
// TurboFan이 최적화하는 코드
function add(a, b) {
return a + b;
}
// 항상 숫자로 호출 → 정수 덧셈으로 최적화
for (let i = 0; i < 10000; i++) {
add(i, i + 1); // 핫 코드 → JIT 컴파일
}
// 갑자기 문자열 전달 → Deoptimization 발생!
add("hello", "world"); // 가정이 깨짐 → 바이트코드로 폴백
Deoptimization을 피하는 방법
// 1. 타입을 일관되게 유지
function process(value) {
return value * 2;
}
// 항상 숫자만 전달
// 2. try-catch를 최소 범위로
// 나쁜 예: 함수 전체를 try-catch로 감싸기
function processBad(data) {
try {
// 긴 로직... TurboFan이 최적화하기 어려움
} catch (e) {}
}
// 좋은 예: 에러 발생 가능 부분만 분리
function processGood(data) {
const parsed = safeParse(data); // try-catch는 이 안에서만
// 나머지 로직은 최적화 가능
}
// 3. arguments 사용 피하기
// 나쁜 예
function bad() {
return arguments[0] + arguments[1]; // 최적화 방해
}
// 좋은 예
function good(a, b) {
return a + b;
}
메모리 관련 최적화
// SMI (Small Integer) — 포인터 없이 직접 저장
const smi = 42; // 효율적
// HeapNumber — 힙에 별도 할당
const heapNum = 1.5; // 소수점 → 덜 효율적
// 배열 최적화
const packed = [1, 2, 3]; // PACKED_SMI_ELEMENTS (가장 빠름)
const withHoles = [1, , 3]; // HOLEY_SMI_ELEMENTS (구멍 있음)
const mixed = [1, "two", 3]; // PACKED_ELEMENTS (타입 혼합)
// 배열 성능 팁
// 1. 구멍 만들지 않기
// 2. 타입 혼합하지 않기
// 3. 배열 크기를 미리 알면 미리 할당
실전 성능 팁 요약
| 팁 | 이유 |
|---|---|
| 프로퍼티 추가 순서 일관성 | Hidden Class 공유 |
| 프로퍼티 delete 피하기 | Hidden Class 변경 방지 |
| 함수에 같은 타입 전달 | Monomorphic IC 유지 |
| 배열 타입 혼합 피하기 | 배열 요소 종류 최적화 |
| try-catch 범위 최소화 | JIT 최적화 범위 확대 |
**기억하기 **: V8은 Hidden Class로 동적 객체에 구조를 부여하고, Inline Cache로 프로퍼티 접근을 캐시합니다. "같은 형태의 객체를 일관되게 사용"하는 것이 V8 최적화의 핵심입니다. 실무에서는 마이크로 최적화보다 알고리즘과 아키텍처가 더 중요하지만, 면접에서는 이 원리를 아는 것이 플러스입니다.
댓글 로딩 중...