---
title: "V8이 JIT 컴파일러에서 Sea of Nodes를 버리고 CFG로 돌아가는 이유"
published: 2026-05-18T21:06:50.000Z
canonical: https://jeff.news/article/2997
---
# V8이 JIT 컴파일러에서 Sea of Nodes를 버리고 CFG로 돌아가는 이유

V8 팀이 JavaScript와 WebAssembly 최적화 컴파일러 Turbofan에서 Sea of Nodes 기반 중간 표현을 줄이고, CFG 기반 Turboshaft와 Maglev로 이동 중이라고 설명했다. 이론적으로는 자유로운 스케줄링이 장점이지만, 실제 JavaScript에서는 효과·제어 체인 관리가 너무 복잡하고 컴파일 시간이 느려져 실용성이 떨어졌다는 결론이다.

## V8이 ‘멋진 이론’에서 다시 실용 쪽으로 움직임

- V8 팀이 최상위 최적화 컴파일러 Turbofan에서 Sea of Nodes를 줄이고 CFG 기반 IR로 이동 중이라고 밝힘
  - JavaScript 백엔드는 이미 Turboshaft를 사용하고 있음
  - WebAssembly는 전체 파이프라인에서 Turboshaft를 쓰는 상태임
  - 아직 Sea of Nodes가 남은 곳은 일부 builtin 파이프라인과 JavaScript 프런트엔드인데, 각각 Turboshaft와 Maglev로 대체 중임

- Sea of Nodes는 원래 “명령 순서를 최대한 늦게 정하자”는 꽤 우아한 아이디어임
  - 전통적인 CFG는 기본 블록 안에서 명령 순서가 암묵적으로 정해져 있음
  - Sea of Nodes는 실제 값 의존성, 제어 의존성, 효과 의존성만 그래프로 표현해서 불필요한 순서 제약을 없애려 함
  - 예를 들어 `a * 2`와 `b * 2`가 서로 독립이면 어느 쪽을 먼저 계산해도 되니, 컴파일러가 나중에 더 좋은 스케줄을 고를 수 있음

- 문제는 JavaScript에서는 “자유롭게 떠다닐 수 있는 노드”가 생각보다 별로 없다는 것임
  - 프로퍼티 접근, 배열 로드, 타입 체크, bounds check, 문자열 연산 같은 것들이 대부분 부작용이나 제어 의존성을 가짐
  - 결국 많은 노드가 effect chain이나 control chain에 매달리면서 CFG처럼 굳어짐
  - V8 팀 표현대로라면 실전에서는 “순수 연산만 떠다니는 CFG”에 가까워지는 셈임

> [!IMPORTANT]
> V8 팀은 새 CFG 기반 Load Elimination이 기존 Turbofan 방식보다 최대 190배 빠른 경우를 봤다고 함. JIT에서는 컴파일도 런타임 비용이라, 이런 차이는 그냥 구현 취향 문제가 아님.

## Sea of Nodes가 V8에서 힘들어진 지점들

- 그래프를 사람이 읽기 너무 어려움
  - 컴파일러 엔지니어는 버그를 찾거나 최적화 기회를 보려고 IR 그래프를 자주 들여다봄
  - Sea of Nodes 그래프는 값 노드들이 떠다니는 구조라 루프, 종료 조건, 특정 명령 위치를 눈으로 찾기 힘듦
  - 반대로 CFG는 소스코드와 어셈블리 둘 다에 가까워서 “이 루프의 헤더가 어디고 문자열 concat이 어디 있는지”가 훨씬 잘 보임

- effect chain과 control chain을 수동으로 관리하는 비용이 너무 큼
  - JavaScript는 타입에 따라 분기하고, 객체 map을 확인하고, 필드를 읽고, 다시 타입을 확인하는 패턴이 흔함
  - 이런 과정에서 effect chain과 control chain이 거의 같은 모양으로 갈라졌다 합쳐지는데, Sea of Nodes에서는 둘을 따로 맞춰줘야 함
  - V8 팀은 실제로 JSNativeContextSpecialization 단계에서 여러 effect chain을 관리하다가 몇 달 뒤에야 실수를 발견한 사례를 소개함

