티스토리 뷰
자바스크립트 엔진
- 컴퓨터가 JS파일을 이해하고 명령을 수행할 수 있도록 한다.
- JS파일을 컴퓨터가 읽을 수 있는 코드로 변환하여 전달하여, 컴퓨터는 이를 실행한다.
- V8은 JS엔진의 종류 중 하나이며, 다양한 브라우저(크롬, 사파리, 엣지 등), 안드로이드 브라우저, NodsJS 런타임등 사용된다.
Compile과 Interpret
- 프로그래밍 언어를 컴퓨터가 읽을 수 있도록 번역을 하는 두가지 방식이 있다.
- Compilation
- 컴파일러가 코드를 기계어로 번역한다.
- 컴파일러는 코드 전체를 읽은 뒤 이를 바로 실행 가능한 기계어로 번역하여 실행한다.
- 함수를 100번 실행 해야된다면 함수의 동작과정을 100번 실행하는 것이 아닌 함수의 결과를 100번 읽는다.
- Interpretation
- 인터프리터가 코드를 기계어로 번역한다.
- 코드 한줄 한줄 읽어서 기계어로 번역한다.
- 함수를 100번 실행해야 된다면 매번 함수를 한줄한줄 읽는 과정을 100번 실행한다.
- JS는 인터프리터가 코드를 한줄 한줄 읽는 방식으로 프로그램을 실행한다.
- 인터프리터 특성상 코드가 많아질수록 속도가 느려진다는 단점이 있다.
- V8엔진은 이러한 JS의 수행 속도의 개선을 위해 설계되었다.
V8 엔진
- V8엔진은 인터프리터를 사용하지 않고 JIT컴파일방식을 이용한다.
JIT (Just In Time)
- 프로그램 실행 시 코드의 일부 또는 전체를 런타임에 컴파일하는 기술이다.
- JIT 컴파일러는 인터프리터와 정적 컴파일러의 장점을 결합한 방식이다.
- 특징
- 런타임 컴파일
- JIT 컴파일러는 프로그램이 실행되는 도중 코드를 컴파일 한다.
- 프로그램이 실행되기 전에 코드 전체를 컴파일하는 정적 컴파일과는 다른 방식이다.
- 성능 최적화
- 프로그램 실행 중 수집된 런타임 정보를 활용하여 최적화된 기계어 코드를 생성할 수 있다.
- 프로그램의 실행 성능을 향상시킬 수 있다.
- 런타임 컴파일
- 동작
- 코드를 토큰화 하여 AST(Abstract Syntax Tree, 추상 구문 트리)를 생성
- 인터프리터를 사용하여 AST 기반 코드를 실행
- 인터프리터는 코드 실행 과정 프로파일링 정보 수집
- 프로파일링 정보를 기반으로 JIT 컴파일러가 IR(Intermediate Representation, 중간 표현)을 생성
- 터보팬에서는 IR를 분석하여 최적화 시도
- 인라인 캐싱, 히든 클래스 등 최적화
- 최적화된 IR코드가 기계어로 컴파일 됨
- 기계어를 실행함
히든클래스
- JS는 동적 언어이며 객체가 생성된 이후에도 속성을 쉽게 추가/삭제할 수 있다.
- 정적 언어는 메모리 offset(위치)를 컴파일 시 결정하며, 프로퍼티를 선언할 때 offset을 어딘가 저장해 둔 뒤 각 프로퍼티의 값이 필요할 때 offset의 값을 그대로 사용한다.
- 동적언어는 특성상 객체에 접근할때 마다 조회(lookup, 해당 변수나 속성의 위치를 찾는 과정)과정을 거치므로 상대적으로 실행속도가 느려진다.
- 히든 클래스는 이를 개선하기 위해 고안된 방법이다.
- V8엔진은 히든 클래스를 활용하여 프로퍼티 구조를 효율적으로 처리한다.
- 새로운 객체를 생성할 때 이에 대한 새로운 히든 클래스를 생성한다.
- 그런 다음 새 프로퍼티를 추가해 동일한 객체를 수정하면 이전 클래스의 모든 프로퍼티가 포함된 새 히든클래스를 만들고 새 프로퍼티를 포함한다.
function Vector(x, y) {
this.x = x; // B
this.y = y; // C
}
const v = new Vector(1, 2); // A
A. V8엔진은 hc1이라는 히든 클래스를 생성하고 v 객체는 hc1을 참조한다.
- propertyTable: 프로퍼티가 메모리의 어디에 있는지 찾기 쉽게 알려줌
- transitionTable: 구조가 변하면 어디로 가야하는지 알려줌
v: {}
// hidden class
hc1: {
propertyTable: {},
transitionTable: {},
}
B. V8엔진은 hc1을 참조하여 hc2라는 히든 클래스를 생성한다.
v: { x: 1 }
// hidden class
hc1: {
propertyTable: {},
transitionTable: {
property: "x",
},
},
hc2: {
propertyTable: {
property: "x",
offset: 1
},
transitionTable: {},
}
C. hc2를 참조하여 hc3라는 히든 클래스를 생성한다.
v: { x: 1, y: 2 }
// hidden class
hc1: {
propertyTable: {},
transitionTable: {
property: "x"
},
},
hc2: {
propertyTable: {
property: "y"
},
transitionTable: {
property: "x",
offset: 1
},
},
h3: {
propertyTable: {
property: "y",
offset: 2
},
transitionTable: {
property: "x",
offset: 1
}
}
- 위의 과정을 통해 히든 클래스가 쌓인다.
- 코드 작성시 히든 클래스가 생성될 때마다 기존에 생성된 히든 클래스를 사용하도록 고려해야 한다.
- 아래의 코드는 히든 클래스를 고려했을 때 좋지 않은 방식이다.
function Vector(x) {
this.x = x;
}
const v1 = new Vector(1);
const v2 = new Vector(1);
v1.y = 2;
v1.z = 3;
v2.z = 3;
v2.y = 2;
- v1, v2는 각각 동일한 데이터를 가지고 있지만 서로 다른 히든 클래스를 참조하게 된다.
- 같은 offset을 참조하지 못하므로 캐싱이 되지 않고, 여러개의 히든 클래스를 만들어 각각 참조하게 된다.
인라인 캐싱
- 반복문 내 객체 접근 시 조회 작업을 생략함으로써 성능을 향상시킨다.
- 객체의 요소에 접근하는 부분에 실제 메모리 주소를 할당하여 조회 과정을 생략한다.
- 즉 객체의 구조가 동일해야 객체를 조회하는 과정을 생략하여 최적화 한다.
- 예시
- 아래의 코드에서 객체들의 구조가 각각 다르다.
- 이때 반복문을 통해 객체에 100억번 접근했을때 약 16초가 걸렸다.
(() => {
const minji = {
firstName: 'Minji',
lastName: 'Kim',
job: 'singer',
};
const hanni = {
firstName: 'Hanni',
lastName: 'Pham',
group: 'New jeans',
};
const daniel = {
firstName: 'Daniel',
lastName: 'Marsh',
nationality: 'Australia',
};
const haerin = {
firstName: 'Haerin',
lastName: 'Kang',
nickName: 'cat',
};
const hyein = {
firstName: 'Hyein',
lastName: 'Lee',
mbti: 'ISFP',
};
const getFullName = (people) => `${people.firstName} ${people.lastName}`;
const people = [minji, hanni, daniel, haerin, hyein];
const t0 = performance.now();
for (let i = 0; i < 1000 * 1000 * 1000; i++) {
getFullName(people[i % 5]);
}
const t1 = performance.now();
console.log(`${Math.floor((t1 - t0) / 1000)}s`);
})();
위와 다르게 객체의 구조를 동일하게 구성하여 100번 접근했을 때 약 9초가 걸렸다.
// 모두 동일하게 객체의 요소는 firstName, lastName으로만 구성되어 있다.
const minji = {
firstName: 'Minji',
lastName: 'Kim',
};
const hanni = {
firstName: 'Hanni',
lastName: 'Pham',
};
const daniel = {
firstName: 'Daniel',
lastName: 'Marsh',
};
const haerin = {
firstName: 'Haerin',
lastName: 'Kang',
};
const hyein = {
firstName: 'Hyein',
lastName: 'Lee',
};
- 따라서 코드를 작성할 경우 아래의 내용을 고려하도록 한다.
- JS에서 코드를 작성할 때 항상 같은 순서로 초기화 하도록 한다.
- 동일한 객체의 구조를 사용하여 인라인 캐싱이 되도록 한다.
- 동일한 메서드를 반복적으로 수행하는 코드가 서로 다른 메소드를 한번씩 수행하는 것 보다 빠르게 동작한다.