sbyeol3/articles

[번역] 자바스크립트 성능의 비밀 : V8 엔진 & 히든 클래스

Opened this issue · 0 comments

부제 : 자바스크립트가 C++ 성능을 달성하는 방법
원문 : Secret Behind JavaScript Performance: V8 & Hidden Classes

오늘날 자바스크립트는 웹 개발에서 가장 많이 사용되는 언어가 되었습니다. 그러나 이러한 단계까지 올라가기 위해 많은 장애물을 통과해야 했습니다. 그 중 하나는 실행 속도에 관한 것인데, C++과 같은 언어와 비슷한 성능을 달성해야 했습니다.

이러한 성취는 V8 자바스크립트 엔진 없이는 불가능합니다.

그래서 이 아티클에서는 이러한 성능을 얻을 수 있었던 기술과 더 나은 성능을 가지는 코드를 작성하기 위해 알아야 하는 것들에 대해 얘기해보고자 합니다.

V8이란 무엇이고 어떻게 동작하나요?

V8은 구글이 제안한 오픈소스 자바스크립트 엔진입니다. C++로 작성되었으며 구글 크롬을 비롯한 크로미움 웹 브라우저 그리고 NodeJS를 지원합니다. 환경과 상호 작용하며 자바스크립트 프로그램을 실행하기 위한 바이트코드를 생성하는 역할을 합니다. 초기에 V8은 웹 브라우저의 성능 향상 메커니즘으로서 소개되었는데요. 시간이 지남에 따라 다른 엔진들보다 더 향상된 인터프리터가 되었습니다.

V8과 다른 엔진들의 가장 두드러지는 차이점은 JIT(Just-In-Time) 컴파일러라는 점입니다.

JIT 컴파일러는 런타임에 모든 자바스크립트 코드를 머신 코드로 컴파일하며 중간 코드는 생성하지 않습니다.

image

위 다이어그램에서 볼 수 있듯이, V8 엔진은 2개의 메인 파트로 이루어져 있습니다. 첫 번째 파트는 코드를 바이트코드로 해석하는 파서의 역할을 하며, V8의 최신 버전은 이 과정에서 Ignition이라고 불리는 인터프리터를 사용하고 있습니다. 파서에 의해 **추상 구문 트리(AST)**가 생성되는데 인터프리터는 이를 입력값으로 받아 바이트 코드를 생성합니다.

그러나 인터프리터보다 컴파일러가 훨씬 빠릅니다. 그렇다면 왜 V8 엔진은 컴파일러 대신에 인터프리터를 사용하는 걸까요?

Ignition 인터프리터를 사용하는 가장 큰 이유는 메모리 사용량을 줄이기 위해서 입니다. 인터프리터는 전체 프로그램을 컴파일하는 컴파일러와 다르게 오직 필요한 라인만 컴파일하기 때문에 메모리 사용량 감소가 가능합니다. 그러나 Ignition 컴파일러는 가장 초기에 동작하는 코드에 대해서만 자신의 역할을 수행합니다. Turbofan이라고 불리는 컴파일러가 생성된 바이트코드를 사용합니다. 이는 코드를 실행하는 동안 얻은 데이터를 기반으로 여러분의 코드를 최적화하고 더 최적화된 버전으로 다시 컴파일합니다.

Note: V8이 자바스크립트를 최적화하기 위해 사용되기는 하나 C++로 작성되었으며 , 한번에 여러 가지 작업들을 관리하기 위해 멀티 스레드 방식을 사용합니다.

V8의 동작 방식을 설명할 때 Ignition 인터프리터가 입력값으로 AST를 받는다고 했는데요. 그럼 AST가 무엇인지, 그리고 어떻게 AST가 V8이 자바스크립트 성능을 향상하는 데 도움이 되는지 살펴봅시다.

팁 : 어플리케이션을 다르게 빌드하기

Bit과 같은 OSS 툴은 모던 앱을 빌드하는 데 새로운 패러다임을 제공합니다.

모놀리틱 프로젝트를 개발하는 대신에 독립적인 컴포넌트를 빌드해보세요. 그러고 나서 여러분의 컴포넌트를 함께 구성하여 원하는 만큼 여러 어플리케이션을 빌드할 수 있습니다. 빌드하기에 더 빠른 방법은 아니지만 더 확장성 있으며 개발을 표준화하는 데 도움이 됩니다.

Abstract Syntax Tree

AST는 컴파일러가 소스코드를 추상적인 구조로 구축하는 데 사용됩니다. 또한 자바스크립트나 V8에 한정되어 있지 않습니다. 대부분의 프로그래밍 언어는 고수준의 코드를 저수준으로 표현하기 위해 AST를 사용합니다. 여러분의 코드를 AST로 변환할 때 이는 변수 타입, 위치, 구문의 순서와 같이 코드에서 필요한 세부사항들을 포함합니다. 그래서 컴파일러는 주석과 같이 불필요한 부분을 다루지 않습니다.

