JavaScript engine 의 optimizing compiler 에 관해 정리를 해보려고 한다.

주요 내용은 구글 V8 팀 멤버의 발표를 기반으로 한다.


JavaScript engine 이란

JavaScript engine 은 대표적으로 V8, SpiderMonkey, Chakra 등이 있다.

기본적으로 JavaScript engine 은 JavaScript 코드를 machine code 로 변환하는 역할을 한다.


JIT

dynamically typed 한 언어임에도 어떻게 그렇게 빠를 수 있을까?

JIT(Just In Time) compilation 에 의해 가능하다. JIT compilation 을 통해 machine code 를 runtime 에 생성한다.

AOT(Ahead Of Time) compilation 을 수행하지 않아서 프로그램을 실행하기 전에 미리 compile 하지 않는다.

compile 과 execution 의 단계를 나누지 않고 동시에 진행한다.

C++ 는 compilation 과 execution 이 별개로 이루어진다면 JavaScript 는 compilation 과 execution 이 동시에 이루어진다.


Optimizing compiler

re-compile hot functions with type information from previous execution

de-optimize if the type has changed


JavaScript engine flow

발표 11분 20초 즈음에 나온 이미지다.


v8 engine flow

발표 12분 50초 즈음에 나온 이미지다.

baseline compiler 에 대응되는 v8 의 baseline compiler 는 Ignition 이 담당한다.

optimizing compiler 에 대응되는 v8 의 optimizing compiler 는 TurboFan 이 담당한다.


Ignition 은 baseline compiler 와 달리 machine code 가 아닌 bytecode 로 변환한다.

machine code 는 CPU 에 의해 직접 동작하는 코드라면 byte code 는 JVM 과 같은 가상 머신에서 동작하는 코드다. 가상 머신에서 동작하므로 OS 의 종류에 관계없이 동작할 수 있다.


byte code 는 machine code 와 source code 중간에 놓인 코드다. CPU 는 byte code 를 이해할 수 없어서 machine code 로 변환하는 작업이 필요하다.

machine code 와 byte code 에 대한 차이는 추가 학습이 필요하다.


re-compile & de-compile

modern JavaScript engine 은 compiler 를 대체로 2개 이상 갖는데, 그 중 하나가 Optimizing compiler 다.


Optimizing compiler 는 hot function 을 recompiling 한다. hot function 은 많이, 자주 사용되는 함수라는 의미를 갖는다.

hot function 은 많이 사용되기 때문에 최적화(optimize) 할 필요가 있다. 이때의 최적화는 machine code 에 대한 최적화를 의미한다.


Optimizing compiler 는 많이 사용되는 함수들에 대해서 정보를 수집하는데 특히 타입에 대한 정보가 중요하다. 최적화를 진행할때 이전에 사용됐던 타입과 유사한 타입을 사용할 것이라고 예상한다.

그러나 JavaScript 는 dynamically typed 하기 때문에 다른 타입을 사용할 수 있다. 그러면 Optimizing compiler 는 optimzed code 에 대한 de-optimize 를 수행하게 된다. 최적화 시켜놓은 코드를 다시 최적화하지 않은 상태로 되돌린다.


가능한 type 을 변경하지 말 것

optimizing compiler 는 이전에 사용한 type 정보를 바탕으로 최적화를 진행하므로 발표자는 이미 작성한 코드의 type 을 바꾸지 말라고 조언한다. 타입을 바꾸면 de-optimizing 을 진행하게 된다.


function load(obj) {
  return obj.x;
}

load 함수는 파라미터로 선언한 obj 의 프로퍼티 x 를 반환하는 단순한 함수다.

그렇지만 compiler 에게는 꽤 복잡한 일이다. compiler 는 obj 가 x 를 실제로 갖고 있는지, 프로토타입 체인 어딘가에 속해있는지, 메모리 어디에 x 가 저장되어 있는지 등을 파악해야 한다.


function load(obj) {
  return obj.x;
}

load({x: 4, y: 7});
load({x: 2, y: 9});
load({x: 1, y: 3});
load({x: 6, y: 1});
load({x: 3, y: 8});

load 함수를 호출하면서 객체를 인자로 넣는다.

x 와 y 의 값이 다를지라도 같은 프로퍼티(키와 값을 가리킨다) 키 x, y 가 계속 쓰이면서 프로퍼티 값의 타입이 변하지 않고 계속해서 number 로 호출된다.

compiler 는 이를 hot function 이라고 간주하고 최적화를 진행한다.


이에 대한 assembly code 코드에서는 parameter 에 대한 타입을 저장해둔다.

새롭게 함수가 호출되면 저장해둔 paramter 의 타입과 새로 들어온 parameter 의 타입이 같은지 비교한다.

assembly code 에서는 obj 객체의 프로퍼티 키에 대한 메모리 주소 정보를 갖고 있다. 프로퍼티 키에 대한 일종의 지름길을 갖고 있어서 빠르게 접근할 수 있다.


타입이 다면 프로토타입 체인을 살펴보거나 side effect 가 있는지 등을 검사하지 않아도 되고, 프로퍼티 키의 메모리 주소에 바로 접근해서 바로 값을 가져올 수 있다.

타입이 다르다면 de-optimize 를 명령하는 메모리 주소로 이동한다.


타입은 프로퍼티 값에 대한 것 뿐만 아니라 프로퍼티 키의 종류도 해당한다.


function load(obj) {
  return obj.x;
}

load({x: 4, a: 7});
load({x: 2, b: 9});
load({x: 1, c: 3});
load({x: 6, d: 1});

compiler 는 위의 코드에 대해 프로퍼티가 다르므로 같은 타입이 아니라고 판단한다.

