JIT Compiler & Chrome V8 Engine

kich555·2021년 9월 11일
7

I don't Know JavaScript

목록 보기
4/9
post-thumbnail

JIT Compiler

우리는 JIT Compiler에 대해 설명하기 전에 미리 알아두어야 할 것이 있다. 바로 중간 언어(IR) 이다.

중간 언어(IR)

중간언어(IR) 는 고급 언어와 기계어 사이의 언어를 뜻하는데 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법이다. 하드웨어가 아닌 가상 머신이라는 소프트웨어에 의해 처리되기 때문에, 보통 기계어보다 더 추상적이다. 크게 인터프리터확장성을 위해 만들어 졌으며 바이트 코드를 사용하여 하드웨어의 의존성을 줄인다.

* 중간 언어를 사용하지 않는다면 왼쪽 그림처럼 각각의 하드웨어에 적합한 모든 컴파일링을 진행하여야 한다. 
* 중간 언어를 사용할 경우 오른쪽 그림처럼 다른 언어에 대한 컴파일러 B를 새롭게 개발하는 상황이 왔을 때 이 과정에서 사용한 중간 단계 언어 번역기를 그대로 사용할 수 있다는 장점이 있다.

출처: https://hdnua.tistory.com/5 [Romance]

위에서 보다시피 중간언어(IR) 개념의 등장과 함께 인터프리터는 확장성까지 갖추게 되었는데, 이는 브라우저같이 다양한 하드웨어 환경에서 동작하는 프로그램을 제어하기에는 더욱 안성맞춤인 언어가 된 계기가 되었다..

그럼 이제 위에서 언급했던 JIT Compiler에 대해 알아보자

JIT 컴파일러

JIT 컴파일러Just In Time Compiler의 줄임말이다.


출처 : https://aboullaite.me/understanding-jit-compiler-just-in-time-compiler/

JIT 컴파일러는 처음에 소스코드를 파싱하여 중간언어(IR)바이트 코드 형태로 먼저 변환한다. 이 후 인터프리터 모드라면 바이트 코드를 하나씩 읽어가며 동작을 수행하고, JIT 모드라면 생성된 바이트 코드를 기반으로 네이티브 코드컴파일 하여 수행하게 된다.

JIT 컴파일러는 컴파일러와 인터프리터의 장점을 합치고자 만들어진 개념이다.
중간 언어라는 이름 답게 중간 언어는 프로그래밍 언어보다 
더욱 컴퓨터에 가까운 언어이다. 이는 JIT 즉 실행과 동시에 즉각적인 컴파일을 가능하게 만든 
계기가 되었는데, 일반 프로그래밍 코드를 기계어로 컴파일하는 것보다 
컴파일 시간이 획기적으로 단축되었기 때문이다.

JITC vs Compiler vs Interpreter

그럼 JITC(JIT컴파일러)컴파일러인터프리터보다 무조건 좋을까?

결론부터 이야기하자면, 실행 성능으로는 컴파일러가 여전히 최고다.

위의 JITC, 인터프리터의 방식 보면 알 수 있듯이, 둘다 모두 동적인 환경에서 실행된다는 측면에서 컴파일 도중 최적화 할 시간이 정적 컴파일러에 비해 상대적으로 짧다. 그럼에도 네이티브 코드의 실행 속도는 바이트 코드에 비해 훨씬 빠르기 때문에 인터프리터보다는 JITC가 훨씬 빠르다.

(오른쪽으로 갈 수록 좋음)
성능 : 인터프리터 -> 동적 컴파일러(JITC) -> 정적 컴파일러
생산성 : 정적 컴파일러 -> 인터프리터 = 동적 컴파일러(JITC)

JITC in JS Engine

하지만 JavaScript Engine에서는 상황이 다르다.

첫번째 이유는 JavaScript가 꽤나 동적 타입의 언어이기 때문이다.

덕분에 JavaScript Engine에서의 JITC컴파일동적 타입에서의 모든 예외적인 케이스를 다 고려하여 코드를 생성해야 한다. 아래 예시를 보자.

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

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

이때 JITC는 int + int, int + string, string + string 등 다양한 변수 타입에 대한 모든 네이티브 코드를 생성해야한다.
이는 매우 불필요한 과정이므로 JITC는 보통 int + int일 경우를 제외하고는 네이티브 코드를 생성하기보다slow case로 코드를 넘겨버린다.

slow case는 위와 같이 네이티브 코드로 생성하기에 비효율적인 동작들을, 엔진 내부에 구현되있던 function을 호출해 동작을 수행하는 기관인데 쉽게말하면 바이트 코드를 네이티브코드로 컴파일링 하지 않고, 바로 인터프리팅 하는 기관이다. 인터프리터가 '실행기'였던 점을 까먹지 말자

이러면 생기는 문제점이, 결국 네이티브 코드컴파일하는 과정보다 그냥 slow case인터프리터가 동작하는 비중이 훨씬 높을 수 밖에 없고, 그러면 그냥 인터프리터랑 별반 차이가 없어진다. 여기에 추가로 JITC인터프리터와는 다르게, 네이티브 코드컴파일할 시 compilation overhead가 발생하기 때문에 JavaScript Engine에서는 꽤나 비효율적이다.

compilation overhead : 컴파일로 인한 과부화

두번째 이유는 초기 JavaScript의 한계성때문이다.

