V8엔진과 JIT 컴파일러 그리고 히든 클래스

dev_dam·2024년 2월 5일
11

Web

목록 보기
1/2
post-thumbnail

들어가며

이번 블로그 주제는 자바스크립트는 코드를 한 줄씩 실행하는 인터프리터 언어라면서요. 그런데 사실 엄밀히 따지면 컴파일 언어라구요? 왜요?
라는 질문을 받았기 때문입니다.
그래서 V8 엔진에 대해 공부해야겠다고 생각했고 빠르게 알아보는 Javascript V8엔진 를 학습하며 정리한 내용으로 자세한 내용은 인프런 인강을 참고하시기 바랍니다.


인터프리터, 컴파일러, JIT 컴파일러

  • 인터프리터 : 위에서 아래로 코드를 한 줄씩 실행하는 방식으로 실행시간이 빠르지만, 성능이 컴파일러보다 좋지 않습니다.
  • 컴파일러 : 실행 전에 모든 코드를 컴파일(기계어로 번역)하기 때문에 실행시간은 조금 느리지만 이후 성능은 인터프리터보다 빠릅니다.
  • JIT(Just-In-Time) 컴파일러 : 인터프리터 방식과 컴파일러 방식을 혼합한 방식으로 코드가 실행 전에 그때그때 처리 및 최적화하는 방식

자바스크립트는 인터프리터 언어인가 컴파일러 언어인가?

인터프리터언어는 코드가 한 줄씩 실행되기 때문에 보통 실행되기 전까지 거치는 사전 단계가 없으므로 5번째 코드에 오류가 있어도 4번째 코드가 실행되지 않았다면 오류를 사전에 발견할 수 없습니다.

하지만 실행 전에 컴파일을 거치는 컴파일언어의 경우 5번째 코드에 오류가 있다면 실행 전 파싱 단계에서 오류를 미리 발견할 수 있으므로 사전에 오류를 차단할 수 있습니다.

자바스크립트는 현재 인터프리터언어로 분류되어 있지만 컴파일언어이기도 합니다.

어째서 자바스크립트는 컴파일언어이기도 한 걸까요?

그 이유는 자바스크립트는 정적 컴파일처럼 프로그램 실행 전에 미리 모든 코드를 컴파일하는 것이 아니라, 실행과정(런타임)에서 그때그때(JIT) 컴파일 처리가 된다는 차이점이 있어서 인터프리터언어면서 컴파일러언어입니다.

즉, 자바스크립트 엔진의 JIT 컴파일러의 동작 방식 때문에 컴파일언어 혹은 인터프리터언어라고 할 수 있습니다.

V8엔진의 파이프라인

1️⃣ Parser

자바스크립트 엔진은 자바스크립트 코드를 받으면 파서(Parser)를 통해서 어휘분석(Lexical Analysis) 과정을 거친 후 토큰으로 분해합니다.

그리고 구문분석(SyntaxAnalysis)과정을 진행하고 이때 문법 에러가 있으면 SyntaxError 를 출력합니다.

SyntaxError : Unexpected token ~~~ 에러 메시지를 보게 된다면 이때 오류가 발생한 것을 알 수 있습니다.

2️⃣ AST(추상구문트리)

AST란, 프로그래밍 언어로 작성된 코드를 구문 구조의 형태를 나타내는 트리 형태로 만드는 자료구조를 말합니다.

이렇게 코드를 작성하면 텍스트로부터 트리와 같은 데이터 구조를 얻습니다.

AST로 변환 확인하는 사이트 링크

3️⃣ Ignition 인터프리터

Ignition 인터프리터란, 바이트 코드를 생성하는 V8엔진의 빠른 인터프리터입니다.

AST의 결과물을 받은 Ignition 인터프리터는 Bytecode로 변환합니다.

이렇게 Ignition 인터프리터는 바이트코드로 변환하면서 이때 실행 컨텍스트가 생성되어 호이스팅이나 this 바인딩 과정이 이뤄집니다.

또한 Ignition 인터프리터는 바이트코드를 실행하면서 프로파일링 및 피드백 데이터를 수집하는데, 이때 수집된 정보는 TurboFan 컴파일러에서 최적화 기계어 코드를 생성하는 데 사용됩니다.

4️⃣ bytecode (IR)

바이트코드란, 기계어를 추상화한 코드이며 중간언어(IR)인 바이트 코드로 변환하는 것을 말합니다.
바이트코드 즉, IR로 변환된 코드들은 TurboFan, Maglev, SparkPlug 중 한 가지 형태의 컴파일 과정을 거치게 됩니다.