더 잘 이해하고자, 간단한 자바스크립트 코드로 AST를 생성해봅시다.

// 함수 정의
function addition(x, y){
   var answer = x + y;
   console.log(answer);
}
// 함수 호출
addition(10,20);

이 코드에 대한 AST를 생성하기 위해 esprima에서 제공하는 온라인 파싱 툴을 사용했는데요. 다음 코드 스니펫은 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가 프로그램에 속하는 것으로 정의하고, 모든 코드 라인은 객체의 배열 본문 내에 정의됩니다. 아까 언급했듯이, 모든 함수 정의나 변수 정의, 이름과 타입 등은 라인 별로 구성되며 주석은 무시됩니다. 최적화 과정과 AST를 사용하는 것과 별개로 V8은 자바스크립트 성능의 향상을 위해 다른 트릭을 사용합니다. 그것이 무엇인지, 어떻게 동작하는지 봅시다.

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

우리 모두가 알듯이, 자바스크립트는 동적 타입 언어입니다. 이는 그때그때 객체에 어트리뷰트를 추가하거나 제거할 수 있다는 뜻입니다.

image

그러나 이 방식은 더 많은 동적인 룩업을 요구하므로 자바스크립트의 성능을 저하시킵니다.

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

히든 클래스는 어떻게 동작하는가?

여러분이 새로운 객체를 생성할 때, V8 엔진은 그에 맞는 새로운 히든 클래스를 생성합니다. 그 후, 여러분이 동일한 객체에 새로운 프로퍼티를 추가함으로써 객체를 변경시킬 때 V8 엔진은 이전의 클래스에서 모든 프로퍼티를 가지는 새로운 히든 클래스를 생성하고 새로운 프로퍼티를 추가합니다. 다시 위의 예시에서 어떻게 히든 클래스가 생성되는지 볼까요?

빈 객체를 생성했을 때 V8은 는그에 상응하여 어떠한 오프셋이 없는 히든 클래스를 생성합니다.

image

그 후 새로운 프로퍼티를 추가하여 객체를 변경시키면 V8 엔진은 새로운 히든 클래스를 생성하는데 이 클래스는 이전의 히든 클래스의 모든 프로퍼티를 상속하며 offset 0에 대한 어트리뷰트를 할당합니다.

image

이러한 방식을 통해 프로퍼티 이름이 접근될 때 딕셔너리 룩업을 무시하며 V8은 클래스 C01을 직접 가리킵니다. 이 객체에 새로운 프로퍼티를 추가하면 동일한 과정이 발생합니다. 또 다른 히든 클래스가 생성되면 이는 이전의 클래스와 새로운 어트리뷰트 모두를 가집니다.

image

이러한 히든 클래스 개념은 같은 객체가 생성되거나 변경될 때 딕셔너리 룩업을 무시할 뿐만 아니라 이미 생성한 클래스를 재사용할 수 있습니다.

예를 들어 여러분이 article이라는 빈 객체를 생성했다면(const articleObject = {}, V8 엔진은 새로운 히든 클래스를 생성하지 않습니다. 대신에 이미 생성된 C01 클래스를 가리킬 것입니다. 그러나 여러분이 articleName이라는 새로운 프로퍼티를 추가하여 articleObject를 변경시키면 V8은 더 이상 이전에 생성한name이라는 프로퍼티만 가지는 C02 클래스를 사용할 수 없습니다.

더 좋은 성능의 자바스크립트 코드 작성하기

그래서 여러분이 자바스크립트 코드의 성능을 극대화하고 싶다면, 여러분은 동적 프로퍼티 할당을 감소해야 할 겁니다. NodeJS에서 루프를 실행하고 있다고 가정해봅시다. 여러분이 객체에 동적 프로퍼티를 추가한다면 루프 내에서 성능이 변경하는 것을 알 수 있습니다. 루프 내에서 동적으로 추가하기 보다는 루프 밖에서 프로퍼티를 생성하고 사용하는 것이 좋습니다. V8은 존재하는 히든 클래스를 재사용하기 때문에 더 나은 방법인 것이죠.

결론

어떻게 자바스크립트가 동작하는지에 대한 논의가 있을 때마다 이벤트 루프, 마이크로태스크, 콜백 큐에 대해 이야기 합니다. 그러나 이러한 것들은 자바스크립트에서 구현되는 것이 아니라 V8 엔진의 한 부분일 뿌이며 여러분의 자바스크립트 코드를 최적화하는 역할을 합니다.
그래서 어떻게 V8이 동작하는지를 설명하고자 했으며 V8이 사용하는 히든 클래스 개념을 말씀드린 겁니다. 여러분이 이 아티클에서 새로운 것들을 배웠길 바라며 댓글창에서 여러분의 생각을 공유해주세요.