본문 바로가기

Javascript

V8 엔진의 히든 클래스와 인라인 캐싱

모던 자바스크립트 Deep Dive책에서 간단하게 소개한 V8 엔진의 히든 클래스 방식에 대해 이해한 바를 간단한게 정리하고자 포스팅을 작성해본다.

 

1. 프로토타입 기반 언어의 함정

 

자바스크립트는 프로토타입 기반 언어로, 클래스 기반 언어와 다르게 객체에 프로퍼티를 런타임에서 추가하거나 삭제할 수 있다.

 

클래스 기반 언어의 경우 같은 클래스에 속한 객체들은 모두 같은 필드 구조를 가지므로, base address에 offset을 더하는 방식으로 프로퍼티에 접근할 수 있다. 따라서 컴파일타임에 객체의 프로퍼티가 어디있는지를 바로바로 알 수 있다.

 

반면 자바스크립트는 런타임에 프로퍼티가 더해지거나 삭제되거나 프로퍼티 값의 타입이 변할 수 있으므로 이런 offset 공유 방식을 사용할 수 없다. 따라서 각 객체마다 다른 offset 테이블을 가져야한다.

 

 

 

Google V8 - Hidden classes

 

 

결론적으로 메모리에 큰 부하가 생긴다.

 

 

 

2. 히든 클래스

 

이러한 단점을 해결하기 위해 나온 개념이 히든 클래스이다. 클래스가 없어 생기는 문제이니 숨겨진 클래스를 만든다는 최적화 기법이다.

히든 클래스의 특징을 먼저 간단하게 살펴보면 다음과 같다.

 

 

- 객체는 반드시 하나의 히든 클래스를 참조한다.

- 히든 클래스는 각 프로퍼티에 대해 메모리 오프셋을 가지고있다.

- 동적으로 프로퍼티 변형이 생기면 새 히든 클래스가 생성되고, 새 히든 클래스는 기존 프로퍼티 오프셋 + 새 프로퍼티 오프셋을 가진다.

- 그리고 Transition table에 어떤 프로퍼티 변경사항 발생할 경우 어느 히든 클래스를 참조해야 하는지를 저장한다.

 

 

간단한 예시로 위 특징들에 대해 알아보자.

 

 

 

let obj = {};

객체가 생성되면 히든 클래스가 생성되고, 아직 아무런 프로퍼티가 없으므로 다음과 같은 형태를 가진다.

 

 

 

 

var obj = {};
obj.x = 1;

 

이제 x 라는 프로퍼티가 동적으로 추가된다. 이제는 새로운 히든클래스가 생성되고 해당 프로퍼티의 offset이 새 히든클래스에 저장된다.

그리고 기존 히든클래스는 전환정보를 저장한다.

 

 

 

이런 히든클래스를 사용하면 추후 객체 를 생성할 때, 같은 형태의 객체라면 히든 클래스를 공유할 수 있다. 가는 길목마다 변화시에 참조할 히든 클래스도 알려주니 중복을 현저하게 줄일 수 있어서 메모리를 많이 아낄 수 있다.

 

 

 

3. 인라인 캐싱

 

히든 클래스를 통해 불필요한 메모리 사용을 아꼈다. 하지만 성능은 어떨까? 

실제 어떤 프로퍼티에 접근하려면 

 

 

- 객체의 히든 클래스에 접근

- 히든 클래스 내 Property Table에 접근

- 해당 프로퍼티의 offset 탐색

- base + offset 연산

 

 

의 과정을 거쳐야 한다. (사실 히든 클래스를 사용하지 않아도 첫 번째 단계만 생략될 뿐 크게 다른건 없다고 한다)

 

 

하지만 V8에 구현된 인라인 캐싱을 통해 특정한 상황에서 프로퍼티 접근을 아주 빠르게 할 수 있다.

다음과 같은 경우를 살펴보자

 

const obj1 = {name: "inho", age: 14};
const obj2 = {name: "sooin", age: 25};
const obj3 = {name: "jisoo", age: 5};

const arr = [obj1, obj2, obj3];

for(let i = 0; i < arr.length; i++){
    console.log(arr[i].age);
}

 

처음 obj1 의 age 프로퍼티에 접근할 때는 위에서 언급한 복잡한 과정을 모두 수행한다.

히든 클래스에 직접 접근하고 Property Table에서 age의 offset을 찾아 연산을 한다. 

 

이후 obj2 의 age 프로퍼티에 접근할 때는 이보다 훨씬 빠르게 할 수 있는데, 이는 앞전 단계에서 인라인 캐시가 일어났기 때문이다. obj1 이 가리키는 히든 클래스와 접근했던 프로퍼티 offset을 캐시에 저장해둔다.

 

 

 

obj2에서 캐시가 일어나며, 기존에 저장되어있는 캐시 정보가 있으니 일치하는지 확인한다.

 

