11.1 자바스크립트 엔진이란
정의
JS 코드를 실행하는 프로그램 또는 개발자가 별도의 컴파일 작업을 수행하지 않는 인터프리터 언어(Interpreter Language)를 말합니다.
조금 더 자세하게 말해 자바스크립트 코드를 마이크로프로세서가 이해할 수 있는 더 낮은 수준의 언어(기계어)로 변환해주는 역할을 담당합니다.
종류
- SpiderMonkey: 최초의 자바스크립트 엔진으로 JS의 창시자인 브랜던 아이크가 넷스케이프 브라우저를 위해 개발했습니다. 현재 FireFox에서 사용합니다.
- Chakra: Microsoft가 개발한 엔진이며, Edge 브라우저에 사용되었습니다.
- JavaScript Core(JSC): 애플에서 개발했고 처음에는 WebKit 프레임워크를 위해 개발되었습니다. 최근에는 Safari와 React Native App에서 사용합니다.
- V8: 구글이 개발한 오픈소스입니다. Google Chrome, Electron, Node.js에서 사용합니다.
V8
V8은 독일의 Google Development Center에서 만들어진 자바스크립트 엔진입니다.
2008년 V8의 출시는 상대적으로 느린 브라우저 인터프리팅을 대체할 수 있었기 때문에 자바스크립트 엔진 역사에서 중요한 사건 중 하나였습니다.
크롬 V8 엔진의 특성은 다음과 같습니다.
- C++ 로 작성된 오픈소스
- 클라이언트(Chrome)과 서버 측(Node.js) 애플리케이션 모두 사용
- ECMA-262에 기재된 ECMAScript를 구현
- standalone 으로 동작할 수 있어 자바스크립트 엔진을 C++ 프로그램으로 내장 가능
서버 사이드 프로그래밍과 네트워킹 애플리케이션을 다룰 수 있게 해줌
11.2 자바스크립트 엔진 파이프라인
위에서 언급한 엔진들은 비교적 처리 속도가 느린 인터프리터의 단점을 해결하고자 인터프리터와 컴파일러의 장점을 결합했습니다.
인터프리터는 소스코드를 즉시 실행하고 컴파일러는 빠르게 동작하는 기계어를 만들고 최적화합니다.
이를 통해 컴파일 단계에서 추가적인 시간이 필요함에도 보다 빠른 코드 실행이 가능합니다.
아래 그림은 자바스크립트 엔진이 소스 코드를 기계어로 만드는 것까지 공통적으로 수행하는 과정입니다.
자바스크립트 엔진은 JS 소스 코드를 파싱해서 Abstract Syntax Tree(AST)로 만듭니다.
만들어진 AST를 바탕으로 인터프리터는 바이트 코드를 생성하는 과정까지가 실제 엔진이 자바스크립트로 작성된 코드를 실행하는 부분입니다.
더 빠른 코드 실행을 위해 바이트 코드는 프로파일링 된 데이터와 함께 최적화 컴파일러(Optimizing Compiler)로 보내집니다.
이곳에서 프로파일링 데이터를 기반으로 최적화된 기계어를 생성하고, 만약 정확하지 않은 결과가 나오면 Deoptimize하여 바이트 코드로 다시 되돌립니다.
11.3 인터프리터 / 컴파일러 파이프라인
일반적으로는 다음과 같은 공통 파이프라인을 가집니다.
- 인터프리터(Interpreter): 최적화되지 않은 바이트 코드(Bytecode)를 빠르게 생성합니다.
- 최적화 컴파일러(Optimizing Compiler): 최적화된 기계어 코드를 생성합니다.
- 바이트 코드(Bytecode): 중간 언어(IR, Intermediate Representation)
(Interpreter 모드 시 바이트 코드를 하나씩 읽어서 실행하고 JIT 모드 시 바이트 코드를 기반으로 컴파일)
11.4 그럼 V8은?
구조
V8 내부를 살펴보면 앞서 본 파이프라인 형태를 거의 그대로 가져는 모습을 보입니다.
네이밍이 마치 자동차 엔진과 유사합니다.
- Ignition: 바이트 코드를 생성 및 실행
- TurboFan: 뜨거워진(Getting Hot) 바이트 코드와 프로파일링 데이터를 받아 식히는 역할
뜨거워진다(Getting hot)는 의미?
자주 반복돼서 수행된다는 뜻입니다.
모던 자바스크립트 엔진들은 최적화 기법으로 한번에 최적화를 적용하는 JITC(Just-In-Time Compiler) 방식에서 Adaptive Compilation 방식을 채택하고 있습니다.
요약해서 말하면, 반복 수행되는 정도에 따라 서로 다른 최적화를 적용하는 것입니다.
초기 모든 코드는 인터프리터에 의해 바이트 코드로 변환되지만, 자주 반복되는 부분이 발견되면 여기에 대해서만 JITC를 적용하는 식입니다.
Hidden Class
자바스크립트는 프로토타입 기반 언어입니다. 또한 동적으로 type이 지정되기 때문에 명시적이지 않고, property는 직접 객체에 추가나 삭제할 수 있습니다.
V8은 런타임 시에 Hidden Class 를 생성해서 type 시스템의 내부 표현을 갖고, property 접근 시간을 향상시키고 있습니다.
V8은 property 가 같은 객체를 그룹화할 수 있습니다. 다시 말하면 p와 q는 최적화된 코드를 같이 사용할 수 있다는 뜻입니다.
그렇다면 선언 이후 q 객체에 property 를 추가하려고 하는 상황에는 어떻게 대응할까요?
새로운 Hidden Class가 생성될 때마다 이전의 Hidden Class는 업데이트됩니다.
11.5 V8의 최적화 기법
두 개의 컴파일러
V8은 두 개의 컴파일러가 존재합니다.
- Full Compiler: 코드를 신속하게 생성하기 위해 type 분석을 하지 않으며 type에 대해 알지 못합니다.
- Optimizing Compiler: 나중에 제공되며 hot function 을 다시 컴파일합니다. 인라인 캐시에서 type을 가져와 좀 더 나은 최적화 방법을 결정합니다.
코드 최적화
V8은 각 property 에 대해 새로운 Hidden Class 를 만들기 때문에 Hidden Class 생성을 최소한으로 유지해야 합니다.
다른 Hidden Class 트리를 만들지 않기 위해 인스턴스를 만든 후 property를 추가하지 않고 같은 순서로 인스턴스 멤버를 항상 초기화해야 합니다.
최적화 해제
V8은 최적화 해제를 지원합니다.
예를 들어, 생성된 Hidden Class 가 예상한 클래스가 아닌 경우 V8은 최적화된 코드를 버리고 Full Compiler로 돌아와 인라인 캐시에서 다시 type을 가져옵니다.
Hidden Class 및 Inline Caching 등의 기법 관련하여 더 자세하게 알고 싶다면 다음 페이지 링크로 확인하시면 됩니다.
인라인 캐싱
예를 들어, 한 객체 형태의 내부 이름이 p1 이라 하고, firstname과 lastname 이라는 두 개의 property 만을 가진다고 해봅시다.
컴파일러가 인라인 캐싱을 적용할 때 함수는 객체 형태 p1을 전달하고 lastname의 값을 즉시 반환한다고 가정합니다.
예제
(() => {
const a = {firstname: "Duzon", lastname: "Kim"};
const b = {firstname: "Dou zone", lastname: "Lee"};
const c = {firstname: "Chan ho", lastname: "Park"};
const d = {firstname: "sung teak", lastname: "Oh"};
const e = {firstname: "a in", lastname: "You"};
const people = [a, b, c, d, e, d, c, b, a];
const getName = (person) => person.lastname;
console.time('engine');
for (let i = 0; i < 1000000000; i++) {
getName(people[i & 7]);
}
console.timeEnd('engine');
})();
Monomorphic Inline Caching
Polymorphic Inline Caching
최적화된 기계 코드는 4개의 모든 위치를 인식하고 있습니다.
하지만 전달된 인수가 속한 4가지 가능한 객체 형태 중 어느 것인지 확인하는 작업이 필요하고 이로 인해 성능이 저하됩니다.
4라는 임계치를 초과하게 되면, 소위 Megamorphic Inline Caching 에 존재합니다.
이 상태에서는 더 이상 메모리 위치의 로컬 캐싱이 없기 때문에 글로벌 캐시에서 조회해야 합니다.
11.6 정리하며
결론
- 모던 자바스크립트 엔진은 인터프리터와 컴파일러의 장점을 취합해 빠른 애플리케이션 시작과 코드 실행을 진행합니다.
- 인라인 캐싱은 강력한 최적화 기술입니다. 단일 오브젝트 형태로 최적화된 상태로 넘어갈 때 가장 효과적입니다.
- TypeScript 와 같은 정적 타입화된 트랜스파일러는 Monomorphic Inline Caching의 가능성을 높입니다.