- 메모리 연산도 기대만큼 자유롭게 이동하지 못함
  - `arr[0]`과 `arr[1]`을 읽고 조건에 따라 하나만 반환하는 코드가 있어도, 로드 순서와 effect chain 때문에 쉽게 각 분기 안으로 내려가지 못함
  - 이를 하려면 중간에 부작용이 없다는 분석을 해야 하는데, 이건 CFG에서 하는 분석과 크게 다르지 않음
  - 결국 Sea of Nodes를 써도 로드 이동 최적화가 자동으로 공짜가 되진 않음

- 스케줄러가 복잡해짐
  - Sea of Nodes는 마지막에 어셈블리를 만들기 위해 결국 명령 순서를 정해야 함
  - 중복 제거로 두 개의 나눗셈을 하나로 합쳤다가, 특정 분기에서만 필요하다는 이유로 다시 복제해야 하는 상황이 생김
  - “2개를 1개로 최적화했다가 다시 2개로 최적화”하는 식의 왕복이 생기고, 이 로직이 스케줄러를 무겁게 만듦

```mermaid
sequenceDiagram
    participant 소스코드
    participant Sea노드
    participant 최적화패스
    participant 스케줄러
    participant 어셈블리

    소스코드->>Sea노드: 값·제어·효과 의존성 그래프 생성
    Sea노드->>최적화패스: 순서 없는 노드를 따라 반복 방문
    최적화패스->>Sea노드: 중복 제거와 lowering 적용
    Sea노드->>스케줄러: 실제 실행 순서 계산 요청
    스케줄러->>스케줄러: 필요한 노드 복제와 배치 결정
    스케줄러->>어셈블리: 순서가 정해진 코드 생성
```

## 숫자로 봐도 손해가 보임

- 그래프 방문 순서가 나빠서 최적화 패스가 비효율적으로 돈다고 함
  - CFG에서는 앞에서 뒤로 노드를 방문하면 입력이 먼저 최적화된 상태라 한 번 방문으로 끝나는 경우가 많음
  - Sea of Nodes에서는 반환 노드 쪽에서 거꾸로 올라가다 보니 같은 노드를 여러 번 다시 방문하게 됨
  - V8 팀은 일반적인 JavaScript 프로그램에서 노드가 평균 20번 방문될 때 한 번만 실제로 변경되는 수준이었다고 밝힘

- 캐시 친화성도 떨어짐
  - Turbofan은 그래프를 in-place로 계속 바꾸는데, lowering 과정에서 새 노드가 여기저기 할당됨
  - 파이프라인 뒤로 갈수록 관련 노드가 메모리상 가까이 있지 않게 되고 캐시 미스가 늘어남
  - 새 CFG IR과 비교하면 Sea of Nodes는 L1 데이터 캐시 미스가 평균 3배, 일부 단계에서는 최대 7배 많았고, 컴파일 시간의 최대 5% 정도를 잡아먹는 것으로 추정됨

- 제어 흐름에 의존하는 타입 최적화도 제한적임
  - `if (x < 42) return x + 1` 같은 코드에서는 분기 안에서 `x + 1`이 31비트 small integer overflow를 낼 수 없다는 걸 알 수 있음
  - CFG에서는 이 사실을 블록 맥락으로 쉽게 활용해 checked add를 일반 add로 바꿀 수 있음
  - Sea of Nodes에서는 순수 연산이 떠다니기 때문에 그 연산이 분기 뒤에서 실행된다는 보장이 없어, 결국 control input을 붙여야 하고 다시 CFG스러워짐

## WebAssembly에서도 같은 문제가 터짐

- WebAssembly는 입력 코드가 이미 꽤 신중하게 스케줄링돼 있을 수 있음
  - C++ 개발자나 Binaryen, Emscripten 같은 AOT 툴체인이 레지스터 사용과 spill을 고려해 명령 순서를 잡았을 수 있음
  - Sea of Nodes는 구조상 기존 스케줄을 버리고 자기 스케줄러에 다시 맡김
  - JIT 시간 제약 때문에 이 새 스케줄이 원래보다 나빠질 수 있고, V8 팀은 실제로 WebAssembly에서 그런 사례를 봤다고 함

- JavaScript와 WebAssembly에 서로 다른 컴파일러를 쓰기도 어려웠음
  - 같은 컴파일러를 쓰면 두 언어 사이의 inlining 같은 최적화가 가능함
  - 그래서 WebAssembly만 CFG로, JavaScript만 Sea of Nodes로 분리하는 선택지가 깔끔하지 않았음
  - 결국 V8은 둘 다 CFG 기반 Turboshaft 쪽으로 옮기는 방향을 택함

