FEInterview Prep

javascript · high priority

JavaScript 엔진 — 내부 동작

V8 파이프라인 · Hidden Class · Inline Cache · JIT · GC — JS 가 "빠른" 이유

advanced 난이도6시간토스카카오네이버라인쿠팡
시작 전
이해도
매우 낮음

학습 개요

탄생 배경

해결하려 했던 문제

초기 브라우저의 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 동작을 설명하세요.

기대 키워드

IgnitionSparkplugMaglevTurboFan실행 카운터타입 피드백deopt → Ignition

자주 하는 오해

"JIT 이면 항상 최적화된다" 고 착각하지만, 대부분 함수는 hot 이 아니어서 Ignition/Sparkplug 에 머뭅니다. 최적화는 비용이 있기에 선별적입니다.

Hidden Class 와 IC 의 관계를 한 문장으로 설명하고, IC 를 megamorphic 으로 만드는 안티 패턴 2 개를 드세요.

기대 키워드

Hidden ClassshapeInline Cachemonomorphicmegamorphicdelete조건부 property

자주 하는 오해

"객체는 해시맵" 이라는 오래된 모델만 알면 왜 성능 차이가 생기는지 이해 못 합니다. 실제로는 구조체에 가깝습니다.

배열에서 `delete arr[i]` 와 `arr[i] = undefined` 는 GC·성능 관점에서 어떻게 다른가요?

기대 키워드

HOLEY 전환element kind단방향 downgradeundefined 는 PACKED 유지

자주 하는 오해

"delete 가 의미적으로 더 깔끔하다" 고 선택하지만, V8 에서는 배열을 HOLEY 로 영구 전환시켜 최적화를 포기하게 만듭니다.

학습 자료