자바스크립트 성능의 비밀 (V8과 히든 클래스)


자바스크립트가 C++ 성능에 도달한 방법 원문 : https://blog.bitsrc.io/secret-behind-javascript-performance-v8-hidden-classes-ba4d0ebfb89d

Thumbnail

오늘날 자바스크립트는 웹 개발에서 가장 많이 사용되는 언어 중 하나가 되었다. 그러나 이 단계까지 오기 위해서 많은 장애물을 통과해야 한다. 이런 목표 중 하나는 C++과 같은 언어와 유사한 성능으로 실행 속도를 달성했다는 것이다.

V8 자바스크립트 엔진의 발명 없이는 이 중 어느 것도 불가능하다.

따라서 이 글에서는 이런 성능 향상의 이면에 있는 기술과 더 나은 성능의 코드를 작성하기 위해 알아야 할 사항에 대해 설명한다.


V8이 무엇이고 어떻게 동작하는가

V8은 구글이 도입한 오픈소스 자바스크립트 엔진이다. C++로 작성되었으며 구글 크롬, 크로미움 웹 브라우저, NodeJS를 지원한다. 환경과 상호작용하고 프로그램을 실행하기 위한 바이트코드를 생성하는 역할을 담당한다.

처음 V8은 웹 브라우저의 성능 향상 메커니즘으로 도입되었으며 시간이 지나면서 다른 엔진보다 훨씬 향상된 인터프리터가 되었다.

V8과 다른 엔진의 가장 큰 차이점은 V8 엔진의 JIT(Just In Time) 컴파일러다.

JIT 컴파일러는 런타임에 모든 자바스크립트를 기계어 코드로 컴파일하고 중간 코드를 생성하지 않는다.

V8 architecture V8 엔진의 고수준 아키텍처

위 다이어그램에서 볼 수 있듯이 V8 엔진은 2개의 주요 부분으로 구성된다. 첫 번째 부분은 코드를 바이트코드로 해석하는 구문 분석을 담당하며 최신 버전의 V8은 이 프로세스에 Ignition이라는 인터프리터를 사용한다. 구문 분석기가 생성한 AST(Abstract Syntax Tree) 를 입력으로 사용하고 바이트코드를 생성한다.

하지만 우린 컴파일러가 인터프리터보다 훨씬 빠르다는 것을 알고 있다. 그렇다면 V8 엔진이 컴파일러 대신 인터프리터를 사용하는 이유는 뭘까?

Ignition 인터프리터를 사용하는 주된 이유는 메모리 사용량을 줄이는 것이다. 전체 프로그램을 컴파일하는 컴파일러와 달리 인터프리터는 필요한 라인만 컴파일하기 때문이다.

그러나 이 Ignition 인터프리터는 코드를 처음 실행할 때만 동작 한다. 생성된 바이트코드는 Turbofan이라는 컴파일러에 의해 사용된다. 코드 실행 중 받는 데이터를 기반으로 코드를 최적화하고 보다 최적화된 버전을 다시 컴파일한다.

참고 V8은 자바스크립트를 최적화하는데 사용되지만 C++로 작성되었으며 다중 스레드 방식을 사용하여 이런 모든 작업을 한 번에 관리한다.

V8의 작동 방식을 설명할 때 Ignition 인터프리터가 추상 구문 트리를 입력으로 사용한다고 언급했으므로 이제 추상 구문 트리가 무엇이고 V8이 자바스크립트 성능을 향상시키는데 어떻게 도움이 됐는지 알아보자.


추상 구문 트리

추상 구문 트리는 컴파일러에서 소스 코드의 추상 구조를 구축하는 데 사용된다. 또한 자바스크립트나 V8에서만 쓰이지 않는다. 거의 모든 프로그래밍 언어는 AST를 이용해서 상위 수준의 코드 표현을 하위 수준의 표현으로 변환한다.

코드를 AST로 변환할 때 변수 타입, 위치, 문장 순서 등과 같은 코드의 세부 정보가 포함된다. 따라서 컴파일러가 주석 같은 불필요한 것들을 처리할 필요가 없다.

이해를 돕기 위해 간단한 자바스크립트 코드를 이용해 AST를 생성해보자.

// 함수 선언
function addition(x, y) {
  var answer = x + y;
  console.log(answer);
}

// 함수 호출
addition(10,20);

esprima에서 제공하는 온라인 구문 분석 도구를 이용해 이 코드에 대한 AST를 생성했다. 다음 코드는 AST의 일부를 보여주며, 여기서 전체 AST를 볼 수 있다.

