자바스크립트 엔진의 최적화 방식 (AJITC)

·2023년 8월 19일
7

프론트엔드

목록 보기
4/12
post-thumbnail

목차

JITC (Just In Time Compiler)
JITC in Javascript
Adaptive JITC
V8 엔진 작동방식
인용
마지막으로
레퍼런스

JITC (Just In Time Compiler)

JIT 컴파일러는 컴파일러와 인터프리터의 장점을 합치고자 만들어진 개념입니다.

현재 많이 사용되는 Javascript 엔진은 모두 JITC 방식을 사용합니다. JIT 컴파일러는 처음에 소스코드를 파싱하여 중간언어(IR)인 바이트 코드 형태로 먼저 변환합니다.

인터프리터 모드라면 ? ➡️ 바이트 코드를 하나씩 읽어가며 동작을 수행합니다.
JIT 모드라면? ➡️ 생성된 바이트 코드를 기반으로 네이티브 코드로 컴파일합니다.

빠르고 쉬운 이해를 위해 컴파일러와 인터프리터의 차이점을 짚고 넘어가겠습니다.

💻 컴파일러

실행시간

컴파일러는 프로그램 전체를 스캔하여 이를 모두 기계어로 번역합니다. 이 때문에 대개 컴파일러는 초기 스캔 시간이 오래 걸립니다. 다만 초기 스캔을 마치면 실행파일을 만들어 놓고 다음에 실행할때 이전에 만들어 놓았던 실행파일을 실행하기 때문에 전체 실행 시간만 따지고 보면 인터프리터 보다 빠릅니다.

메모리효율

고급 언어로 작성된 소스를 기계어로 번역하고 이 과정에서 오브젝트 코드라는 파일을 생성합니다. 이 코드를 묶어 하나의 실행 파일로 다시 만드는 링킹(Linking) 이라는 작업을 거칩니다. 이때문에 컴파일러는 통상적으로 인터프리터 보다 많은 메모리를 사용합니다.

오류발견 시점

컴파일러는 오류 메시지를 생성할때 전체 코드를 검사한 후에 오류 메시지를 생성하기 때문에 실행 전에 오류를 발견 할 수 있습니다.

언어

C,C++,JAVA 언어 등이 컴파일러를 사용합니다.

💻 인터프리터

실행시간

인터프리터는 실행시 소스 코드를 한 줄씩 변환합니다. 이 때문에 프로그램 실행은 컴파일러에 비해 상대적으로 느립니다.

메모리효율

컴파일러와 같은 링킹(Linking) 과정이 없기 때문에 메모리 효율이 좋습니다.

오류발견 시점

인터프리터는 한번에 한문장씩 번역하기 때문에 프로그램을 실행시키고 한 문장씩 번역될때 오류를 만나게 되면 바로 프로그램을 중지합니다. 프로그램을 실행해봐야지만 오류를 발견 할 수 있습니다.

언어

Python, Ruby, Javascript 언어 등이 인터프리터를 사용합니다.

JITC in Javascript

JITC는 빠릅니다. But, Javscript 에서는 아닐 수도 있습니다.

1. 동적언어

JavaScript는 잘 알려진 대로 변수의 타입이 동적인 언어입니다 이러한 특성상 JavaScript JIT compiler는 모든 예외적인 케이스를 다 고려하여 코드를 생성해야 합니다.

function carculate(a,b){
  return a + b;
};

carculate(a,b); 
// 간단한 덧셈 함수이다. 하지만 JavaScript에서는 변수 `a` 와`b`에 다양한 타입의 데이터가 들어갈 수 있다.

만약 a,b 둘다 정수형 타입이면 더해서 변수 값에 저장하면 그만입니다. 하지만 둘 중 하나다로 다른 타입을 가지고 있거나 혹은 더하고 보니 예상 범위를 벗어나는 등 예외 케이스가 발생하게 되면 slow case로 코드를 넘겨버립니다.

*slow case