IR이란 고수준언어의 코드들을 가상머신이 편하게 이해할 수 있도록 중간언어로 번역한 것을 말합니다.
즉, 기계어로 바로 번역하는 것이 아닌 중간단계 언어로 번역하는 것입니다.

5️⃣ JIT 컴파일러 (TurboFan, Maglev, SparkPlug)

생성된 바이트 코드를 기반으로 기계어로 번역해주는 컴파일러입니다.

JIT(Just-in-Time) 컴파일러는 그때그때 실시간으로 컴파일하는 방식을 말하며, 인터프리터에 의해 코드가 수행될 때 해당 코드를 모니터링하여 필요에 맞는 컴파일러로 컴파일하여 실행합니다.

  • TurboFan : 최적화 컴파일러로, 프로파일링에 의해 자주 사용되는 코드 (hot code)를 최적화 시켜줍니다. 터보펜의 최적화 기법에는 대표적으로 히든 클래스, 인라인 캐싱등이 있으며 최적화했던 코드가 더이상 자주 사용되지 않는다고 판단되면 원래의 비 최적화 코드로 돌리는 역 최적화를 진행합니다.
  • Maglev : 2023년에 나온 컴파일러로 TurboFan SparkPlug 의 중간 단계의 최적화 컴파일러입니다. Sparkplug와 TurboFan 사이의 격차를 메우기 위해 나온 SSA 기반 JIT 컴파일러입니다. TurboFan과 마찬가지로 최적화가 필요없다면 역최적화를 진행합니다.
  • SparkPlug : 비최적화 컴파일러로, 빠르게 컴파일되도록 설계된 컴파일러입니다. SparkPlug는 bytecode를 기반으로 코드를 만듦으로서 오직 빠르게 컴파일 하는데 중점을 두었습니다.

정적 단일 할당(SSA)은 프로그래밍에서 사용되는 최적화 기법입니다.
SSA는 변수의 사용과 재 할당을 제한하는 정적 형태의 프로그래밍 언어 표현법으로 각 변수는 한 번만 값을 할당할 수 있고 재 할당은 새로운 변수를 만들어서 이루어집니다. 이로써 SSA는 변수의 의미를 더 명확하게 추적할 수 있게 되므로 코드를 보다 효율적으로 분석하고 변환할 수 있게 해줍니다.

6️⃣ Profiler

JIT컴파일을 거쳐서 코드가 실행(런타임)되면 프로파일러는 자주 사용되는 코드들인지 프로파일링하여 자주 사용 되는 코드(hot code)라면 TurboFan에게 넘겨줌으로써 최적화를 진행합니다.

사실 모든 엔진들의 파이프라인을 살펴보면 인터프리터와 컴파일러를 가지고 있다는 공통점이 있습니다.
자바스크립트 엔진들이 이렇게 컴파일러와 인터프리터의 계층을 나누는 이유는 코드의 실행 시간을 빠르게 가져갈지, 실행 시간은 좀 느려도 좋은 성능을 가져갈지의 트레이드 오프입니다.
트레이드 오프(trade-off)란, 하나를 얻으면 하나를 잃을 수 있다는 뜻입니다.


히든 클래스 (Hidden Class)

히든클래스는 터보펜(TurboFan)의 최적화 방법입니다.
히든 클래스란, 자바스크립트 엔진 내부의 최적화를 위해 사용되는 기법입니다.

히든 클래스는 객체의 구조와 속성을 추적하고, 접근에 대한 빠른 메모리 해결 방법을 제공하여 실행 속도를 향상시키는 기술입니다.

객체로 정의된 코드는 각각 프로퍼티 어트리뷰트(Property Attribute)를 가지게 되는데, 자바스크립트는 동적 언어로 코드를 실행하는 중에 타입이나 값이 변경될 수 있습니다.

이로 인해 컴파일 시점에 프로퍼티 Offset을 기억해뒀다가 쓰지 못하고 매번 프로퍼티를 찾아서 읽어야 하며, 프로퍼티 접근 속도가 떨어지고 그만큼 메모리도 많이 사용하게 됩니다.

오프셋(Offset)이란, 객체의 속성에 대한 메모리 접근 경로를 나타내며 각 속성은 객체의 메모리에서 일정한 위치를 차지하고 이 위치를 오프셋이라고 합니다.

히든 클래스의 기본 구조

