하루에 한 문제

V8 작동원리 본문

Dev/JavaScript

V8 작동원리

dkwjdi 2021. 4. 20. 01:33

우리가 작성한 자바스크립트 파일을 컴퓨터가 읽을 수 있는 것은 자바스크립트 엔진이 있기에 가능한 것이다.

자바스크립트 엔진은 일련의 과정을 거쳐 컴퓨터에 최적화된 코드로 변환해 전달하게 된다.

이번 글에서는 자바스크립트엔진 중 가장 많이 사용하는 V8 엔진의 작동원리를 설명해보겠다!

 

 

컴파일러와 인퍼프리터

V8엔진이 어떻게 작동하는지 알아보기 전, 프로그래밍에서 사용하는 두 가지 번역 방식에 대해 먼저 알아보자.

 

인터프리터의 경우 자바스크립트 파일을 입력받으면 한 줄 한 줄 해석하면서 중간 단계의 Bytecode로 변환한다. 반면 컴파일러는 자바스크립트 코드를 입력받으면 파일 전체를 읽은 뒤, 이를 컴파일하여 기계어로 변환한다. 그리고 이 기계어(Machine Code)는 CPU로 입력되어 코드가 실행된다.

 

인터프리터와 컴파일러, 둘 중 어떤 것이 더 나은 방식일까? 절대적으로 더 나은 것은 없다. 각각의 장단점을 알고, 언제 어떻게 사용하는 것이 효율적인지를 아는게 중요하다.

 

인터프리터 vs. 컴파일러

1. 인터프리터

  • 장점 : 코드 전체가 컴파일 된 Compilation이 완성되는 것을 기다릴 필요 없이, 한 줄 한 줄 변환하기 때문에 실행 속도가 빠르다. 자바스크립트는 웹을 위해 개발된 언어이고, 유저에게 최상의 경험을 제공해야 하기 때문에 빠르게 실행되는 것이 중요한 자바스크립트에 이상적인 코드 변환 방식이다.
  • 단점 : 자바스크립트 코드가 복잡해질수록 점점 속도가 느려진다. 예를 들어, 같은 코드를 여러차례 반복하는 반복문의 경우, 같은 결과를 반복하는 것임에도 불구하고 코드를 한 줄 한 줄 읽는 방식 때문에 10억번을 반복해야 한다면 말 그대로 10억번을 반복해서 코드 변환을 진행하게 된다.

2. 컴파일러

  • 장점 : 컴파일러는 작업을 단순화시킨다. 예를 들어 특정 함수를 10억번 반복해야 할 경우, 컴파일 과정에서 함수를 반복하는 것이 아니라 함수의 결과물을 반복하도록 컴파일 한다. 이처럼 불필요한 동작을 제거하는 컴파일러의 방식을 최적화, optimization이라고 한다. (인터프리터는 optimize하지 않는다.)
  • 단점 : 코드를 바로 실행하지 않고, 코드 실행 전 전체를 컴파일 하는 과정이 필요하기 때문에 초기에 속도가 느릴 수 밖에 없다.

자바스크립트 엔진의 내부를 공부하다가 잠시 인터프리터와 컴파일러의 간단한 개념과 장단점을 짚어봤다. 그러면 과연 V8 엔진은 어떤 방식을 취했길래 다른 자바스크립트 엔진보다 더 빠르고 효율적으로 자바스크립트 명령을 수행할 수 있을까? 바로 여기에서 V8 엔진의 독특한 언어 변환 도구인 JIT Compiler가 들어온다. JIT은 Just In Time의 약자이다.

 

 

JIT컴파일러 (Just In Time Compiler)

자바스크립트 V8 엔진은 위 사진과 같은 구조로 되어있다. 순차적으로 보자

 

1. Parser : 낱말 분석(Lexical Analysis) 이라는 과정을 통해 코드를 토큰으로 분해한다.
ex) 'var a = 5' -> ['var' , 'a' , '=' , '5']

 

2. AST(Abstract Syntax Tree) : Parser에서 분해된 토큰을 바탕으로 tree를 생성한다.

 

3. AST에서 나온 코드가 인터프리터(ignition)에게 전달되고 인터프리터는 코드를 빠르게 Bytecode 로 변환한다.

바이트 코드(Bytecode)는 고오급 언어로 작성된 소스 코드를 가상머신이 한결 편하게 이해할 수 있도록 중간 코드로 한번 컴파일 한 것을 의미한다. V8에서는 Ignition이 이 역할을 수행하고 있다.