지금이야 옛날 이야기지만, 초창기 JavaScript는 단순히 웹 페이지 요소들의 layout을 건드리거나, 간단한 반응형 환경을 만들기 위한 언어였다. 이는JITC를 지원하는 타 언어 ex)JAVA 들 보다 비교적 간단한 프로그래밍을 염두해 두고 만든 언어라는 뜻이고, 실제로도 대부분의 JavaScript로 작성한 코드는 Java로 작성한 코드보다 덜 연산적이었다. 즉 JavaScriptJAVA보다 Hot spot이 적었다.

Hot spot은 '자주 반복되서 수행되는 구간' 즉 최적화가 필요성이 높은 부분을 뜻한다. 

Hot spot이 적다는건 네이티브 코드로의 최적화 컴파일이 과연 필요한가 라는 의문을 낳게된다.
어차피 최적화할 부분(Hot spot)이 적다면, 굳이 네이티브 코드로의 컴파일을 위한 compilation overhead를 감내할 필요가 있을까?

정리해서 말하자면 JavaScript JITCJavaScript동적 특성때문에, JavaScript사용 환경때문에 실행 성능이 인터프리터와 별반 차이가 없거나, 오히려 더 느릴 수 있다.

하지만 웹의 사용자가 늘어나며, 개발자들이 더 나은 웹 환경을 만들고자 노력함에 따라 HTML,JavaScript도 점점 진화해왔고,복잡한 연산이나 반복 연산을 포함하는 JavsScript코드도 점점 늘어갔다. 이는 결국 대다수의 JavaScript Engine들이 인터프리터 방식보다 JITC 방식을 사용한 이유이기도 하다.

하지만 결국 JavaScript JITC도 JavaScript환경에 맞게 개선되어야함에는 틀림없었다.

AJITC

현재 대부분의 브라우저 엔진은 AJITC방식을 사용한다.

구글의 개발자들은 Google Maps를 개발하며 당시 JavaScript Engine에 많은 한계를 느꼈다.
많은 유저 인터랙션이 필요했고, 이를 커버할 수 있는 좋은 엔진이 필요했다.

이는 기존 전통적인 `JITC`를 개선하려는 움직임으로 이어졌다.

그래서 구글 개발자들은 기존 JITC를 약간 비틀은 AJITC방식의 JavaScript Engine을 만들었는데, 그 이름도 유명한 V8이다.

v8

Chrome의 엔진명이 'V8'이라니 왜 구글 본사 이름은 발할라가 아니죠?

AJITC는 Adaptive Just In Time Compiler 즉 적응형 JIT컴파일러로, 실행하는 JavaScript 코드에 적응하는 똑똑한 JIT컴파일러다.

아래 대표적인 AJITCV8의 작동방식을 잠시 살펴보자.

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

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

보다시피 AJITC는 JITC와는 다르게 모든 바이트코드를 네이티브 코드로 컴파일 하지않는다. 다만 프로파일링을 통해 최적화 할 코드를 선별한 후 해당 코드들만 컴파일한다.
이를 통해 compilation overhead 를 최소화 하고, Hot spot도 해결한다.

Hot spot

아래 예시를 보며 Hot spot optimizing최적화 컴파일이 왜 필요한지 살펴보자.

function sum () {
	let result = 0
	for (let i = 1 ; i <= 10 ; i++){
		result += i;
	}
	return result;
};

sum() 
sum() 
sum() 
.
.
.
// compile 결과
sum() = //55
sum() = //55
sum() = //55
.
.
.  
// in interpreter
sum() = // for loop.. -> 55
sum() = // for loop.. -> 55
sum() = // for loop.. -> 55 
.
.
.  
// in JIT Compiler
sum() = // for loop.. -> 55 & compile to native code = compilation overhead
sum() = //55
sum() = //55

// in AJIT Compiler
sum() = // for loop.. -> 55 & profiling
sum() = // for loop.. -> 55 & profiling & 최적화 할 코드 선정
sum() = // for loop.. -> 55 // 코드 최적화(컴파일) & 최적화된 기계어 생성
sum() = //55  
sum() = //55 
sum() = //55 
//💡이해를 돕기 위한 단순 예시이지 저렇게 1버 반복하고 컴파일, 3번 반복하고 컴파일하며 실행되진 않는다. 
.
.
.

컴파일을 마친 기계어는 최적화를 통해 sum 함수의 결과를 55로 '기억'하고 있다. 반면 인터프리터바이트 코드는 매번 sum함수를 실행해 10번의 loop 를 거쳐, 55를 반환한다.
AJITC인터프리터바이트 코드프로파일되면서, 최적화 될 코드를 선정하고, 최적화 컴파일을 진행하여 최적화된 네이티브 코드를 생성한다.

이는 Hot spot이 많은 코드일수록 최적화 컴파일이 얼마나 중요한지 보여준다.

결론

🔥 컴파일러 / 인터프리터 / JITC는 모두 각각의 장단점이 있다.
🔥 Hotspot이 별로 없는 고전적인 JavaScript 프로그램들에는 인터프리터가 JITC보다 효율이 좋다.
🔥 최근 많이 사용되는, 복잡하고 연산이 많은 JavaScript 프로그램들에는 JITC가 좋다.
🔥 현대 대부분 브라우저는 AJITC 방식의 엔진으로 실행된다.

profile
const isInChallenge = true; const hasStrongWill = true; (() => { while (isInChallenge) { if(hasStrongWill) {return 'Success' } })();

1개의 댓글

comment-user-thumbnail
2023년 8월 19일

이해하기 쉽게 글 작성해주셔서 감사합니다. 도움이 많이 됐어요!

답글 달기