javascript · high priority
JavaScript 엔진 — 내부 동작
V8 파이프라인 · Hidden Class · Inline Cache · JIT · GC — JS 가 "빠른" 이유
학습 개요
탄생 배경
해결하려 했던 문제
초기 브라우저의 JS 엔진은 순수 tree-walking interpreter 로, 간단한 DOM 조작 이상의 작업에는 느렸다. AJAX · gmail 같은 앱이 등장하면서 JS 성능이 제품 경쟁력이 됐다.
역사적 맥락
2008 년 Google Chrome 이 V8 과 함께 등장하며 JS JIT 컴파일을 메인스트림에 올렸다. V8 의 초기 파이프라인은 Full-Codegen(baseline) + Crankshaft(optimizing). 2017 년 Ignition(bytecode interpreter) + TurboFan(modern JIT) 으로 재설계됐고, 2021 년 Sparkplug(baseline JIT) 을 추가해 "Ignition → Sparkplug → TurboFan" 삼각 구조가, 2023 년 Maglev(중간 티어)가 들어오며 현재의 4-티어 구조가 완성됐다. SpiderMonkey(Firefox) · JavaScriptCore(Safari) 도 유사한 다중 티어 구조로 수렴 중.
이전에는 어떻게 했나
SpiderMonkey 는 LLInt → Baseline Interp → Baseline JIT → WarpMonkey, JavaScriptCore 는 LLInt → Baseline JIT → DFG → FTL. 티어 이름과 IR 은 다르지만 "핫 패스를 점진 최적화 + 실패 시 deopt" 라는 구조는 공통이다.
멘탈 모델
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“식당 주방의 티어별 효율화”
주문이 처음 들어오면 신입 요리사(Ignition)가 레시피를 한 줄씩 읽으며 만든다. 같은 메뉴가 자주 나오면 선임(Sparkplug) 이 해당 레시피를 "바로 실행 가능한 체크리스트" 로 바꿔두고, 히트 메뉴가 되면 수석 셰프(Maglev/TurboFan) 가 "재료가 항상 A, B, C 라고 가정한 특화 공정" 을 짜 놓는다. 어느 날 재료가 D 로 바뀌면 "어? 가정 깨짐" 하고 신입에게 다시 맡긴다(deopt). 요리사별로 준비 레벨이 다르고, 손님이 같은 조합만 주문할수록 빨라진다.
핵심 개념
Ignition
레지스터 기반 바이트코드 인터프리터. 바이트코드가 **실행의 정식 형태** 이며 AST 는 곧 폐기된다. lazy 함수는 pre-parse 만 하고 실제 호출 시 다시 파싱.
Sparkplug (2021)
IR 없이 바이트코드를 선형 패스로 머신코드로 찍어내는 "non-optimizing baseline JIT". 컴파일 비용이 거의 0 이고, Ignition 대비 ~5-15% 속도 향상.
Maglev (2023)
SSA 기반이지만 공격적 최적화를 자제해 "빠른 중간 티어" 역할. Sparkplug ↔ TurboFan 사이의 큰 성능 격차를 채움.
TurboFan
최상위 최적화. IC 에서 수집된 타입 피드백을 바탕으로 인라이닝 · escape analysis · 타입 추론으로 머신코드를 생성. 전제가 깨지면 deopt.
바이트코드가 진짜 실행 단위
V8 은 AST 를 저장하지 않는다. 바이트코드만 유지하고, 필요하면 원본 소스를 다시 파싱한다. 이 설계 덕분에 메모리 절약이 크지만, "함수를 동적으로 생성하는" 패턴이 재파싱 비용을 부를 수 있다.
실무 적용
어떤 상황에서 사용하는가
성능 프로파일러에서 특정 함수가 핫 리스트에 올라왔는데, 코드를 읽어도 왜 느린지 모르겠다. `--trace-opt` / `--trace-deopt` 로 찍어보니 "deopt: wrong map" 이 반복된다.
어떻게 적용하는가
해당 함수가 받는 인자들의 Hidden Class 가 오염됐을 가능성. `console.log(obj)` 로 Chrome DevTools Memory 탭의 "Retainers" 를 보거나 `%HaveSameMap(a, b)` 같은 V8 flag 를 켜서 검증. 해결은 ① 생성자에서 모든 필드를 한번에 초기화, ② 조건부 추가 프로퍼티를 nullable 로 미리 선언, ③ `delete` 를 피하고 `null` 할당으로 대체, ④ 다형적 입력을 받는 hot 함수를 type 별로 분기하는 monomorphic wrapper 로 분할. 대부분 이 네 단계로 deopt 로그가 사라진다.
흔한 실수와 안티패턴
- 프로파일링 없이 "for 가 forEach 보다 빠르다" 같은 일반론으로 최적화 — 실제 병목은 다른 곳
- 클래스 필드 초기화를 conditional 로 하면 hidden class 가 갈라짐을 간과
- 배열을 `delete arr[i]` 로 지우고 PACKED 유지될 거라 착각
- `new Array(N)` 으로 pre-allocate 하면 빨라진다고 오해 (실제로는 HOLEY 로 시작)
- 수백만 개 객체를 freeze / seal 하면 빨라질 거라 기대 — 오히려 IC 가 특별 취급
흔한 오해
"JavaScript 는 인터프리터 언어다."
교정현대 엔진은 모두 JIT 컴파일러를 포함한 멀티 티어 시스템. 인터프리터는 시작 티어일 뿐이다.
왜 중요V8 기준 핫 함수는 머신코드로 컴파일돼 실행된다. "인터프리터" 라는 꼬리표는 ES3 시대의 유산.
"객체 프로퍼티를 추가하는 순서는 성능과 무관하다."
교정순서가 다르면 서로 다른 Hidden Class 가 만들어져 IC 가 오염된다.
왜 중요Hidden Class 는 "추가된 프로퍼티의 순서가 기록된 상태" 다. 같은 이름이라도 순서가 다르면 다른 class.
"delete 는 그냥 프로퍼티를 지우는 연산이다."
교정Hidden Class 전이를 발생시키고, 배열이라면 HOLEY 로 강제 downgrade 한다.
왜 중요엔진은 빈 슬롯을 특별 표시해야 하므로 레이아웃 최적화를 포기하게 된다. 프로퍼티를 "논리적으로 없음" 으로 처리하고 싶다면 `obj.x = undefined` 가 보통 더 낫다.
면접 질문
답변 방향 힌트
Ignition / Sparkplug / Maglev / TurboFan 순서로 컴파일 비용 vs 실행 속도 트레이드오프를 매핑하세요.
반드시 언급할 키워드
- Ignition = 바이트코드 인터프리터, 시작 즉시 실행 가능, 속도 느림
- Sparkplug = 바이트코드를 무-IR 선형 패스로 머신코드화, 거의 공짜 컴파일
- Maglev = SSA 기반 중간 최적화, TurboFan 과의 격차 보완 (2023)
- TurboFan = 타입 피드백 기반 공격적 speculative 최적화
- 승격 기준은 실행 카운터와 타입 피드백 안정성
- deopt 시 항상 Ignition 으로 복귀해 안전성 보장
예상 꼬리 질문
- 왜 Sparkplug 이 IR 없이 바이트코드를 직접 머신코드화하는 설계를 선택했나요?
- TurboFan 의 "bailout" 로그를 실제 앱 프로파일링에서 어떻게 활용할 수 있나요?
자기 점검
스크롤 올리지 말고 답해보세요. V8 의 4-티어 파이프라인 이름을 순서대로 말하고, 각 티어로 승격되는 조건 · deopt 동작을 설명하세요.
기대 키워드
자주 하는 오해
"JIT 이면 항상 최적화된다" 고 착각하지만, 대부분 함수는 hot 이 아니어서 Ignition/Sparkplug 에 머뭅니다. 최적화는 비용이 있기에 선별적입니다.
Hidden Class 와 IC 의 관계를 한 문장으로 설명하고, IC 를 megamorphic 으로 만드는 안티 패턴 2 개를 드세요.
기대 키워드
자주 하는 오해
"객체는 해시맵" 이라는 오래된 모델만 알면 왜 성능 차이가 생기는지 이해 못 합니다. 실제로는 구조체에 가깝습니다.
배열에서 `delete arr[i]` 와 `arr[i] = undefined` 는 GC·성능 관점에서 어떻게 다른가요?
기대 키워드
자주 하는 오해
"delete 가 의미적으로 더 깔끔하다" 고 선택하지만, V8 에서는 배열을 HOLEY 로 영구 전환시켜 최적화를 포기하게 만듭니다.
학습 자료
- JavaScript engine fundamentals: Shapes and Inline CachesHidden Class (Shape) 와 IC 의 메커니즘을 그림과 예시로 가장 명료하게 설명한 글. 이 주제의 정석.BlogMathias Bynens · V8 팀
- Sparkplug — a non-optimizing JavaScript compiler바이트코드를 IR 없이 선형 패스로 머신코드화하는 baseline JIT 의 설계 논리. "IR 이 왜 항상 필요하지 않은가" 의 실증.BlogV8 Blog · 2021
- Maglev — V8's Fastest Optimizing JIT2023 년에 추가된 중간 티어의 설계 의도 · Sparkplug / TurboFan 와의 차이.BlogV8 Blog · 2023
- Trash talk: the Orinoco garbage collectorV8 GC 의 현대적 구조 — concurrent marking, parallel sweep, incremental compaction.BlogV8 Blog
- Elements kinds in V8PACKED vs HOLEY · SMI vs DOUBLE 등 array elements kind 의 전이 규칙과 성능 영향.BlogV8 Blog
- Speculation in JavaScriptCoreSafari/JavaScriptCore 의 LLInt → Baseline → DFG → FTL 4-티어 구조. V8 과의 대조.BlogWebKit Blog