obj1 과 obj2 는 동일 필드와 동일 타입을 가졌으니 같은 히든 클래스를 가리키고 있다.

따라서 위 그림의 캐시 조건을 모두 만족하므로 캐시된 offset을 바로 사용할 수 있다. obj3의 경우도 동일하다.

 

이런 원리로 반복문에서 동일한 히든 클래스를 가진 객체 프로퍼티에 접근 할 경우 성능을 극대화 할 수 있다.

 

 

 

 

같은 원리로 다음과 같은 형태의 코드는 지양해야 한다.

 

const obj1 = {name: "inho", age: 14, hobby: "surfing"};
const obj2 = {name: "sooin", age: 25, gender: "female"};
const obj3 = {name: "jisoo", age: 5, height: 162};

const arr = [obj1, obj2, obj3];

for(let i = 0; i < arr.length; i++){
    console.log(arr[i].age);
}

 

위 코드에서 obj1, obj2, obj3 은 각자 다른 히든클래스를 가리킨다. 

이렇게 되면 반복문으로 공통된 프로퍼티에 접근하더라도 인라인 캐시에서 계속 miss 가 나서 매번 일일히 offset을 구해야 한다. 

이 경우 인라인 캐시를 쓰지 않았을 때 보다도 더 오랜 시간이 걸리게 된다. 일일히 offset 구하는거에 추가로 캐시까지 확인해야 하니까.

 

 

 

따라서 반복적으로 접근 할 일이 있는 경우, 아니 어지간하면 동일한 프로퍼티 구조를 가지는 객체를 사용하는 것이 좋다.

즉, 자바스크립트를 c나 java와 같은 정적 타이핑 언어로 사용하는것이 성능 면에서 훨씬 우세하다. (Typescript의 중요성이 여기서 또..)

 

 

 

+ 참고로 실제로는 반복문의 첫번째 수행이 아닌 두번째 수행에서 캐시가 수행된다고 한다. 첫 번째보다 이미 한번 반복된 두번째 수행이 이후로 cash hit 일 확률이 훨씬 높기 때문이다. 

 

 

 

실제로 인라인 캐싱에서 객체 프로퍼티 구조를 동일하게 할 시 얼마나 성능이 좋아지는지를 측정해볼 수 있다.

 

 

1. 동일한 프로퍼티 구조를 가지는 객체를 순회할 경우

 

;(() => {
  const han = { firstname: "Han", lastname: "Solo" }
  const luke = { firstname: "Luke", lastname: "Skywalker" }
  const leia = { firstname: "Leia", lastname: "Organa" }
  const obi = { firstname: "Obi", lastname: "Wan" }
  const yoda = { firstname: "", lastname: "Yoda" }
  const people = [han, luke, leia, obi, yoda, luke, leia, obi]
  const getName = (person) => person.lastname
  console.time("engine")
  for (var i = 0; i < 1000 * 1000 * 1000; i++) {
    getName(people[i & 7])
  }
  console.timeEnd("engine")
})()

 

 

 

 

2. 다른 프로퍼티 구조를 가지는 객체를 순회할 경우

 

;(() => {
  const han = { firstname: "Han", lastname: "Solo", spacecraft: "Falcon" }
  const luke = { firstname: "Luke", lastname: "Skywalker", job: "Jedi" }
  const leia = { firstname: "Leia", lastname: "Organa", gender: "female" }
  const obi = { firstname: "Obi", lastname: "Wan", retired: true }
  const yoda = { lastname: "Yoda" }
  const people = [han, luke, leia, obi, yoda, luke, leia, obi]
  const getName = (person) => person.lastname
  console.time("engine")
  for (var i = 0; i < 1000 * 1000 * 1000; i++) {
    getName(people[i & 7])
  }
  console.timeEnd("engine")
})()

 

 

 

거의 세 배 이상 차이가 나는 것을 볼 수 있다.

 

 

 

 

 

 

Reference

 

https://engineering.linecorp.com/ko/blog/v8-hidden-class/

 

V8의 히든 클래스 이야기 - LINE ENGINEERING

자바스크립트가 되어 그 기분을 헤아릴 수 있다면 안녕하세요? LINE Fukuoka의 프론트엔드 엔지니어 Yonehara입니다. 저는 프론트엔드 개발자로서 아직 웹 브라우저나 자바스크립트의 기분을 헤아려

engineering.linecorp.com

https://meetup.toast.com/posts/78

 

자바스크립트 엔진의 최적화 기법 (2) - Hidden class, Inline Caching : NHN Cloud Meetup

자바스크립트 엔진의 최적화 기법 (2) - Hidden class, Inline Caching

meetup.toast.com

https://mrclap.github.io/posts/js/2019-08-09-javascript-inline-caching-/

 

JS - 인라인캐싱을 고려한 최적화(Chrome only)

JS - 인라인캐싱을 고려한 최적화(Chrome only) by 박수August 9, 2019 1 min read

mrclap.github.io