들어가며
자바스크립트는 컴파일 언어인가요, 인터프리터 언어인가요? 라는 내용을 검색하면 종종 보이는 단어가 있다.
바로 JITC (Just-In-Time Compiler) 라는 단어인데, 같이 나오는 내용인 즉 JITC는 인터프리팅을 하다가, 필요한 부분에 대해서는 컴파일을 하여 최적화를 한다 이다. 이 말은 내용은 맞지만 이름이 틀렸다.
위의 방식은 Adaptive JITC 라고 하여, 최근 대다수의 JavaScript 엔진들이 차용하는 방식이다. 그리고 JITC 는 과거의 JavaScript 엔진들이 차용하던 방식이다.
그렇다면 JITC와 Adaptive JITC는 무엇이고, 이들의 차이점은 무엇인지 알아보자.
JITC
JITC는, 위에서 언급했듯이 인터프리터 언어와 달리 bytecode를 native code로 컴파일을 한다. 즉 최적화를 한다는 것이다. 과거의 JavaScript 엔진들은 이러한 방식을 사용하였다.
단 이때 같이 알아두어야 할 것은, 모든 코드를 native code 로 컴파일을 한다는 것 이다. 이 말인즉, 최적화가 필요 없는 코드에게도 최적화를 한다 는 것이다. 따라서 이는 최적화를 했음에도, 성능의 향상을 가져오지 못하거나, 오히려 떨어뜨릴 수 있다는 것이다.
특히 웹페이지를 보여주는데 주로 사용되었던 JavaScript는 여타 언어들에 비하여 자주 반복되는 구간이 매우 적으며, 실제로 JITC가 큰 성능 개선을 가져오지 못한다는 연구가 있었다.
이에 등장한 것이 Adaptive JITC 이다.
Adaptive JITC
Adaptive JITC 방식을 한 문장으로 간결하게 표현하자면, 위에 언급한 인터프리팅을 하다가, 필요한 부분에 대해서는 컴파일을 하여 최적화를 한다 와도 같다. 그리고 이 필요한 부분 이라는 것은, 모니터(혹은 프로파일러)가 반복이 얼마나 되는지를 체크하며 판단한다.
필요한 부분 1: Warm code, Baseline-JITC
반복이 일어난다면 JITC는 Baseline-JITC 에게 컴파일을 요청한다. 그리고 컴파일 된 정보는 JITC가 저장한다.
필요한 부분 2: Hot code, Optimizing-JITC
더 많은 반복이 일어난다면 JITC는 Optimizing-JITC 에게 컴파일을 요청한다. 마찬가지로 컴파일 된 정보는 JITC가 저장한다.
이 때 주의해야 할 것은, Optimizing-JITC 는 profiling을 수행하는 동안 특정 변수의 타입이 변하지 않았다면 그 이후에도 그 변수는 타입이 변하지 않을 가능성이 매우 높을 것이다 라는 가정을 하고 최적화를 한다. 그러나 이런 가정이 틀렸다는 것을 알게 될 경우, 즉 타입이 바뀌었을 경우에는 JIT는 가정이 잘못되었다고 판단하고 최적화된 코드를 버린다. 그러면 다시 인터프리터 혹은 기본 컴파일된 버전으로 돌아간다. 이 과정은 역최적화(deoptimization) 혹은 구제(bailing out) 라고 한다.
당연하게도 최적화와 역최적화를 번갈아서 하는 데에는 꽤나 시간이 소요될 것이다. 따라서 이 과정은 성능의 악화를 가져올 수 있다.
대부분의 브라우저들은 이러한 최적화/역최적화 싸이클이 발생했을 때 중지할 수 있는 제한을 갖고 있다. 예를들어 만약 JIT가 10번의 이상의 최적화를 시도하고 계속해서 그 코드를 버린다면, 더이상 최적화를 그만 시도하게 될 것이다.
마치며
이러한 내용들을 공부하면서 이전에 갖고 있던 궁금증 하나가 해결됐다.
왜 예전의 JavaScript 엔진들은 컴파일을 했다고 하는거지?
요즘 사용하는 방식인 JITC를 사용했다고 하는데, 그건 인터프리팅을 하잖아! 헷갈리네..
먼저, 요즘 사용하는 것은 Adaptive-JITC 이고, 과거에 사용하던 것은 JITC였다.
다음으로, 바로 위에서 말했듯이 JITC는 bytecode를 바로 실행하는 인터프리터 방식과 달리, native code로 최적화를 한 후에 실행하였기 때문이다.
따라서 과거의 JavaScript 엔진들이 컴파일을 했다는 말이 그 것이다.
뿐만 아니라, 실행 컨텍스트에 대해서 공부하며 그동안 고민했던 부분 또한 해결이 되었다.
동적으로 실행 컨텍스트가 생성될 때마다 컴파일을 하고, 그렇게 만들어 놓은 것을 바탕으로 런을 하는건가?
그렇다면 JavaScript의 엔진은 컴파일 타임과 런타임이 바톤 터치하듯 일어나는걸까?
(이 질문에 대한 직접적인 대답을 찾지는 못했지만) 위의 내용들로 보건대, 그렇다고 생각한다.
왜냐하면, 요즘의 JavaScript 엔진들이 차용한 Adaptive JITC는 인터프리팅과 컴파일을 번갈아서 하기 때문이다. 따라서 컴파일 타임에 최적화 뿐만 아니라 실행 컨텍스트를 만드는 것이다.
추가적으로, const
를 사용하면, 개발자가 얻을 수 있는 이점이 사이드 이펙트를 막을 수 있다 뿐만 아니라, 성능 또한 개선할 수 있다 라는 것을 알게 되었다.
왜냐하면, 위에서 언급했듯이 optimizing-JITC 의 최적화는 profiling을 수행하는 동안 특정 변수의 타입이 변하지 않았다면 그 이후에도 그 변수는 타입이 변하지 않을 가능성이 매우 높을 것이다 라는 가정을 바탕에 두었기 때문이다.
그러나 이런 가정이 틀렸다는 것을 알게 될 경우, 즉 타입이 바뀌었을 경우에는 JIT는 가정이 잘못되었다고 판단하고 최적화된 코드를 버린다. 그러면 다시 인터프리터 혹은 기본 컴파일된 버전으로 돌아간다. 이 과정은 역최적화(deoptimization) 혹은 구제(bailing out) 라고 한다.
따라서 재 할당이 되지 않는 const
는 가진 값(또는 타입)이 바뀔 일이 전혀 없기 때문에, hot code 에서 애써 일궈놓은 최적화된 코드를 다운그레이드 하지 않을 것이기 때문이다.
최적화는 함수 단위로 일어날까, 아니면 반복문 등 그보다 작은 단위에서 일어날까?
이에 짧은 실험을 해보았고, 최적화는 보다 작은 단위에서 일어난다 라는 결론을 내렸다.
함수 단위로 1000번 반복을 하는 코드(1)와, 반복문 만으로 1000번 반복을 하는 코드(2)를 만들고, 각각 이들의 실행 시간을 측정했다. 만약 (1)의 시간이 (2)의 시간보다 훨씬 짧다면 함수 단위로 최적화 가 일어나는 것이고, 그렇지 않다면 보다 작은 범위에서 최적화 가 일어난다고 생각했다.
// 1. 함수 자체를 반복시킨다
var startTime = new Date().getTime();
function loopFunction() {
console.log(". ");
}
var startTime = new Date().getTime();
for (let i=0; i<10000; i++) {
loopFunction();
}
var endTime = new Date().getTime();
console.log(endTime - startTime);
// 2. 반복문만으로 반복시킨다
var startTime = new Date().getTime();
for (let i=0; i<10000; i++) {
console.log(". ");
}
var endTime = new Date().getTime();
console.log(endTime - startTime);
(1) | (2) |
---|---|
921 | 778 |
880 | 843 |
1040 | 1047 |
이 표는 둘의 실행 시간을 비교한 것으로, 큰 차이가 없는 것을 알 수 있었다. 따라서 최적화는 함수 보다 작은 단위에서 일어난다 라는 결론을 내렸다.
'자바스크립트 JavaScript' 카테고리의 다른 글
Closure (0) | 2023.01.18 |
---|---|
Scope (0) | 2023.01.18 |
실행 컨텍스트 (Execution Context) (2) | 2023.01.17 |
JavaScript 의 역사 (0) | 2023.01.16 |
변수의 타입과 Scope, Hoisting, 함수 (0) | 2023.01.16 |