{
 “type”: “Program”,
 “body”: [
   {
     “type”: “FunctionDeclaration”,
     “id”: {
       “type”: “Identifier”,
       “name”: “addition”
     },
     “params”: [
      {
       “type”: “Identifier”,
       “name”: “x”
      },
      {
       “type”: “Identifier”,
       “name”: “y”
      }
     ],
     “body”: {
       “type”: “BlockStatement”,
       “body”: [
        ...
       ],
      “kind”: “var”
     },
   ...
 “sourceType”: “script”
}

AST는 코드의 각 라인에 대해 키값 쌍을 정의한다. 초기 타입 식별자는 AST가 프로그램에 속한다고 정의한 다음 모든 코드 라인이 객체의 배열인 본문 내부에 정의된다.

언급했듯이 모든 함수 선언, 변수 선언, 이름, 타입 등이 라인별로 정리되어 있고 주석은 무시돼있다.

V8은 최적화 프로세스와 AST 사용 외에도 자바스크립트의 성능을 향상시키기 위해 또 다른 트릭을 사용한다. 그럼 이게 무엇이며 어떻게 작동하는지 보자.


자바스크립트 코드 최적화를 위한 히든 클래스

우리 모두 알다시피 자바스크립트는 동적 타입 언어다. 즉, 객체에서 속성을 즉시 추가하거나 제거할 수 있다.

dynamically object 즉시 객체 속성 변경

그러나 이 접근 방식은 자바스크립트의 성능을 떨어뜨리는 동적 조회를 더 필요로 하게 된다.

V8 엔진은 히든 클래스를 사용해 이 문제를 해결하고 자바스크립트 실행을 최적화한다.

히든 클래스의 작동 방식

새 객체를 생성할 때 V8 엔진은 이에 대한 새로운 히든 클래스를 생성한다. 그런 다음 새 프로퍼티를 추가해 동일한 객체를 수정하면 V8 엔진에서 이전 클래스의 모든 프로퍼티가 포함된 새 히든 클래스를 만들고 새 프로퍼티를 포함한다.

위의 예제를 다시 살펴보고 히든 클래스가 생성되는 방법을 살펴보자.

따라서 빈 객체(const userObject = { })를 생성하면 V8은 오프셋 없이 해당 히든 클래스(C01)을 만든다. hidden1

그런 다음 새 프로퍼티를 추가해 해당 객체를 수정한다. (userObject.name = "Chameera"). 이제 V8 엔진은 이전 히든 클래스(C01)의 모든 프로퍼티를 상속해 새 히든 클래스(C02)를 생성하고 이름 프로퍼티를 오프셋 0에 할당한다. hidden2

이렇게 하면 컴파일러는 프로퍼티 이름에 접근할 때 사전형 탐색(dictionary lookups)을 우회할 수 있으며 V8은 클래스 C01을 직접 가리킨다.

이 객체에 다른 프로퍼티를 추가하면 동일한 과정이 수행된다. 또 다른 히든 클래스가 생성되고 이전 프로퍼티와 새 프로퍼티가 모두 오프셋으로 포함된다.

hidden3 이 히든 클래스를 이용하면 사전형 탐색(dictionary lookups)을 우회할 수 있을 뿐만 아니라 유사한 객체가 생성되거나 수정될 때 이미 생성된 클래스를 재사용할 수 있다.

예를 들어 기사(const articleObject = { })라는 다른 빈 객체를 생성하면 V8 엔진은 새로운 히든 클래스를 생성하지 않는다. 대신 이미 생성된 C01 클래스를 가리킨다.

그러나 articleName이라는 새 프로퍼티를 추가해 articleObject를 수정하면 V8은 name이라는 프로퍼티만 있기 때문에 이전에 생성한 클래스(C02)를 사용할 수 없다.

고성능 자바스크립트 코드 작성

따라서, 자바스크립트 코드의 성능 향상을 최대화하려면 동적 프로퍼티 추가를 줄여야 할 수도 있다.

NodeJS에서 반복문을 실행하고 있다고 가정하자. 객체에 동적 프로퍼티를 추가하면 반복문 내부에 성능 차이가 나타난다. 따라서 반복문 내부에서 프로퍼티를 동적으로 추가하는 대신 반복문 외부에서 프로퍼티를 만들어 사용하는 것이 좋다.

즉 V8이 기존의 히든 클래스를 재사용할 때, 성능이 훨씬 향상된다.


결론

자바스크립트의 작동 방식에 대한 토론이 있을 때마다 이벤트 루프, 마이크로 태스크, 매크로 태스크, 콜백 큐에 대해 이야기한다. 하지만 이 모든 것이 자바스크립트로 구현되진 않았다. 대신 V8 엔진의 일부이며 자바스크립트 코드 최적화를 담당한다.

그래서 V8의 작동 방식과 히든 클래스 개념을 사용해 코드를 최적화하는 방법에 대해 논의하고 싶었다.

이 글을 통해 자바스크립트에 대해 새로운 것을 배웠기를 바라며, 의견이 있다면 공유해 주길 바란다.

이 글을 읽어줘서 고맙다!!!