assembly code 에는 load 함수 호출에 사용된 객체인 {x: 4, a: 7}, {x: 2, b: 9}, {x: 1, c: 3}, {x: 6, d: 1} 에 대한 각각의 메모리 주소 정보(객체 자체 뿐만 아니라 프로퍼티에 대한 정보도 포함한다)를 갖는다.


load 함수에 대한 새로운 호출이 발생하면 4개와 각각 비교하게 된다.

4개 중 하나라도 같은 타입이 있다면 메모리 정보를 바탕으로 빠르게 프로퍼티 값에 접근할 수 있다.

4개 중 같은 타입이 하나라도 없으면 de-optimize 를 진행한다.


모든 JavaScript engine 에 해당하는 내용인지는 모르겠지만 4번을 초과하는 비교를 수행하지 않는다고 한다. 특정 함수 호출에 사용된 객체의 메모리 정보를 4개 까지만 갖고 이 다음부터는 추가로 객체의 메모리 정보를 저장하지 않는다.

객체의 프로퍼티 키에 대해 개별적으로 방대한 기존 메모리 리스트에서 프로퍼티 키와 같은 정보가 있는지 조회한다(없으면 어떻게 되는지는 설명하지 않았고 expensive 한 call 이라고만 표현했다).


Write code that looks like statically typed.

성능을 위해 객체에 대한 타입을 변경하지 말라고 얘기한다.

같은 프로퍼티 키를 유지하면서 값이 없으면 프로퍼티를 빼지 않고 차라리 값을 undefined 로 설정하는 편이 낫다고 한다. compiler 는 같은 타입으로 간주할거고 그러면 메모리에 빠르게 접근할 수 있다.


function load(obj) {
  return obj.x;
}

load({x: 4, a: 7, b: undefined, c: undefined, d: undefined});
load({x: 4, a: undefined, b: 9, c: undefined, d: undefined});
load({x: 4, a: undefined, b: undefined, c: 3, d: undefined});
load({x: 4, a: undefined, b: undefined, c: undefined, d: 1});

참고

https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8_%EC%97%94%EC%A7%84

https://www.youtube.com/watch?v=p-iiEDtpy6I

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

https://www.tutorialspoint.com/difference-between-bytecode-and-machine-code

https://byjus.com/gate/difference-between-byte-code-and-machine-code/

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

var, let, const 호이스팅  (0) 2023.02.16

변수 생성 3단계

1. 선언 단계

  • 실행 컨텍스트에 변수를 등록한다.
  • 변수 선언문을 의미하는게 아니다.

2. 초기화 단계

  • 실행 컨텍스트에 등록된 변수를 메모리에 할당한다.
  • var, let 의 경우 이 단계에서 변수에 undefined 가 할당된다.
  • var 의 경우 변수 선언 단계에서 초기화가 이루어진다.
    • 선언 단계와 초기화 단계가 동시에 발생한다.
  • let 의 경우 변수 선언문에서 초기화가 이루어진다.
    • 선언 단계와 초기화 단계가 따로 발생한다.

3. 할당 단계

  • 변수 선언문에 작성한 값을 변수에 할당한다.

변수 선언문과 변수 할당문

// 변수 선언문
var a;
let b;

// 변수 할당문
a = 1;
b = 1;

// 변수 선언문과 할당문이 함께 존재
const c = 1;

TDZ

스코프의 시작 지점부터 초기화 시작 지점까지는 변수를 참조할 수 없다. 스코프의 시작 지점부터 초기화 시작 지점까지의 구간을 ‘일시적 사각지대(Temporal Dead Zone; TDZ)’라고 부른다.


변수 선언문 이전에 변수를 호출하면 var 는 undefined, let 과 const 는 reference error 가 발생한다.

let, const 는 변수 선언문 이전에 변수 접근하는 상황에선 아직 변수의 초기화가 수행되지 않았다. let, const 의 초기화는 변수 선언문에서 발생하며 메모리 할당이 이루어진다.

var, let 의 경우 할당 값 없이 변수 선언만 있는 경우 undefined 가 초기화 시점에 할당된다.

let, const 는 변수 선언문 이전에 변수 접근하면 메모리에 변수가 할당되지 않아서 tdz 에 놓인 상황이라 reference error 가 발생한다.

var, let, const 모두 자신이 포함된 스코프가 실행 컨텍스트에 올라가면 코드가 실행되기 전에 실행 컨텍스트에 등록된다. 다만 초기화 여부의 차이로 선언문 이전에 호출시 undefined, reference error 로 다른 양상을 보인다.


var, let, const 키워드로 선언한 변수들 모두 호이스팅 된다

호이스팅(Hoisting)이란, var 선언문이나 function 선언문 등을 해당 스코프의 선두로 옮긴 것처럼 동작하는 특성을 말한다.

reference error

let, const 모두 호이스팅이 일어나지 않는 것처럼 보이지만 실제로는 둘 다 호이스팅이 발생한다.

let, const 는 상위에 선언된 1을 출력할 것 같지만 실제로는 호이스팅으로 인해 블록 스코프에 선언된 a 에 접근하다보니 (초기화가 수행되지 않아서) reference error 가 발생한다.


let a = 1;

{
  console.log(a);
  let a = 2;
}

// Uncaught ReferenceError: Cannot access 'a' before initialization

const a = 1;

{
  console.log(a);
  const a = 2;
}

// Uncaught ReferenceError: Cannot access 'a' before initialization

참고

https://poiemaweb.com/es6-block-scope

모던 자바스크립트 Deep Dive

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

JavaScript engine - optimizing compiler  (0) 2023.02.24

+ Recent posts