Javascript Hiddenclass와 최적화 기법

March 29, 2019

해당 포스팅은 https://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html 을 참고하였습니다.

Javascript는 object가 정의된 후에도 property들을 유동적으로 추가 또는 제거할 수 있는 동적인 언어입니다. 예를 들면, 아래의 code snippet을 보시면 car라는 객체는 makemodel을 코드 정의 시점에 추가하고 있습니다. 그러나, car 라는 객체가 추가되고 난 이후에도 year라는 property를 동적으로 추가됩니다.

var car = function (make,model) {
	this.make = make;
	this.model = model;
}

var myCar = new car(honda,accord);

myCar.year = 2005;

대부분의 javascript의 인터프리터는 객체 속성값의 위치정보를 메모리에 저장하기 위해 사전형 객체 (dictionary-like object) (hash function을 기반으로 한)을 사용합니다. 이러한 구조는 javascript내의 객체 속성값을 검색하는 과정에서 JAVA등과 같은 비동기적 언어보다 더 많은 검색 비용을 소모하게 됩니다. JAVA에서는, 객체의 속성값들은 컴파일전에 이미 고정된 레이아웃에 의해 결정되며, 런타임 과정에서 속성값들을 동적으로 추가 또는 삭제할 수 없습니다. 결과적으로는, 객체의 속성값 (또는 속성의 포인터값) 들은 연속적인 버퍼로써 메모리 사이에 고정 오프셋으로 저장될 수 있습니다. 즉, 오프셋의 길이는 속성의 유형에 따라 쉽게 정의 가능하나, 런타임중에 속성의 유형이 변경될 수 있는 javascript에서는 해당 길이에 대한 정의가 불가능 합니다.

JAVA와 같은 비동기적 언어에서 메모리내 속성값의 위치는 하나의 명령어로 결정할 수 있지만, 동기적인 언어인 javascriptd에서는 속성값의 위치를 결정하는데에 여러개의 명령어가 필요합니다. 결과적으로 javascript내의 속성값의 검색은 다른 언어들보다 느립니다. javascript내에서 dictionary에 정의된 속성값을 찾는것은 느리기 때문에, V8 엔진에서는 Hidden class라는 다른 방식으로 검색합니다. Hidden class 는 런타임 과정에서 JAVA의 객체와 비슷하게 고정된 객체의 레이아웃 (클래스) 처럼 작동합니다. 아래의 내용들은 V8은 각 객체에 Hidden class에 연결하여 속성값에 엑세스하는 시간을 최적화 하는 것을 다루고 있습니다. 아래의 코드를 먼저 살펴보겠습니다.

function Point(x,y) {
	this.x = x;
	this.y = y;
}

var obj = new Point(1,2);

위의 코드에서 Point function이 new 생성자를 통해서 선언될떄, javascript는 C0이라는 Hidden class를 만들 것입니다.

선언되는 과정에서, 아직 pointer에 대한 정의가 되지 않았기 때문에, C0 class는 비어있는 상태입니다.

Point function 내에서 this.x = x 가 실행되면, V8 엔진은 C0 class를 기반으로 한 C1이라는 두번째 Hidden class를 생성할 것입니다. C1 클래스는 x라는 속성값을 찾을 수 잇도록 메모리상의 x의 위치 (객체의 포인터와 관련되어 있는)를 설명하고 있습니다. x 속성값의 경우, x의 속성은 오프셋 0에 저장되며, 연속적인 메모리 버퍼내에서 0번째 속성값은 x로 정의됩니다. V8 엔진은 C0 클래스를 class transition (클래스 전환) 을 통해 업데이트를 할 것이며, x속성이 포인트 객체에 추가시, 히든 클래스는 C0에서 C1으로 전환해야 합니다. 아래 그림은 관련된 설명에 대한 그림입니다.

새로운 속성이 객체에 추가되는 모든 시점에서, 객체의 이전 히든클래스는 새로운 히든 클래스로 클래스 전환이 일어납니다. 히든클래스의 전환은 생성된 클래스들끼리 객체를 같은 방법으로 공유하고 있기 때문에 상당히 중요한 개념입니다. 만약 두 객체가 숨겨진 클래스를 공유하고, 두 객체에 동일한 속성이 추가되는 경우 클래스 전환을 통해 두객체 모두 동일한 새로운 클래스와 최적화된 코드를 실행하게 됩니다.

이 프로세스는 위의 코드에서 this.y = y 실행시에도 반복해서 동일한 새로운 히든 클래스를 생성 후 전환하게 됩니다. C2라고 불리우는 생성된 새로운 히든 클래스의 상태는 y속성이 포인터 객체에 추가되면서 C1 객체에 해당 상태가 추가되며, C2로 히든클래스가 전환됩니다.