slow case는 native code로 생성하기 어려운 (정확히는 native code로 표현하면 양이 많아지는) 동작들을 native code로 뽑아내는 것이 아니라 엔진 내부에 C로 구현된 function(helper function)을 호출하여 동작을 수행하는 경우를 말합니다.
*조금 더 쉽게말하면 바이트 코드를 네이티브코드로 컴파일링 하지 않고, 바로 인터프리팅 한다는 것입니다.

만약 덧셈에서 int+int, string+string, string+int 이런 케이스를 모두 native code로 뽑아낸다면 단순한 덧셈을 위해 엄청나게 많은 native code가 필요하게 되기 때문입니다. 즉, JIT compiler로 native code를 수행한다 해도 많은 부분은 interpreter와 별반 차이가 없게 됩니다. 게다가 심지어는 컴파일 오버헤드( * 컴파일로 인한 과부화 ) 가 더해지게 되므로 JavaScript JITC는 다른 컴파일 언어에 비해 비효율적입니다.

2. 초기 JavaScript 의 한계성

현재는 좀 다르지만 초기의 JavaScript가 사용되는 용도는 주로 복잡한 연산식 보다는 web page의 layout을 건드리거나 사용자 입력에 반응하는 방식의 프로그램이 많았습니다. JavaScript는 상대적으로 hotspot이 매우 적습니다.

* hot spot

자주 반복돼서 수행되는 구간(hotspot)이 얼마나 많은가. 즉 최적화가 필요성이 높은 부분

결국 예전 기준으로 hotspot이 별로 없는 JavaScript는 interpreter로 수행하는 것이 나았습니다.

다만 최근에는 꼭 그렇다고 말하기가 어렵습니다. 최신 JavaScript는 비즈니스 로직에도 어느 정도 관여를 할 만큼 많은 일들을 수행하도록 요구되어갑니다. 이는 결국 대다수의 JavaScript Engine들이 인터프리터 방식보다 JITC 방식을 사용한 이유입니다.

우리는 고전적인 방식의 JavaScript 코드와 compute-intensive 코드들의 수행 성능을 모두 만족시킬 수 있는 방법이 필요했습니다.

Adaptive JITC

인터프리팅을 하다가, 필요한 부분에 대해서는 컴파일을 하여 최적화를 한다.

최근의 JS엔진들은 JIT의 업그레이드 버전인 Adaptive JITC를 사용합니다. AJITC는 모든 코드를 일괄적으로 같은 최적화를 적용하지 않고, 반복 수행 정도에 따라 유동적으로(adaptive) 서로 다른 최적화 수준을 적용하는 방식입니다.

출처 : https://meetup.nhncloud.com/posts/77

기본적으로 모든 코드는 처음에 interpreter로 수행합니다. 자주 반복되는 부분(hotspot)이 발견되면 그 부분에 대해서만 JITC를 적용하여 native code로 수행합니다.

V8 엔진 작동방식

V8은 구글에서 개발한 오픈 소스 자바스크립트 엔진으로, C++로 개발되었습니다. 이 엔진은 주로 크롬 웹 브라우저와 Node.js 같은 환경에서 사용됩니다. V8은 현대의 IT 환경에서 광범위하게 사용되는 Adaptive JITC(Just-In-Time Compilation) 방식을 활용하기 때문에 어떻게 작동하는지 자세히 알아보면 좋을 것 같다고 생각 되어 Adaptive JITC 다음 목차에 넣었습니다.

*참고로 "Adaptive JITC"는 V8 엔진이 사용하는 특정한 컴파일 방식이 아닌 개념적인 용어일 뿐입니다.

V8 엔진은 Ignition과 TurboFan이라는 두 가지 주요 JIT 컴파일러를 사용합니다. Ignition은 빠른 시작을 위한 컴파일러이고, TurboFan은 고급 최적화를 위한 컴파일러입니다. 이 두 컴파일러를 조합하여 Adaptive JITC를 실현합니다.

