V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법

7 min read
V8WebAssemblyWasmGCJIT성능 최적화
V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법

WebAssembly에는 JIT 최적화가 필요 없다는 게 기존 통념이었다. 정적 타입 정보가 있으니 컴파일 시점에 이미 최적화가 끝난다는 논리였다. 그런데 WasmGC가 등장하면서 이 전제가 흔들렸다.

왜 WasmGC에서는 상황이 다른가

기존 WebAssembly는 C나 Rust처럼 가상 메서드 디스패치가 없는 언어를 대상으로 설계됐다. 함수 포인터를 통한 간접 호출이 있어도, 어떤 함수가 호출될지 정적으로 알 수 있는 경우가 많았다.

WasmGC는 다르다. Java나 Dart 같은 관리형 언어를 WebAssembly로 컴파일하기 위한 확장이다. 이 언어들은 다형성이 핵심이다. 같은 메서드 호출이 런타임에 어떤 구체 클래스를 거칠지 컴파일 타임에 알기 어렵다.

[💡 잠깐! 이 용어는?] 가상 메서드 디스패치(Virtual Method Dispatch): 객체지향 언어에서 메서드 호출 시, 실제 호출될 구현을 런타임에 결정하는 메커니즘. animal.speak()를 호출하면 Dog인지 Cat인지에 따라 다른 구현이 실행된다. 호출마다 "어느 함수를 쓸지" 테이블을 조회하는 비용이 생긴다.


투기적 최적화란

투기(Speculation)는 "아마 이럴 거야"라는 가정을 코드에 굽는 것이다. 실행 중에 수집한 피드백을 보고, "이 호출 지점은 거의 항상 Dog.speak()를 부른다"는 걸 알면, 굳이 테이블 조회 없이 바로 Dog.speak()를 인라인으로 심어버릴 수 있다. 속도가 확 빠르다.

물론 가정이 틀릴 때를 대비해야 한다. 예상과 다른 타입이 오면 **디옵티마이제이션(Deoptimization)**이 발동해 최적화 전 코드로 되돌아간다.

[💡 잠깐! 이 용어는?] 디옵티마이제이션(Deoptimization): 투기적 최적화의 가정이 깨졌을 때 최적화 전 상태로 복귀하는 과정. V8은 프로그램 상태를 저장해두었다가, 가정이 실패하면 그 지점부터 기준 코드로 재실행한다. 세 단계(상태 저장 → 변환 → 점프)를 거친다.


V8이 구현한 방식

V8의 WasmGC 최적화는 두 레이어로 동작한다.

V8 WasmGC 최적화 파이프라인
[기준 컴파일러 (Baseline)]
  → 런타임에 호출 타겟 기록
  → "이 호출은 A 타입만 왔다", "B와 C가 섞였다" 등 피드백 수집
 
[최적화 컴파일러 (Turboshaft)]
  → 피드백 기반으로 최대 4개 타겟까지 인라인 처리
  → 인라인된 코드 안에서 추가 최적화 적용 가능
  → 가정 실패 시 → Deopt → 기준 코드로 복귀

[💡 잠깐! 이 용어는?] 인라이닝(Inlining): 함수 호출을 함수 본문으로 직접 교체하는 최적화. 호출 오버헤드가 없어지고, 인라인된 코드가 주변 컨텍스트와 함께 추가로 최적화될 여지가 생긴다. 짧은 함수에 특히 효과적이다.


실제 성능 향상 수치

환경성능 향상
Dart 마이크로벤치마크평균 1.59배 빠름
Google Sheets (Dart 기반)7% 향상
Flutter/Flute (Dart 기반)8% 향상
SQLite 포팅 (C 기반 WasmGC)1% 향상

Dart처럼 가상 메서드 디스패치가 많은 언어에서 효과가 집중된다. SQLite가 1%에 그치는 이유는 간접 호출 자체가 많지 않기 때문이다.