히든클래스간의 전환은 객체에 새로운 속성값이 할당되는 경우 발생하게 됩니다. 아래의 코드를 살펴보겠습니다.

function Point(x,y) {
2    this.x = x;
3    this.y = y;
4  }
5 
7  var obj1 = new Point(1,2);
8  var obj2 = new Point(3,4);
9
10 obj1.a = 5;
11 obj1.b = 10;
12
13 obj2.b = 10;
14 obj2.a = 5;

9번째줄까지 거치게 되면, obj1obj2는 동일한 히든클래스를 공유하게 됩니다. 그러나, 각 객체에서 a와 b속성이 다르게 추가되면, obj1obj2는 이제 서로 다른 히든클래스를 가지게 됩니다.

이런 과정을 보게 된다면, 두개의 히든클래스를 가지고 있는 obj1obj2는 큰문제가 아니라고 생각할 수도 있습니다. 각각의 히든클래스에 오프셋이 지정되어 있는 한, 해당 속성에 엑세스하는 것은 히든클래스를 공유하는 것처럼 빠르게 수행되어야 합니다. 이에 대한 것이 왜 사실이 아닌지 확인하기 위해서는 V8에서 사용되는 또다른 최적화 기법인 inline caching을 살펴봐야 합니다.

Inline Caching

V8은 동적인 타입의 언어를 최적화 하기 위해 다른 최적화 기법인 inline caching을 사용합니다. Inline caching은 동일한 메소드에 대한 반복되는 호출이 동일한 유형의 객체에서 발생하는 경향이 있다는 점에 의존하여 만들어진 기법입니다.

V8 엔진에서는 최근에 호출된 메세드에 매개변수로 전달된 객체에 대한 type을 캐시로 유지하여 이후 매개변수로 전달되는 객체에 대한 유형을 가정합니다. 만약 V8에서 전달된 가정된 객체 유형이 정확하다면, 객체 속성에 엑세스하는 대신 이전에 저장된 객체의 히든 클래스 정보를 사용하는 프로세스로 객체 속성을 대신하여 수행할 수 있습니다.

특정 객체에 대한 메소드가 호출될때마다 매번 V8엔진은 해당 객체에 대한 조회를 수행하며 특정한 속성에 접근하기 위한 오프셋을 결정해야 합니다. 동일한 히든클래스에 대해 동일한 메소드를 두번 호출하게 되면, V8 엔진은 히든클래스에 대한 직접 접근을 생략하고 단순하게 포인터 객체에 대한 속성의 오프셋만을 추가하게 됩니다. 이후에 모든 메서드 호출에 대해서 V8 엔진은 히든클래스가 변경되지 않았다고 가정하여 이전 조회에서 저장했던 오프셋을 사용하여 특정된 속성의 메모리 주소로 바로 이동할 수 있게 됩니다. 이러한 과정은 실행속도를 크게 증가시킬 수 있습니다.

Inline caching은 동일한 유형의 객체가 동일한 히든클래스를 공유하는 것이 중요한 포인트입니다. 왜냐하면 동일한 유형의 객체를 두개 생성했으나, 위의 예처럼 서로 각 객체가 다른 히든클래스를 사용하면 V8엔진에서는 Inline caching을 사용할 수 없게 됩니다. 두 객체가 같은 유형이나, 서로 다른 히든클래스를 가지고 있는 경우 각 속성에 다른 오프셋을 할당하기 때문입니다.

물론, javascript는 동적인 타입의 언어이기 때문에, 각 객체에 히든클래스에 대한 가정이 부정확 할 수 있습니다. 이러한 경우 V8엔진은 최적화를 취소하고, 원래 버전의 메서드 호출로 돌아가 히든클래스를 재 검사하게 됩니다.

최적화 계획

  1. 항상 히든클래스와 최적화된 코드를 공유할 수 있도록 같은 순서로 객체의 속성을 인스턴스화 합니다.
  2. 인스턴스화를 한 후 객체의 속성을 추가하게 되면, 히든클래스가 변경되고, 이전 히든클래스에 대해 수행된 최적화 기법이 취소되어 모든 메서드가 재 실행을 해야합니다. 인스턴스화시에 생성자에서 모든 객체의 속성을 할당할 수 있도록 하는것이 중요합니다.
function Test(a, b) {
	this.a = a;
	this.b = b;
}

var test = new Test(1,2);
test.c = 3; // bad!!

// instead
function Test(a, b, c) {
	this.a = a;
	this.b = b;
	this.c = c;
}

var test = new Test(1,2,3);
  1. 동일한 메소드를 반복적으로 실행하는 코드는 인라인 캐싱으로 인해서 여러 다른 메소드를 한번만 실행하는 코드보다 빠르게 실행됩니다.
...