동작 순서

  1. V8 Engine은 JS 소스코드를 받으면 Parser에 보낸다.
  2. Parser는 소스코드를 파싱 한 후 AST를 만들어 Ignition에 보낸다. 💡var 호이스팅은 참고로 이단계에서 이루어진다.
  3. 인터프리터인Ignition에서는 AST를 바이트 코드로 '중간 번역' 하고 실행시킨다.
  4. V8 Engine은 런타임 과정 중 지속적인 프로파일링을 통해 Hot spot 즉 반복되어 사용하는 코드 등 과열지점을 찾는다.
  5. 과열 코드를 TurboFan 으로 보내 최적화 컴파일을 진행한다.
  6. Hot Spot을 식혔다 ! 실행속도는 더 높아질 것이다.

출처: https://medium.com/@poojasharma_93670/sneak-peek-into-javascript-v8-engine-d2bb2eb2bdb2 [Sneak peek into Javascript V8 Engine by Pooja Sharma]

V8 나라의 일꾼들

🤔 Parser : 나는 js 소스코드를 파싱할게. (뚝딱뚝딱 AST를 만든다)
내가 만든 AST 야. Ignition 아 받아~!

😎 Ignition: AST 를 넘겨받았어! 이제 나는 바이트 코드(Bytecode)로 변환할거야. 이렇게 하면 컴퓨터가 좀 더 해석하기 쉬워질거야. 코드양도 줄이고 실행때 차지하는 메모리 공간도 아낄 수 있지. (열심히 실행시키는중)

👾 V8 Engine은 런타임 : Profiler! Hot Spot좀 모아서 TurboFan 에게 넘겨봐.
🚀 Profiler : (Hot Spot 찾는중...)
이 코드는 너무 반복적으로 사용되어 과열되고 있어. TurboFan! 너한테 넘길게.

🥰 TurboFan : ㅇㅋ~ 최적화하고 코드 다시 컴파일 할게~
다시 사용 덜된다 싶으면 최적화 해제 할거야 ~

V8 엔진은 어떻게 내 코드를 실행하는 걸까?

https://evan-moon.github.io/2019/06/28/v8-analysis/#v8-%EC%97%94%EC%A7%84%EC%9D%98-%EC%9E%91%EB%8F%99%EC%9B%90%EB%A6%AC%EB%A5%BC-%EC%82%B4%ED%8E%B4%EB%B3%B4%EC%9E%90

또는 해당 글을 보면 작동 방식을 더욱 빠르게 이해하실 수 있습니다.

인용

정원기님 NHN엔터테인먼트 / TOAST앱개발팀

" 만약 성능이 좋은 JavaScript 코드를 만들고 싶다면, JavaScript 코드를 작성할 때 마치 C나 Java처럼 static typing 언어라고 생각하세요.
특히 array가 중요한데, 하나의 array에는 하나의 type만 넣어주는 것이 최고입니다! "

마지막으로

해당 레퍼런스들을 참고해서 저만의 언어로 해석하려고 노력했습니다. 글을 보고 제대로 이해하는 것이 어려워 직접 블로그 글을 작성하기로 결심했는데 너무 좋은 레퍼런스들이 많아서 글 작성에 도움을 많이 받았습니다.

레퍼런스

인터프리터와 컴파일러 차이
https://velog.io/@jhur98/%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%ACcompiler%EC%99%80-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0interpreter%EC%9D%98-%EC%B0%A8%EC%9D%B4

JIT Compiler & Chrome V8 Engine
https://velog.io/@kich555/JIT-Compiler-Chrome-V8-Engine
https://yceffort.kr/2020/11/deep-dive-into-v8

https://gist.github.com/snaag/5943c77869498a30310e5b13b53aaae3
https://samslow.github.io/development/2020/07/06/JIT/

자바스크립트는-Compiler-Interpreter-언어다
https://velog.io/@seungchan__y/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-Compiler-Interpreter-%EC%96%B8%EC%96%B4%EB%8B%A4

v8엔진
https://evan-moon.github.io/2019/06/28/v8-analysis/#v8-%EC%97%94%EC%A7%84%EC%9D%98-%EC%9E%91%EB%8F%99%EC%9B%90%EB%A6%AC%EB%A5%BC-%EC%82%B4%ED%8E%B4%EB%B3%B4%EC%9E%90

profile
My Island

0개의 댓글