Programming/V8 Engine

[ Node.js #02 V8 아키텍처 JIT ] Node JS V8 Engine 이란?

들어가기에 앞서

Nodejs 동작원리를 이해하기 위해 V8 공식 홈페이지를 기반으로

과거(2015년)부터 현재(2023년)까지 초기 아키텍처를 변역하며 연구하는 과정입니다.

 

초기 아키텍처에서 현재 아키텍처로 변화하는 과정에서

어떤 부분이 메모리 누수가 있었고 어떤식으로 해결해왔는지

엔진 전문가가 아닌 입장에서 해석하는 과정이므로 작성에 틀린 내용이 있을 수 있습니다.

또한 공식 사이트에 간단하게 설명되어 있는 부분이 있다면 그 내용도 연구하고 작성하오니

틀린 내용이 있다면 댓글로 남겨주시면 정정하도록 하겠습니다.

 

Nodejs V8 엔진

V8 엔진이란?

C++로 개발된 Google의 JavaScript 및 WebAssembly 기반 오픈소스 엔진으로

V8은 JIT compilation를 사용하여 JavaScript 코드를 실행하는 방식입니다.

간단하게 정리하면 자바스크립트 코드를 받아 컴파일(Compile)하고 실행하는 엔진이며 Chrome 및 Node.js 등에서 사용됩니다.

 

자세한 역할을 알기 위해서는 컴파일(Compile)인터프릿 (Interpret)에 대해 이해가 필요합니다.

구분 인터프릿 (Interpret) 컴파일 (Compile)
번역 한줄 씩 번역 후 실행 전체를 번역 한번에 실행
장점 코드 전체가 컴파일 된 Compilation이 완성되는 것을 기다릴 필요 없이, 한 줄 한 줄 변환하기 때문에 실행 속도가 빠르다 컴파일러는 작업을 단순화시키므로 효율적인 실행 코드가 생성된다.
(Optimization 불필요한 동작을 제거하는 컴파일러의 방식을 최적화) 
ex) 특정 함수를 10억번 반복해야 할 경우, 함수를 반복하지 않고 함수의 결과물을 반복하도록 컴파일 한다.
단점 실행시에는 인터프리터가 항상 필요하다. 기계어에 종속적인 실행코드가 생성되므로 실행 기계가 달라지면 새로이 컴파일해야 한다.
언어 Nodejs, Python, Visual basic, LISP C, C++, JAVA 등

 

이 글을 읽고 있으신 모든 개발자분들은

응용 프로그램이 제대로 작동하기 위해서는 메모리를 효율적으로 사용해야한다고 알고 있으실겁니다.

사용자가 이용하는 속도에 큰 영향을 미치는데 V8 엔진의 경우 Interpret 방식을 채택했습니다.

스크립트를 실행하기 위해 구문분석 및 컴파일을 하나씩 진행한다면 상당한 Overhead가 발생할 수 있겠죠..

그래서 그동안의 일반적인 웹 사이트는 Java를 기반으로 개발했다고 볼 수 있습니다.

 

필자가 위에서 Compile, Interpret 2가지를 설명하면서 V8엔진은 Interpret 방식을 사용했다고 말씀을 드렸는데

실제로 V8엔진은 JIT(juest-in-time comepilation)를 사용하여 JavaScript 코드를 실행합니다.

[JIT은 동적번역(Dynamic Translation)이라고도 불리우며 프로그램을 실제 실행하는 시점에 기계어로 번역하는 컴파일 기법입니다. ]

 

그렇다면 짧은 코드의 경우 Interpret 방식이 좋겠지만

대규모 플랫폼의 경우 기존과 동일하게 JAVA와 같이 Compile하여 개발해야하는게 아닌가? 라는 생각이 듭니다.

JIT가 무엇이길래 퍼포먼스를 낼 수 있을까라는 의문이 생기는데

답은 Code Caching 기술이 핵심입니다.

 

특히 Code Caching 기술은 V8 v4.2 부터 사용할 수 있으며 Chrome에만 국한하지 않고

