[JavaScript] Engine - Compiler & Interpreter

joong dev·2021년 1월 2일
0

Javascript

목록 보기
1/5
post-custom-banner

컴퓨터는 0과 1밖에 이해하지 못한다. 하지만 우리가 JavaScript로 코딩할 땐 사람이 이해할 수 있는 언어로 작성한다. 이것을 컴퓨터가 읽을 수 있도록 변환 작업을 해주는 녀석이 JavaScript Engine이다.(이하 JSE)

사람이 작성한 코드를 컴퓨터가 이해하기 위해서 코드를 변환하는 방식으로는 2가지가 있다. 우선 이 2가지를 알아야 한다.

Interpret

Interpret는 코드를 한 줄씩 순차적으로 bytecode(abstraction of machine code)로 변환하는 방식이다.

$ cat add.js
function add(a, b) {
    return a + b;
}
...
$ node --print-bytecode add.js
...
[generated bytecode for function: add]
Parameter count 3
Register count 0
Frame size 0
   12 E> 000001A2DFA15B36 @    0 : a5                StackCheck 
   26 S> 000001A2DFA15B37 @    1 : 25 02             Ldar a1
   35 E> 000001A2DFA15B39 @    3 : 34 03 00          Add a0, [0]
   39 S> 000001A2DFA15B3C @    6 : a9                Return 
Constant pool (size = 0)
Handler Table (size = 0)

이것은 위에서 볼 수 있듯이 bytecode 역시 컴퓨터가 바로 이해할 수 있게 0, 1로 이루어진 row-level 수준의 언어가 아니다. 오히려 intermediate-language라고 불리기도 한다. 즉, 이 bytecode를 가지고 컴퓨터가 이해할 수 없기에 이것을 받아서 실행해주는 프로세스 가상 머신이 필요하다.

예를 들면 Java의 경우 이 역할을 JVM(Java Virtual Machine)이 하는 것이고, JavaScript에서는 어떤 JSE를 사용하냐에 따라 다르겠지만 V8의 경우 Ignition bytecode executor가 할 것이다. 사실 V8의 Ignition은 Interpreter 역할도 맡고 있다.

이 프로세스 가상 머신이 런타임에 호출되는 부분을 골라서 실행한다.

이렇게 컴퓨터가 바로 이해할 수 있는 언어가 아닌 중간 언어를 만들고 그것을 실행하는 녀석에게 전달해줘야 하기 때문에 밑의 다른 방식인 compile에 비해 느린 속도를 보인다.
하지만 런타임에 호출되는 부분만 실행하기 때문에 동작 중에도 수정, 디버깅이 가능하다. 또한, 프로세스 가상 머신만 있다면 어디서든 같은 코드를 실행할 수 있기 때문에 특정 환경에 종속적이지 않다.

Compile

Compile은 코드를 실행하기 전에 컴퓨터가 이해할 수 있는 언어로 변환해두는 방식이다. 한 번 변환해두면 컴퓨터가 기계어를 보고 바로 실행하는 방식이기 때문에 Interpret 대비 빠른 속도를 보인다.
하지만 실행 전에 변환하기 때문에 런타임에 수정, 디버깅이 불가능하다는 단점이 있다. 또한, 컴퓨터의 OS나 CPU에 맞춰 이해할 수 있게 변환하기 때문에 빌드 환경에 종속적이기도 하다.

JIT Compiler

위에서 설명한 interpret 방식과 compile 방식의 장점을 섞어 구글에서 JIT(Just In Time) Compiler를 만들었고, 이 Compiler를 현재 Chrome의 V8 JSE에서 사용하고 있다.

V8(JSE)의 동작 방식

JSE는 아래 사진과 같은 순서로 작동한다.

출처: JavaScript engine fundamentals: Shapes and Inline Caches

노란 영역의 부분을 Chrome과 NodeJS의 JSE인 V8에 대응하면 아래 그림과 같다.

출처: JavaScript engine fundamentals: Shapes and Inline Caches

  1. JS파일이 들어온다
  2. parser가 JS파일을 위에서부터 순차적으로 낱말 단위로 분해한다.
  3. AST(Abstract Syntax Tree)를 생성한다.
    -> 작성한 코드의 tree는 https://astexplorer.net/ 에서 확인해볼 수 있다.
  4. Intrepreter가 AST를 받아 Bytecode로 변환한다.
  5. 실시간으로 Bytecode가 실행된다.
    -> V8에서 실행 주체는 Ignition이다.
  6. Bytecode는 실행되면서 최적화를 위해 profiling data와 함께 compiler에서 보내진다.
    -> Profiling data는 Chrome의 developer tools에서 확인이 가능하다.
  7. Compiler가 Profiler에게 받은 부분을 최적화한다.
    -> V8의 Compiler는 TurboFan이다.
  8. 최적화된 부분이 실행될 차례가 되면 Bytecode 대신 이 optimized code를 실행한다.
  9. 만약 최적화에 실패하면 원래의 Bytecode를 돌려준다.

하지만 code의 구현에 따라 Compiler가 최적화할 수 있는 정도가 다르다. 그렇기에 작동 방식을 이해하고 최적화하기 좋은 코드를 짤 수 있도록 노력해야 한다.

다음 포스팅 예고: JSE 최적화 방법

post-custom-banner

0개의 댓글