[번역] V8은 자바스크립트 코드를 어떻게 최적화 시키는가

우선, 이 글은 아래 링크를 번역한 것입니다. 번역이 저질이라 그렇지 영어가 되시는 분들은 원문을 봐도 좋을 것 같습니다. (this post is translated the link below. if this post make problem, please let me know, i will remove this post.)


이 글을 읽는 사람들 중 JavaScript가 C++만큼 빠르게 실행된다고 들은 사람이 있을 겁니다. 그 사람들은 그게 어떻게 가능한지는 모릅니다. JavaScript는 JIT(Just in Time) 컴파일과 함께 동작하는 동적인 언어라면, C++은 AoT(Ahead of Time) 컴파일에 의한 정적 언어입니다. 최적화 된 JavaScript 코드는 C++과 비슷하게 또는 조금 느리게 실행됩니다.

왜 그런지 이해하기 위해 V8 동작의 몇 기본지식을 알고 있어야 합니다. 그것은 매우 큰 범위의 주제이고 이 포스트에서만 핵심만 보도록 하겠습니다.

AST(Abstract Syntax Tree)

모든 것은 JavaScript 코드와 AST에서 출발합니다.

컴퓨터에서 AST or Syntax Tree란 프로그래밍 언어로 짜인 소스코드의 추상화 논리구조의 트리 표현이다. 트리의 각 노드는 소스코드에서 발생하는 건설을 의미한다. abstract 구문은 실제 구문에서 나타나는 모든 디테일을 표현하진 않는다.

AST로 뭘 할 수 있을까요? 코드를 최적화 하기 전에 low level 표현으로 변환할 필요가 있습니다. 이건 AST에 필수적입니다. AST에는 V8 분석을 위한 모든 정보를 담고 있습니다. 모든 코드는 AST로 변환되며 컴파일러를 거쳐 full-codegen이 됩니다.

Full-Codegen Compiler

이 컴파일러의 주 목적은 JavaScript 코드를 가능한 빨리 native code로 최적화 없이 컴파일 하는 것입니다. 이것은 다양한 case 를 다루며 JavaScript 함수의 여러 위치에 있는 자료형과 같은 정보를 수집합니다.

이것은 AST를 필요로 하며 microprocessor를 직접 호출합니다. 이 연산의 결과는 일반적인 native code 입니다.

특별한 건 없습니다. 이게 전부입니다. 최적화는 여기서 이뤄지지 않으며 런타임 프로시져 호출에 의해 복잡한 코드가 다뤄지고 모든 로컬 변수가 heap에 저장됩니다.

Crankshaft Compiler

언급했듯이, full-codegen 컴파일러는 함수의type-feedback 정보를 수집해 code로 부터 일반적인 native code를 생성합니다. 자주 호출되는 함수는 crankshaft가 AST를 활용하여 최적화 된 코드가 되도록 컴파일합니다. 그 후 최적화된 기능은 OSR(on-stack replacement)을 사용해 최적화 되지 않은 코드로 대체됩니다.

그러나 모든 경우에 최적화 된 기능이 적용되는 것은 아닙니다. 어떤 type이 잘 못 되었다면, 예를들어 정수 대신 실수를 리턴했다면 최적화 된 기능은 해제되며 최적화 되지 않은 코드로 대체됩니다. 예를 들어 두 개의 숫자를 더하는 함수가 있습니다.

const add = (a, b) => a + b;
// Let's say we have a lot of calls like this
add(5, 2);
// ...
add(10, 20);

우리가 정수만 가지고 이 함수를 호출했다면, type-feedback 정보는 a,b 인자가 정수라는 정보로 구성됩니다. 이 정보와 함수의 AST를 이용해서 crankshaft는 이 함수를 최적화 할 수 있습니다. 허나 아래와 같이 호출하면 모든게 망합니다.

add(2.5, 1); // float number as the first argument

이전 type-feedback 정보를 근거로 crankshaft는 이 함수의 인자는 오직 정수라고 생각합니다. 근데 우린 실수를 전달했습니다. 이런 경우를 다룰 최적화 된 코드가 없기 때문에 최적화 되지 않습니다.

Crankshaft가 모든 걸 마법처럼 동작시켜 줄 수 없냐고 물어볼수 있겠네요. 음.. 몇 가지 방법이 있습니다.

Hydrogen Compiler

Hydrogen은 AST와 type-feedback 정보를 입력으로 받습니다. 그 정보를 근거로 SSA(static-single assignment)에 있는 control-flow graph로 구성된 고 수준의 표현(HIR — high-level intermediate representation)을 생성합니다.

HIR 생성 동안, constant folding, 메소드 인라인과 같은 최적화과 적용됩니다. 이 최적화의 결과는 최적화 된 control-flow graph 이며 다음 컴파일러(Lithium)의 입력으로 사용됩니다.

Lithium Compiler

Lithium 컴파일러는 최적화 된 HIR을 통해 기계 고유의 저 레벨 표현(LIR-low-level intermediate representation)을 생성합니다. LIR은 개념적으로 기계어랑 비슷하지만 여전히 platform 독립적입니다. LIR 생성동안 crankshaft는 몇 저 수준 레벨의 최적화를 수행합니다. LIR 생성 후에 crankshaft는 각 Lithium 명령을 위한 일련의 native 명령어를 생성합니다. 그 후 native 명령어들은 실행됩니다.

Summary

위의 모든 작업들은 V8 내부에서 JavaScript 코드를 C++만큼 빠르게 실행 시키기 위해 발생합니다. 그리고 여전히 JavaScript 코드를 최적화 되지 않도록 우리가 만들 경우, JavaScript 최적화 효과를 얻을 수 없다는 것을 알아야 합니다.