우리는 JIT Compiler
에 대해 설명하기 전에 미리 알아두어야 할 것이 있다. 바로 중간 언어(IR)
이다.
중간언어(IR)
는 고급 언어와 기계어 사이의 언어를 뜻하는데 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법이다. 하드웨어가 아닌 가상 머신이라는 소프트웨어에 의해 처리되기 때문에, 보통 기계어보다 더 추상적이다. 크게인터프리터
의 확장성을 위해 만들어 졌으며바이트 코드
를 사용하여 하드웨어의 의존성을 줄인다.* 중간 언어를 사용하지 않는다면 왼쪽 그림처럼 각각의 하드웨어에 적합한 모든 컴파일링을 진행하여야 한다. * 중간 언어를 사용할 경우 오른쪽 그림처럼 다른 언어에 대한 컴파일러 B를 새롭게 개발하는 상황이 왔을 때 이 과정에서 사용한 중간 단계 언어 번역기를 그대로 사용할 수 있다는 장점이 있다.
출처: https://hdnua.tistory.com/5 [Romance]
위에서 보다시피 중간언어(IR)
개념의 등장과 함께 인터프리터
는 확장성까지 갖추게 되었는데, 이는 브라우저같이 다양한 하드웨어 환경에서 동작하는 프로그램을 제어하기에는 더욱 안성맞춤인 언어가 된 계기가 되었다..
그럼 이제 위에서 언급했던 JIT Compiler에 대해 알아보자
JIT 컴파일러
는 Just In Time Compiler의 줄임말이다.
출처 : https://aboullaite.me/understanding-jit-compiler-just-in-time-compiler/
JIT 컴파일러는 처음에 소스코드를 파싱하여 중간언어(IR)
인 바이트 코드
형태로 먼저 변환한다. 이 후 인터프리터
모드라면 바이트 코드
를 하나씩 읽어가며 동작을 수행하고, JIT 모드
라면 생성된 바이트 코드
를 기반으로 네이티브 코드
로 컴파일
하여 수행하게 된다.
JIT 컴파일러는 컴파일러와 인터프리터의 장점을 합치고자 만들어진 개념이다.
중간 언어라는 이름 답게 중간 언어는 프로그래밍 언어보다
더욱 컴퓨터에 가까운 언어이다. 이는 JIT 즉 실행과 동시에 즉각적인 컴파일을 가능하게 만든
계기가 되었는데, 일반 프로그래밍 코드를 기계어로 컴파일하는 것보다
컴파일 시간이 획기적으로 단축되었기 때문이다.
그럼 JITC(JIT컴파일러)
가 컴파일러
와 인터프리터
보다 무조건 좋을까?
결론부터 이야기하자면, 실행 성능으로는
컴파일러
가 여전히 최고다.
위의 JITC
, 인터프리터
의 방식 보면 알 수 있듯이, 둘다 모두 동적인 환경에서 실행된다는 측면에서 컴파일
도중 최적화 할 시간이 정적 컴파일러
에 비해 상대적으로 짧다. 그럼에도 네이티브 코드
의 실행 속도는 바이트 코드
에 비해 훨씬 빠르기 때문에 인터프리터
보다는 JITC
가 훨씬 빠르다.
(오른쪽으로 갈 수록 좋음)
성능 : 인터프리터 -> 동적 컴파일러(JITC) -> 정적 컴파일러
생산성 : 정적 컴파일러 -> 인터프리터 = 동적 컴파일러(JITC)
하지만 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
로 작성한 코드보다 덜 연산적이었다. 즉 JavaScript
는 JAVA
보다 Hot spot
이 적었다.
Hot spot은 '자주 반복되서 수행되는 구간' 즉 최적화가 필요성이 높은 부분을 뜻한다.
Hot spot
이 적다는건 네이티브 코드
로의 최적화 컴파일
이 과연 필요한가 라는 의문을 낳게된다.
어차피 최적화할 부분(Hot spot
)이 적다면, 굳이 네이티브 코드
로의 컴파일
을 위한 compilation overhead
를 감내할 필요가 있을까?
정리해서 말하자면 JavaScript JITC
는 JavaScript
의 동적 특성
때문에, JavaScript
의 사용 환경
때문에 실행 성능이 인터프리터
와 별반 차이가 없거나, 오히려 더 느릴 수 있다.
하지만 웹의 사용자가 늘어나며, 개발자들이 더 나은 웹 환경을 만들고자 노력함에 따라
HTML
,JavaScript
도 점점 진화해왔고,복잡한 연산이나 반복 연산을 포함하는JavsScript
코드도 점점 늘어갔다. 이는 결국 대다수의 JavaScript Engine들이 인터프리터 방식보다 JITC 방식을 사용한 이유이기도 하다.하지만 결국 JavaScript JITC도 JavaScript환경에 맞게 개선되어야함에는 틀림없었다.
현재 대부분의 브라우저 엔진은
AJITC
방식을 사용한다.
구글의 개발자들은 Google Maps를 개발하며 당시 JavaScript Engine
에 많은 한계를 느꼈다.
많은 유저 인터랙션이 필요했고, 이를 커버할 수 있는 좋은 엔진이 필요했다.
이는 기존 전통적인 `JITC`를 개선하려는 움직임으로 이어졌다.
그래서 구글 개발자들은 기존 JITC
를 약간 비틀은 AJITC
방식의 JavaScript Engine
을 만들었는데, 그 이름도 유명한 V8이다.
Chrome의 엔진명이 'V8'이라니 왜 구글 본사 이름은 발할라가 아니죠?
AJITC
는 Adaptive Just In Time Compiler 즉 적응형 JIT컴파일러
로, 실행하는 JavaScript
코드에 적응하는 똑똑한 JIT컴파일러
다.
아래 대표적인 AJITC
인 V8의 작동방식을 잠시 살펴보자.
V8 Engine
은 JS 소스코드를 받으면Parser
에 보낸다.Parser
는 소스코드를 파싱 한 후AST
를 만들어Ignition
에 보낸다.💡var 호이스팅은 참고로 이단계에서 이루어진다.
인터프리터
인Ignition
에서는AST
를바이트 코드
로 '중간 번역' 하고 실행시킨다.V8 Engine
은 런타임 과정 중 지속적인프로파일링
을 통해Hot spot
즉 반복되어 사용하는 코드 등 과열지점을 찾는다.- 과열 코드를
TurboFan
으로 보내 최적화 컴파일을 진행한다.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 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 방식의 엔진으로 실행된다.
이해하기 쉽게 글 작성해주셔서 감사합니다. 도움이 많이 됐어요!