V8의 API를 통해 노출되므로 모든 V8 embedder가 이를 활용할 수 있있다는 정보도 공식문서를 통해 확인할 수 있었습니다.

 

 

V8엔진의 Code Flow를 조금 더 이해하자면 다음 그림과 같습니다.

V8엔진 Flow [ Franziska Hinkelmann (Aug 16, 2017) ]

원본

1. JavaScript Source Code
    - 사용자가 JavaScript 코드 작성하고 실행

2. Parser

    AST(Abstract Syntax Tree)

Example of AST tree

 (이미지 출처)

    - Parser에서 JavaScript Source Code를 분석(Lexical Analysis 과정)을 거치고 AST로 변환합니다.

3. Ignition Interpreter

    - AST를 ByteCode로 변환합니다.

      (처음에는 ByteCode로 변환하는 과정에서 많은 시간이 소요될 수 있으나 추후 컴파일에 가까운 성능을 보임)

      원본 소스코드보다 컴퓨터가 해석하기 쉬운 바이트코드로 변환함으로서 원본코드를 다시 파싱해야하는 수고를 덜고

      코드의 양도 줄이면서 코드를 실행할 때 차지하는 메모리 공간을 아끼는 장점이 있습니다.

Code로 변환과정 [ Franziska Hinkelmann (Aug 16, 2017) ]

4. Compiler TurboFan

   - ByteCode를 실행하므로서 실제 우리의 코드가 작동하게 되고

   - 그 중 자주 사용되는 코드는 TurboFan으로 보내져서 Optimized Machine Code, 즉 최적화된 코드로 다시 컴파일된다

      [ Profiler는 최적화(Optimizes)가 가능한 부분을 찾아서 기록해둡니다. ]

   - JIT Compiler를 통해 엔진은 더 빠르게 실행하기 위해 최적화 피드백 데이터와 함께 컴파일러로 보냅니다.

   - 어떤 시점에서 가정 중 하나가 잘못된 것으로 판명되거나 사용이 덜된다 싶으면 Deoptimizing..

      최적화 컴파일러는 최적화를 해제하고 Interpret로 돌아갑니다.

 

 

 

* 위의 3, 4번의 역할 요약

Interpreter 방식으로 컴파일 하고 이를 ByteCode 로 만들며

Compile 속도를 높이기 위해  자주 쓰이는 ByteCode를 인라인 캐싱(inline caching)과 같은 최적화 기법으로 최적화하는

이러한 방식을 JIT (Just-In-Time) Compiler 이라고 하며 Interpreter 의 느린 실행 속도를 개선하는 엔진의 핵심 입니다.

 

 

V8 엔진은 2015년 TurboFan을 처음 도입하게 되었고 2016년 Ignition을 도입하면서

5.9버전 이후로 Ignition과 TurboFan 를 전적으로 사용하고

기존에 사용하던 Full-codegen과 Crankshaft 컴파일 기법은 더는 V8에 사용하지 않게되었습니다.

따라서 2017년에 위의 그림과 같은 완전히 새로운 컴파일러 파이프라인을 구축하게 되었습니다.

 

Chrome은 동일한 스크립트가 며칠 내에 두 번 이상 표시되는 경우에만 캐시 데이터를 생성하도록 개발하였고

결과적으로 Chrome은 스크립트 파일을 실행 가능한 코드로 평균 2배 빠르게 변환할 수 있어서

사용자들이 지금과 같이 빠른 속도로 이용이 가능해졌다고 합니다.

 

 

그 외 JIT 장점을 덧붙이자면 ..

OS에 구애받지 않고 컴파일러에 인터프리터가 들어가 속도가 빠르다는 장점을 가지고 있는점 참고해주세요!

 

 

Node.js의 V8 엔진의 매력은 Code Caching만이 아닙니다.

다양한 역할로 성능을 발휘하는데 다음 포스팅을 통해 공유하겠습니다.

 

 

 

참고자료

 - V8 공식홈페이지

 - RisingStack