## 결론은 꽤 현실적임

- Sea of Nodes는 이론적으로 강력하지만, V8의 JavaScript·WebAssembly JIT에는 너무 비쌌다는 판단임
  - 그래프가 읽기 어렵고, effect/control chain 관리가 버그를 부르고, 새 최적화를 넣기도 힘들었음
  - 많은 노드가 어차피 제약을 받아 자유로운 스케줄링 장점이 줄어듦
  - 컴파일 속도와 메모리·캐시 효율까지 손해를 보니 JIT 환경에서는 부담이 더 커짐

- V8 팀의 선택은 “덜 멋져 보여도 더 운영 가능한 IR”로 가는 흐름임
  - 컴파일러 내부 구조는 런타임 성능뿐 아니라 엔지니어가 계속 개선할 수 있는가도 중요함
  - Turboshaft와 Maglev는 그 지점에서 Sea of Nodes보다 실용적인 선택으로 보임

---

## 기술 맥락

- V8이 바꾼 건 단순히 자료구조 하나가 아니에요. JIT 컴파일러가 프로그램을 이해하고, 최적화하고, 마지막에 기계어로 바꾸는 전체 작업 방식이 바뀌는 거예요. Sea of Nodes는 자유도를 크게 주는 대신, 그 자유도를 관리하는 비용을 컴파일러가 계속 떠안아야 했어요.

- JavaScript에서는 이 비용이 특히 커져요. 객체 프로퍼티를 읽는 것만 해도 hidden class 확인, 타입 체크, 메모리 로드, deoptimization 지점이 줄줄이 붙거든요. 그래서 “순수하게 떠다니는 연산”보다 “순서를 지켜야 하는 연산”이 많아지고, Sea of Nodes의 장점이 줄어들어요.

- CFG로 돌아가는 이유는 보수적인 후퇴가 아니라, 최적화를 더 안정적으로 넣기 위한 선택에 가까워요. 루프가 어디 있는지, 어떤 블록에서 어떤 타입 조건이 성립하는지, 어떤 로드가 어느 경로에서 필요한지 보이기 쉬워야 Load Elimination이나 bounds check 제거 같은 최적화도 과감하게 넣을 수 있거든요.

- JIT에서는 컴파일 시간이 곧 사용자 체감 성능이에요. 최적화된 코드를 만들기 전까지는 덜 빠른 코드가 실행되고, 컴파일러는 가비지 컬렉터나 다른 컴파일 작업과 CPU를 나눠 써야 해요. 그래서 V8 팀이 최대 190배 빠른 분석, 평균 3배 적은 캐시 미스 같은 지표를 중요하게 보는 거예요.

- WebAssembly까지 같이 고려하면 선택은 더 명확해져요. WebAssembly 입력은 이미 AOT 툴체인이 어느 정도 좋은 순서로 만들어놨을 수 있는데, Sea of Nodes는 그 순서를 버려요. CFG 기반 IR은 기존 구조를 더 보존하면서도 필요한 최적화를 얹기 쉬워서, 브라우저 JIT 입장에서는 더 예측 가능한 기반이 돼요.

## 핵심 포인트

- V8의 JavaScript 백엔드는 이미 Turboshaft로 대체됐고 WebAssembly도 전체 파이프라인에서 Turboshaft를 사용함
- Sea of Nodes는 값 의존성만 남겨 연산을 자유롭게 배치할 수 있지만, JavaScript에서는 부작용과 타입 체크 때문에 대부분 노드가 다시 제약을 받음
- V8 팀은 새 CFG 기반 Load Elimination이 기존 방식보다 최대 190배 빠른 벤치마크를 확인함
- Sea of Nodes는 L1 데이터 캐시 미스가 평균 3배, 일부 단계에서는 최대 7배 많았고 컴파일 시간도 손해를 봄

## 인사이트

이 글은 ‘멋진 컴파일러 이론’이 대형 프로덕션 런타임에서 항상 이기는 건 아니라는 사례다. 특히 JIT에서는 최종 실행 속도만큼 컴파일 속도, 디버깅 난이도, 최적화 구현 비용이 다 같이 제품 품질이 된다.