비유하면, 배달 기사가 "이 집은 항상 같은 동에 배달 간다"는 걸 경험으로 알면 네비를 매번 켜지 않고 바로 가는 것과 같다. 습관적 경로에는 최적화가 효과적이다. 가끔 이상한 주소가 오면 그때만 네비를 켜면 된다.


이전과 무엇이 달라졌나

기존 WebAssembly는 투기적 최적화가 필요 없었다. 정적 타입 정보가 있고, 관리형 객체를 다루는 WasmGC가 없었기 때문이다. WasmGC 이전에는 간접 호출이 있더라도 어느 정도 정적 분석이 가능했다.

WasmGC 이후로는 Java/Dart/Kotlin 같은 관리형 언어 전체가 WebAssembly 타깃이 됐다. 이 언어들은 JVM이나 Dart VM에서 수십 년간 쌓인 JIT 최적화 기법들의 혜택을 받아왔다. V8이 이 최적화를 WebAssembly 레이어에서 재구현하는 건 자연스러운 흐름이다.


마무리

WasmGC는 WebAssembly가 "C/C++/Rust용 실행 환경"을 넘어 "모든 관리형 언어의 실행 환경"으로 진화하는 신호다. 투기적 인라이닝은 그 진화에서 필수 기반 기술이다. Dart와 Java 앱이 브라우저에서 네이티브에 가까운 속도로 돌아가는 미래가 조금씩 가까워지고 있다.


참고:

관심 있을 만한 포스트

V8 Mutable Heap Numbers — 숫자 하나 바꿀 때마다 새 객체를 만들던 비효율을 잡다

V8 엔진이 스크립트 컨텍스트의 숫자 변수를 매번 새 HeapNumber로 할당하던 방식을 제자리 수정(mutable)으로 바꿔 최대 2.5배 성능 향상을 달성했다.

V8JavaScript

V8 Explicit Compile Hints — 주석 한 줄로 JavaScript 시작 속도를 630ms 줄이는 법

Chrome 136에 도입된 V8의 Explicit Compile Hints 기능으로 JavaScript 초기 로딩 성능을 개선하는 원리와 사용법을 분석한다.

V8성능 최적화

V8의 JSON.stringify가 2배 빨라졌다 — 6가지 최적화 기법 해부

V8 13.8(Chrome 138)에서 적용된 JSON.stringify 성능 개선의 기술적 배경과 6가지 핵심 최적화 전략을 분석한다.

V8JSON

Docfind — Rust와 WebAssembly로 만든 서버 없는 브라우저 검색 엔진

Microsoft VS Code 문서 팀이 Rust와 WebAssembly로 구현한 클라이언트 사이드 검색 엔진 Docfind의 내부 설계를 파헤친다.

RustWebAssembly

V8의 Sea of Nodes 탈출기 — 왜 우아한 이론이 실전에서 무너졌는가

V8 팀이 10년간 사용한 Sea of Nodes IR을 포기하고 Turboshaft로 전환한 7가지 이유와 그 교훈을 정리한다.

V8컴파일러

LCP 28초짜리 React 앱을 1초로 깎아낸 기록 — 4단계 성능 수술 프레임워크

번들 분석부터 에셋 최적화까지, React 앱의 LCP를 단계적으로 개선하는 실전 프레임워크를 다룬다.

React성능 최적화

AI 코딩의 맹점 — Artifacts 없이 에이전트는 기억을 잃는다

PRD, ADR, TDD가 AI 코딩 워크플로우에서 왜 선택이 아닌 필수인지, 실전 구조와 함께 살펴본다.

AI 코딩Artifacts

Next-Translate 3.0 — Turbopack과 App Router를 위한 i18n 재건

1년간 공백 후 돌아온 Next-Translate 3.0이 Turbopack 지원, 비동기 params, App Router 안정화를 한 번에 처리하는 방법.

Next.jsi18n