JavaScript는 인터프리터 언어로 알려져있습니다. 최근 대부분의 모던 자바스크립트 엔진(크롬의 V8, 마이크로 소프트 엣지의 ChaKra 등)은 인터프리터와 컴파일러의 장점을 결합해 비교적 처리 속도가 느린 인터프리터의 단점을 해결하였습니다. 이러한 방식을 Adaptive JIT Compilation 이라고 하는데, 이러한 엔진을 사용하게 된 흐름을 이해하려면 컴파일러, 인터프리터 , JITC 에 대해 알 필요가 있습니다.
컴파일러
특징
- 코드가 실행되기 전 단계인 컴파일 타임에 소스코드 전체를 한번에 머신 코드(CPU가 바로 실행할 수 있는 기계어)로 변환한 후 실행합니다.
- 실행 파일을 생성합니다.
- 컴파일 단계와 실행 단계가 분리되어 있습니다. 명시적인 컴파일 단계를 거치고, 명시적으로 실행 파일을 실행합니다.
- 실행에 앞서 컴파일은 단 한번 수행됩니다.
- 컴파일과 실행 단계가 분리되어 코드 실행 속도가 빠릅니다.
장점
- 실행 파일이 생성되고 나면 실행 속도가 JIT, 인터프리터 보다 빠르다.
- 프로그램 실행 전에 오류를 발견할 수 있다.
단점
- 오브젝트 코드를 묶어 하나의 실행 파일로 만드는 링킹작업시 인터프리터 보다 많은 메모리를 사용한다.
인터프리터
우선 인터프리터가 동작하기 전에 어떠한 과정이 이루어지는지 알아보겠습니다.
JavaScript 소스코드가 토큰으로 변환되고 파싱의 과정을 거쳐 AST(추상적 구문 트리)가 생성됩니다. 그리고 AST를 기반으로 인터프리터가 실행할 수 있는 중간언어(IR, Intermediate Representation)인 바이트 코드 형태가 생성됩니다.
이제 바이트 코드가 생성된 단계에서 인터프리터 모드라면 바이트코드를 하나씩 읽어가며 코드가 실행됩니다.
* 후에 나올 JITC과 비교되는 부분이 해당 부분입니다. JITC 은 이 단계에서 바이트 코드를 기반으로 native code로 컴파일을 수행합니다.
특징
- 코드가 실행되는 단계인 런타임에 바이트코드를 한줄씩 읽어가며 코드를 실행합니다.
- 실행 파일을 생성하지 않습니다.
- 인터프리트단계와 실행단계가 분리되어 있지 않습니다.
- 인터프리트 단계와 실행 단계가 분리되어 있지 않고 반복 수행되므로 코드 실행 속도가 비교적 느립니다.
장점
- 별도의 링킹 과정이 없어 메모리 효율이 좋습니다.
- 코드 변경시 실행 파일을 만들지 않고 바로 실행이 가능합니다.
단점
- 런타임이 되어야 오류를 확인할 수 있습니다.
Just-in-Time Compilation (JITC)
특징
- 바이트 코드가 생성된 후 최적화 알고리즘을 적용하여 native code 를 생성합니다.
- native code 가 컴파일 되는 과정에서 overhead 가 발생합니다.
- overhead가 포함되더라도 native code 의 성능이 더 낫기 때문에 interpreter 보다 빠르게 수행됩니다.
* overhead : 특정 기능을 수행하는데 걸리는 비용(시간, 메모리)
그런데 이는 Java VM 을 기준으로 할 때입니다.
JITC 라는 녀석 JavaScript 에서는 어떨까?
JavaScript 동적 타입 언어 입니다. 즉 타입이 런타임에서 결정이 납니다.
이러한 특성으로 인해 자바와 같은 정적 타입 언어에서는 발생하지 않는 일들이 생겨나곤 합니다.
"99" + 1; // "991"
"99" - 1; // 98 ?
"2" * "3" + "4"; // "64"
"2" * "3" + "4" * "5"; // 26
// https://samslow.github.io/development/2020/07/06/JIT/
단순히 Number 타입을 더하는 것이 아닌, 서로 다른 타입 간의 연산 발생하는 예외 케이스가 존재하게 됩니다.
이러한 이유로 정적 타입 언어였다면 int + int 하나의 케이스만 존재했을 것이 int + int , int + string , string + string 등 과 같이 엄청나게 많은 native code 가 필요하게 됩니다.
엄청나게 많은 native code 의 요구는 helper function 을 호출하게 되고, 이 helper function 은 인터프리터를 사용할 때와 똑같은 성능을 가지게 된다는 것입니다.
* helper function 은 모르겠고, 동적 타입 언어인 자바스크립트가 초래하는 엄청난 양의 native code 가 결국에는 인터프리터와 같은 성능을 만들게 된다. 정도로 정리하자.
그렇게 되면 바이트 코드를 native code 로 컴파일 할때 발생하는 overhead 를 감내하고 native code 성능이 더 좋으니까 JITC 을 사용하는 것인데, 이게 인터프리터와 같은 성능을 가지게 된다?
그러면 인터프리터 모드를 사용했으면 발생하지 않았을 overhead 만큼 손해를 보는 것입니다.
또 다른 문제는 JavaScript 는 클라이언트 언어인 반면에 Java 는 비즈니스 로직이 포함된 백엔드 언어입니다. 이러한 특징은 Java 로 작성된 프로그램이 대체로 연산량이 많을 것을 의미하고 JavaScript 에서는 비즈니스 로직 보다는 사용자에 반응하는 프로그램이 많아 자주 반복돼서 수행되는 구간(HotSpot) 비교적 백엔드 프로그램보다 적습니다.
이게 왜 문제가 되냐하면 바이트 코드를 native code로 overhead 만큼 손해보는 시간만큼 native code 를 많이 사용해서 뽕을 뽑아야하는데 그럴 수 있는 HotSpot이 적다는 겁니다.
그래서 이런 반복되는 연산이 적은 프로그램에서는 JITC 보다는 인터프리터를 사용하는 것이 더 좋습니다.
* 하지만 최근 동향을 봤을 때, '프론트 <- 백엔드 로직 , 백엔드 <- devOps 의 코드들이 점차 넘어오고 있다.' 라는 말을 듣기도 하였고, 프론트 코드에서도 훅으로 비즈니스 로직을 다루는 경우가 많아지고 있어 진리의 케바케 인 것 같습니다.
그래서 이러한 단점을 보완하고 최근 자바스크립트 엔진은 Adaptive Compilation 방식을 택하고 있습니다.
Adaptive JIT Compilation
Adaptive Compilation 이란, 모든 코드를 일괄적으로 같은 수준으로 최적화를 적용하는 것이 아니라, 반복되는 정도에 따라 유동적으로(Adaptive) 하게 최적화 하는 방식입니다.
기본적으로 모든 코드는 인터프리터로 수행을 합니다. 그러다 자주 반복되는 부분(HotSpot)을 발견하면 해당 부분에 대해서만 JITC 을 적용하여 native code 로 컴파일을 수행하게 됩니다.
* 여기서 최소한의 최적화 baseline-JITC 과 더 많이 반복되는 경우에 사용되는 JITC(Optimizing-JITC) 이 존재하는데, 이 정도로 똑똑하게 최적화 하구나~ 정도로 이해했습니다.
번외: 타입스크립트
저는 타입스크립트를 사용하면서 느낀 좋은점은 다음과 같았습니다.
- 컴파일 타임에 에러체킹이 된다.
- API 응답으로 어떤 데이터가 오는지 까먹을때 타이핑을 보면 바로 알 수 있다.
- 자동완성 기능으로 인한 생산성 증가
바로 딱 떠오르는게 위 내용인데, 오늘로 하나가 추가되었습니다. 타입스크립트를 사용하여 정적 타입을 사용하게 된다면 JITC 을 사용할때 필요한 native code 가 적어지고 따라서 JITC 을 사용할때 효율이 더 좋아지지 않을까? 라고 말이죠.
참고자료
1. https://samslow.github.io/development/2020/07/06/JIT/
2. https://meetup.nhncloud.com/posts/77
3. https://coding-nyan.tistory.com/85
4. https://jhyonhyon.tistory.com/18
5. 모던 자바스크립트 딥 다이브
'FrontEnd > JS' 카테고리의 다른 글
this (0) | 2023.05.30 |
---|---|
프로미스 (5) | 2023.05.30 |
태스크 큐와 마이크로 태스크 큐 (0) | 2023.05.29 |
실행 컨텍스트 (0) | 2023.05.25 |
자바스크립트 이벤트 루프 (2) | 2022.09.23 |