Ignition을 개발할 때는 모든 소스를 한번에 해석하는 컴파일 방식이 아닌 코드 한줄 한줄이 실행될 때마다 해석하는 인터프리트 방식을 채택하여 다음 세가지 이점을 가져가고자 하였다.

  1. 메모리 사용량 감소. 자바스크립트코드에서 기계어로 컴파일하는 것보다 바이트코드로 컴파일하는 것이 더 편하다.
  2. 파싱 시 오버헤드 감소. 바이트 코드는 간결하기 때문에 다시 파싱하기도 편하다.
  3. 컴파일 파이프 라인의 복잡성 감소. Optimizing이든 Deoptimizing이든 바이트 코드 하나만 생각하면 되기 때문에 편하다.

4. 이후 이 바이트 코드를 실행함으로써 우리의 소스 코드가 실제로 작동하게 되고, 그 중 자주 사용되는 코드는 TurboFan으로 보내져서 Optimized Machine Code, 즉 최적화된 코드로 다시 컴파일된다. 그러다가 다시 사용이 덜 된다 싶으면 Deoptimizing 하기도 한다

     - 인터프리터가 코드를 실시간으로 변환하면서 브라우저에게 작업을 지시하는동안 프로파일러가 최적화 할 수 있는        부분을 찾아서 기록한다.

     - 최적화가 가능한 부분을 찾으면 프로파일러는 이를 컴파일러에게 전달하고, 컴파일러는 인터프리터에 의해

       실시간으로 웹사이트가 구동되는 동안 필요한 부분을 기계어로 변환하여 최적화를 진행한다.

     -최적화한 코드를 수행할 차례가 다가오면, Bytecode 대신 컴파일러가 변환한 최적화된 코드가 그 자리를 대체하여        실행된다.

V8은 런타임 중에 Profiler라는 친구에게 함수나 변수들의 호출 빈도와 같은 데이터를 모으라고 시킨다. 이렇게 모인 데이터를 들고 TurboFan에게 가져가면 TurboFan은 자기 기준에 맞는 코드를 가져와서 최적화를 하는 것이다.

최적화 기법으로는 히든 클래스(Hidden Class)나 인라인 캐싱(Inline Caching) 등 여러가지 기법을 사용하지만 이 내용은 추후 다른 포스팅에서 더 자세히 다루도록 하겠다. 간단히만 설명하자면 히든 클래스는 비슷한 놈들끼리 분류해놓고 가져다 쓰는 것, 인라인 캐싱은 자주 사용되는 코드가 만약 hello()와 같은 함수의 호출부라면 이걸 function hello () { ... }와 같이 함수의 내용으로 바꿔버리는 것이다. 말 그대로 캐싱(Caching)이다.

 

어떤 조건으로 최적화 하는 걸까?

1. 코드가 뜨겁고 안정적인 것, 쉽게 말하면 자주 호출되고(뜨겁고) 코드가 안 변함(안정적)이라는 것이다. 매번 같은 행동을 수행하는 반복문 내에 있는 코드 같은 경우가 여기에 해당하기 쉽다.

 

2. 인터프리팅된 바이트 코드의 길이를 보고 특정 임계점을 넘기지 않으면 작은 함수라고 판단해서 최적화를 진행하는 것이다. 작고 단순한 함수는 크고 복잡한 함수보다 동작이 매우 추상적이거나 제한적인 확률이 높기 때문에 안정적이라고 볼 수 있다

 

 

한편, 컴파일러는 100% 완벽하지 못하기 때문에 의도와 다르게 발적화(deoptimization)가 일어날 수도 있다. 따라서 현재까지 이해한 자바스크립트 엔진의 구동 원리를 기반으로, 빠르고 효율적인, 최적화된 코드를 작성할 수 있는 방법을 알아보자.

 

dkwjdi.tistory.com/195

 

자바스크립트 런타임 : 콜스택과 메모리 힙

인라인 캐싱 // Inline Caching function findUser(user) { return `found ${user.firstName} ${user.lastName}`; } const userData = { firstName: 'Johnson', lastName: 'Junior' } findUser(userData); // 'fou..

dkwjdi.tistory.com

 

참고

evan-moon.github.io/2019/06/28/v8-analysis/

velog.io/@cheal3/JavaScript-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC

soldonii.tistory.com/52?category=862198

'Dev > JavaScript' 카테고리의 다른 글

프론트엔드 개발에 Node.js가 필요한 이유  (1) 2021.04.26
자바스크립트 런타임 : 콜스택과 메모리 힙  (0) 2021.04.20
Arrow Function에서 this바인딩  (0) 2021.04.20
Promise  (0) 2021.04.19
async VS defer  (0) 2021.04.17
Comments