2. JavaScript engines how do they even? | Franziska Hinkelmann
3/6/2025, 2:03:56 AM
V8의 hidden class 생성과 디버깅
- 본 포스트는 1편 번역본의 내용을 보충설명합니다.
- 보충 설명은 코드 스니펫을 통해 설명합니다.
1편의 내용을 통해 알게 된 내용은 자바스크립트는 여러 엔진(V8, spider monkey, chakra)을 통해 실행되고 있으며,
그 중에서 가장 대중적인 V8엔진의 JIT compile
과 hot function
그리고 인터프리터인 ignition
, 최적화 컴파일러인 turbofan
까지 알아봤습니다.
알아보는 것에 그치지 않고,
실제 위 내용들을 엮어서 V8 엔진이 내 코드를 더 빨리 더 쉽게 hot function
으로 인식하게 하려면 어떻게 코드를 작성해야 하는지
알아보겠습니다.
V8의 ignition pipeline
위 이미지는 V8 blog의 2016년 post인 Firing up the Ignition interpreter 에 있는 이미지입니다.
최근에는 Full codegen과 crankshaft가 사라졌습니다.
현재 기준으로
Ignition을 통한 JS 코드를 바이트 코드로 변환하는 과정 바이트코드를 해석하여 실행하는 과정을 모두 Ignition이 진행합니다.
최적화는 TurboFan이 담당하게 되는데 Hot Code를 네이티브 코드로 변환하는 작업을 담당합니다. (* 네이티브 코드는 기계어를 의미합니다 = Machine code)
function add(x, y) {
return x + y;
}
⬆️ 위 코드는 아래와 같은 코드로 변하게 됩니다. ⬇️
mov eax, edi; x 값을 eax 레지스터에 저장
add eax, esi; y 값을 eax에 더함
ret ; 결과를 반환
V8의 hidden class
V8의 히든 클래스는 인터프리터인 ignition에 많은 힌트를 주어 hot function
을 잘 만들 수 있는 기반이 됩니다.
자바스크립트 파일은 기본적으로 인터프리터에게 해석되므로 속도는 항상 아쉬울 수 밖에 없습니다.
그 힌트를 주는 방법 중 하나인 hidden class(= 히든 클래스)에 대해 알아보겠습니다.
히든 클래스는 보이지 않는 클래스로써 클래스처럼 어떠한 객체의 shape을 정보로 가집니다. 이 정보는 인터프리터에게 타입 힌트를 주게 됩니다.
hidden class 예시
1. JavaScript engines how do they even? | Franziska Hinkelmann We represent object types incrementally by transitioning for every property to a new type. V8 엔진은 객체 타입(Object Shape) 을 점진적으로 변경하며 추적합니다. So, if you have an empty object literal, it is represented by just Object(). 예를 들어, {} (빈 객체)를 생성하면, 내부적으로 Object() 형태로 표현됩니다. If you have a literal with a property x, then we transition from the empty literal type to the next type, 객체에 x 속성을 추가하면, 기존 타입에서 새로운 타입으로 전환(Transition) 이 발생합니다.
히든 클래스는 특정 객체가 생성될 때 함께 생성됩니다. 해당 객체의 형태를 ignition은 hidden class라는 숨겨진 값으로 그 객체의 shape을 추적합니다.
var obj0 = {};
obj0.x = 1;
obj0.y = 2;
var obj1 = { x: 3, y: 4 };
위 코드에서 obj0
은 ignition이 관리하는 hidden class 객체에 의해
빈 객체
> x property를 가진 객체
> x, y property를 가진 객체
로 그 히든 클래스
가
변화해갑니다.
obj1
의 히든 클래스는 한번에 x, y가 literal하게 생성되었으므로 obj0
과 obj1
은 서로 다른 히든 클래스 객체를 가지게 됩니다.
반대로 아래와 같은 코드는 같은 히든 클래스를 공유합니다.
var obj2 = { x: 1, y: 2 };
var obj3 = { x: 3, y: 4 };
hidden class를 직접 보기
위의 두 예시의 hidden class를 직접 확인해보겠습니다.
아래와 같은 js 코드를 --allow-natives-syntax
flag를 추가해 실행하면
var obj0 = {};
obj0.x = 1;
obj0.y = 2;
var obj1 = { x: 3, y: 4 };
%DebugPrint(obj0);
%DebugPrint(obj1);
아래와 같은 출력을 받아볼 수 있습니다.
obj0과 obj1의 hidden class의 메모리주소를 확인해보면 16진수로 메모리 주소가 있고, 0x00cd097de2c1, 0x00cd097de351로 서로 다른 것을 알 수 있습니다.
DebugPrint: 0x167bfc5de359: [JS_OBJECT_TYPE] // obj0의 memory address
- map: 0x00cd097de2c1 <Map[56](HOLEY_ELEMENTS)> [FastProperties] // obj0의 hidden class의 메모리주소
// ... 생략
DebugPrint: 0x167bfc5de409: [JS_OBJECT_TYPE] // obj1의 메모리주소
- map: 0x00cd097de351 <Map[40](HOLEY_ELEMENTS)> [FastProperties] // obj1의 hidden class의 메모리주소
// ... 생략
아래의 예시도 확인해보겠습니다.
var obj2 = { x: 1, y: 2 };
var obj3 = { x: 3, y: 4 };
%DebugPrint(obj2);
%DebugPrint(obj3);
위와 같은 코드는 아래와 같은 출력을 받습니다.
DebugPrint: 0x33cbd86de349: [JS_OBJECT_TYPE] // obj2의 memory address
- map: 0x35c0490de301 <Map[40](HOLEY_ELEMENTS)> [FastProperties] // obj2의 hidden class의 메모리주소
// ... 생략
DebugPrint: 0x33cbd86de3e9: [JS_OBJECT_TYPE] // obj3의 memory address
- map: 0x35c0490de301 <Map[40](HOLEY_ELEMENTS)> [FastProperties] // obj3의 hidden class의 메모리주소
// ... 생략
엄연히 다른 두 객체의 hidden class가 같고, 이를 활용해 인터프리터인 ignition
이 타입 힌트를 얻어 마치 컴파일러처럼 컴파일이 가능하게 됩니다.
따라서 만약 아래와 같은 코드가 있는 경우
function readX(obj) {
return obj.x;
}
var a = {x: 1};
var t = {o: 2};
readX(a);
readX(t);
readX
함수에 변수 t
를 그냥 넣지 말고
var t = {x:undefined, o: 2};
이런식으로 기존 프로젝트나 개념들이 오염되지 않는 수준이라면 hidden class를 맞추어 주는 쪽이 성능 면에서 이점이 있습니다.
요약
hidden class를 고려하여 코드를 작성하고, 되도록이면 같은 shape가 되는 것과 함수가 다형성을 지키는 것은 성능을 저해할 수 있습니다 (optimize가 풀림)