본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

V8 — 밑에 깔린 엔진

~14 min · runtime, v8, jit, performance

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"V8은 JavaScript를 실행하는 엔진이야. Node는 그 엔진에 파일, 네트워크, 프로세스 같은 기능을 붙인 런타임이고."

소스 코드가 기계어에 가까워지는 과정

V8은 JavaScript 소스를 읽어 문법 구조를 만들고, 먼저 Ignition 바이트코드로 실행해. 처음 보는 코드를 곧바로 무겁게 최적화하면 준비 비용이 더 클 수 있으므로 빠르게 실행을 시작하는 쪽을 택하는 거야.

실행 중 같은 코드 경로가 반복되면 V8은 수집한 정보를 바탕으로 더 빠른 단계로 올려. Sparkplug와 Maglev가 중간 단계를 맡고, 충분히 뜨거우면서 입력 형태가 안정된 코드는 TurboFan이 최적화한 기계어로 바꿔. 모든 함수가 반드시 각 단계를 순서대로 거치는 건 아니지만, 먼저 실행하고 관찰한 뒤 필요한 부분을 최적화한다는 흐름은 같아.

그래서 짧은 벤치마크의 첫 실행과 충분히 예열한 뒤의 실행은 결과가 다를 수 있어. 최적화가 기대한 입력 형태가 깨지면 V8은 안전한 코드로 되돌아가기도 해. 이 과정을 역최적화라고 불러.

객체 모양을 기억하는 숨은 클래스

JavaScript 객체는 실행 중에도 속성을 더하거나 지울 수 있어. 매번 속성 이름을 처음부터 찾아야 한다면 접근 비용이 커지지. V8은 속성의 이름과 추가 순서가 같은 객체들을 같은 모양으로 보고, 그 모양을 나타내는 숨은 클래스와 속성 위치 정보를 재사용해.

비슷한 객체를 늘 같은 순서로 초기화하면 V8이 이미 배운 모양을 계속 활용하기 쉬워. 반대로 뜨거운 반복문 안에서 속성을 자주 추가하거나 삭제하고, 같은 함수에 서로 전혀 다른 모양의 객체를 계속 넘기면 기존 최적화가 풀릴 수 있어. 그렇다고 모든 객체를 이 규칙에 맞춰 억지로 바꿀 필요는 없어. 실제 병목을 측정한 뒤 이 현상이 보일 때 살펴보면 돼.

V8과 Node가 맡는 일은 달라

V8은 JavaScript의 값과 타입, 코드 실행을 맡아. 파일을 읽고 소켓을 열며 프로세스를 다루는 일은 Node가 제공해. JavaScript에서 readFile()이나 http.get()을 호출하면 Node의 네이티브 코드와 libuv, 운영체제 쪽으로 작업이 넘어가. 성능 문제를 추적할 때도 계산이 V8 안에서 느린지, I/O가 Node와 운영체제 쪽에서 기다리는지 먼저 나눠 봐야 해.

같은 언어를 실행하는 다른 엔진들

V8만 JavaScript를 실행하는 건 아니야. Firefox는 SpiderMonkey를, Safari와 Bun은 JavaScriptCore를 사용해. Deno는 Node처럼 V8을 사용하지만 V8 바깥에 제공하는 API와 권한 모델은 달라.

같은 JavaScript라도 엔진의 최적화 방식과 런타임이 제공하는 API가 다르면 속도와 호환성이 달라질 수 있어. Bun의 시작 시간이 빠른 것도 JavaScriptCore 하나로 설명할 수는 없어. 엔진 특성과 런타임 설계, 도구 통합을 함께 봐야 해.

Pippa의 고백

처음에는 V8과 Node를 같은 이름처럼 섞어 썼어. 아빠가 둘을 한 문장씩 나누게 했지. “V8은 JavaScript 엔진이고 Node는 V8을 사용하는 런타임.” Bun도 같은 구조였어. “JavaScriptCore는 엔진이고 Bun은 그 엔진을 사용하는 런타임.” 구현체가 달라도 엔진과 런타임의 관계는 그대로야. 아빠식으로 말하면 같은 역할을 다른 객체가 맡는 다형성이 여기에도 보이는 셈이지.

Code

객체를 일정한 모양으로 초기화하기·javascript
// 비슷한 객체는 같은 순서로 초기화한다.
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

const a = new Point(1, 2);
const b = new Point(3, 4);

// 실행 중 속성을 더하고 지우면 객체 모양이 계속 달라진다.
const changing = {};
changing.x = 1;
changing.y = 2;
delete changing.x;
changing.x = 3;

console.log(a, b, changing);
V8의 최적화 기록 살펴보기·bash
# 최적화와 역최적화 기록을 출력한다.
node --trace-opt --trace-deopt -e "
function add(a, b) { return a + b }
for (let i = 0; i < 100000; i++) add(i, i);
add('x', 'y');  // 입력 타입을 바꾸어 기존 가정을 깨 본다.
"

External links

Exercise

sum(arr) 함수를 만들고 숫자 배열로 충분히 반복 호출한 뒤, 마지막 한 번만 문자열 배열을 넘겨 봐. node --trace-opt --trace-deopt app.js로 실행하고 sum이 최적화되거나 역최적화된 기록을 찾아. 어떤 입력 가정이 바뀌었는지 코드와 함께 설명해.
Hint
기록이 보이지 않으면 반복 횟수를 늘려 함수가 최적화될 시간을 줘. 목표는 실행 시간을 겨루는 게 아니라, 안정된 숫자 입력을 관찰한 V8이 문자열 입력을 만났을 때 기존 최적화를 거두는 과정을 확인하는 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.