만약 객체에 name, height, weight 의 속성을 가진 객체가 있으면 v8은 히든 클래스를 이용하여 위의 그림처럼 나타낼 수 있습니다.

각 프로퍼티 정보에는 value라는 내부 슬롯 대신에 offset 값이 들어가게 됩니다.

히든 클래스의 최적화 이점

만약 같은 프로퍼티가 여러 개 있다면 각 객체마다 프로퍼티를 중복으로 들고 있어야 하지만, 위의 그림처럼 객체들은 모두 동일한 히든 클래스를 공유하고 있습니다.

히든 클래스는 이렇게 동일한 프로퍼티를 가지고 있을 때 유용합니다.

이로 인해서 동일한 구조를 가지는 객체가 100개가 생성되어도 히든 클래스와 프로퍼티 정보는 한 번만 저장하면 되기 때문에 객체들의 프로퍼티에 접근할 때 동적 탐색을 피하고 이미 생성된 히든 클래스를 재사용 할 수 있다는 이점이 있습니다.

객체를 동적 생성했을 때의 히든 클래스

만약 빈 객체를 생성해서 동적으로 프로퍼티를 할당할 때에는 human에 빈 객체를 할당하고 Object는 empty의 히든 클래스를 가리키게 됩니다.

그리고 한 줄씩 코드가 실행되면서 위의 그림처럼 프로퍼티별로 각각의 히든 클래스를 생성하고 가리키게 됩니다.

각 히든 클래스들은 Transition Chain으로 연결되는데, 포인터를 생성하여 이전 히든 클래스에 대한 참조 정보를 저장하여 서로 체인 형태로 연결되어 있습니다.

Transition Chain이란, 자바스크립트 엔진이 객체의 속성을 찾는 과정에서 사용되는 내부 메커니즘입니다.

즉 히든 클래스들은 이전 히든 클래스에 대한 정보를 들고 있어서 최종적으로는 위의 이미지처럼 진행됩니다.

만약 human.name에 접근하는 경우 name 프로퍼티가 포함된 히든 클래스를 찾기 위해 엔진은 Transition Chain을 거슬러 올라가며 하나씩 찾게 됩니다.

객체를 한 번에 생성하는 것보다 동적으로 생성할 때 거치는 과정이 더 많으므로 객체를 생성할 때는 특별한 이유가 있지 않다면 한 번에 속성을 정의해서 객체를 생성하는 것이 Transition Chain을 단축하고 하든 클래스의 개수를 최소화할 수 있어서 최적화 이점을 챙길 수 있습니다.

객체 속성에 따른 히든 클래스 비교

여기까지 히든 클래스에 대해 살펴보고 나서 드는 의문점이 두 가지가 있습니다.

  • 객체의 프로퍼티 속성의 순서가 다르면 동일한 히든 클래스를 공유할까?
  • 객체의 프로퍼티의 값이 다르면 동일한 히든 클래스를 공유할까?

먼저, 객체의 프로퍼티 속성의 순서가 다르면 서로 다른 히든 클래스를 가지게 됩니다.

그 이유로는 히든 클래스가 만들어지는 과정은 객체의 속성이 추가될 때마다 기존에 있던 히든 클래스에서 Transition이 일어나 새로운 히든 클래스가 만들어지고 추가된 속성에 대한 메모리 Offset이 각각 설정되기 때문입니다.

코드가 한줄씩 실행되는 Ignition 인터프리터이기 때문에 객체가 생성될 때 순서대로 Offset이 설정됩니다.

그래서 프로퍼티 속성의 순서가 다르다면 서로 다른 히든 클래스를 가지게 되며, 속성에 대한 memory Offset 또한 달라집니다.

두 번째로 객체의 프로퍼티의 속성명은 같지만, value의 값이 다르면 어떻게 될까요?

정답은 같은 히든 클래스를 공유하게 됩니다.

현재는 string, null, undefined, {}, [], number, symbol 모두 비교했을 때 같은 히든 클래스를 가지는 걸 확인할 수 있지만 추후 히든 클래스의 관리 방식이 바뀔 수 있습니다.

히든 클래스 정리

  • 객체는 한 번에 생성하는 게 좋습니다.
  • 객체를 생성할 때는 동일한 순서의 프로퍼티 속성으로 작성하는 것이 좋습니다.
profile
병아리에서 닭이 될 때까지

1개의 댓글

comment-user-thumbnail
2024년 2월 6일

자바스크립트는 인터프리터 언어다!!라고만 알고있었는데 이런 뒷내용이,, 잘읽었습니다!

답글 달기

관련 채용 정보