개요
-
웹 어플리케이션 성능 튜닝을 위한 기본적인 절차
- 문제가 발생하기 전에 준비
- 문제 발생시 식별하고 이해
- 문제를 해결
-
V8 엔진이 어떻게 JS를 최적화하는지 이해하는 것이 가장 중요하다.
Hidden Classes
- Javascript는 컴파일시에 사용하는 타입정보에 대해 제한적이다.
- Javascript는 런타임시에 데이터 타입을 변경할 수 있다.
-
V8은 런타임시에 객체 처리를 위해 내부적으로 hidden class를 만들어서 사용한다.
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(11, 22); var p2 = new Point(33, 44); // 여기에서 p1과 p2는 hidden class를 공유합니다. p2.z = 55; // 경고! p1과 p2는 이제 다른 hidden class를 갖습니다.
- p2에서 z속성을 추가하기 전까지는 같은 hidden class 를 갖지만, z가 추가되고 나면 다른 hidden class를 갖기 때문에 성능에 악영향을 미친다.
결론
- 모든 객체 멤버를 생성자 함수 안에서 초기화 (나중에 멤버 타입이 변하지 않음)
- 항상 같은 순서로 멤버를 초기화
Numbers
- V8은 데이터 타입 변환시 값을 효율적으로 나타내는 태그 사용
- 사용자가 사용하는 값을 통해서 number타입 추론
- 데이터 타입은 동적으로 변할 수 있기 때문에, 효율적으로 값을 나타내는 태그 사용
-
number타입을 지속적으로 쓰는 것이 중요
var i = 42; // 31비트 부호있는 정수입니다. var j = 4.2; // 이 값은 double 타입의 부동 소수점 숫자 데이터입니다.
결론
- 31비트 부호있는 정수를 사용
Normal Arrays
-
큰 배열 처리를 위해 두가지 유형의 내부 배열 저장소가 존재한다.
- Fast Elements : 키 값이 순서대로 채워진 경우 사용되는 선형 저장소
- Dictionary Elements : 위 경우가 아닐 때 사용하는 해쉬 테이블 저장소
-
배열 저장소가 한 유형에서 다른 유형으로 변경되지 않는것이 중요
결론
- 인덱스 0부터 시작하는 연속키 사용
- 배열 선언시 최대사이즈를 할당하지 말고 ( > 64K 원소), 사용하면서 크기를 늘려간다
- 숫자 배열의 요소를 삭제하지 않는다
- 초기화 안한 요소는 호출하지 않는다 (아래 코드 참조)
a = new Array();
for (var b = 0; b < 10; b++) {
a[0] |= b; // 안 좋아요!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
a[0] |= b; // 훨씬 좋습니다. 2배 더 빨라요.
}
Double Arrays
- double 타입 배열이 일반 배열보다 빠른 이유 : 배열의 hidden class는 일반적으로 요소의 타입을 검사하여 hidden class를 변경하는 작업이 있으나 double의 경우 여기서 제외된다.
-
아래와 같이 부주의한 배열요소 변경은 할당과 변환이라는 추가 작업들을 만든다.
// 비효율적인 코드 var a = new Array(); a[0] = 77; // 할당 a[1] = 88; a[2] = 0.5; // 할당, 배열 타입 변환 (일반배열 -> double 배열) a[3] = true; // 할당, 배열 타입 변환 (double 배열 -> 일반배열) // 효율적인 코드 var a = [77, 88, 0.5, true];
- 일반 배열 선언후
a[2]
요소에 double 형태를 할당하면, 일반 배열에서 double 배열 형태의 배열 형태가 바뀐다. - 그리고 나서
a[3]
요소에 다시 일반 배열을 할당하면, double 배열에서 일반배열로 다시 전환된다. - 이런 추가 작업들이 결국 성능에 영향을 미치게 되기 때문에
var a = [77, 88, 0.5, true];
형태로 한번에 선언하면 컴파일러가 요소의 타입을 모두 알고 hidden class를 미리 결정할 수 있다. - 따라서 중간에 배열의 타입을 바꾸는데 생기는 불필요한 전환 작업들을 줄일 수 있다.
결론
- 초기화시 배열 리터럴을 이용하여 배열 크기를 작게 고정
- 사용하기 전 크기에 맞게 배열 사이즈를 할당 - 배열의 크기가 작으면 작은 크기의 배열에 (<64K) 에 할당
- 숫자 배열에 비숫자 값 사용 자제
- 리터럴 사용하지 않고 배열 초기화 할 때 작은 배열의 재변환 되지 않도록 주의
Javascript 컴파일
- Javascript 는 동적인 언어이고 원래는 인터프리터 방식으로 구현
- 최근 Javascript 런타임 엔진은 컴파일을 이용
-
V8 (Chrome browser’s Javascript Engine) 에는 두가지 Just-In-Time(JIT) 컴파일러가 있다.
- Full Compiler : 일반적인 Javascript를 좋은 코드로 변환
- Optimizing Compiler : 대부분의 Javascript들을 뛰어난 코드로 변환하지만 시간 더 오래걸린다
The Full Compiler
- 모든 코드에서 동작하고, 최대한 빨리 코드를 실행시키지만 뛰어난 코드보다는 일반적으로 좋은 코드들을 만든다.
- 컴파일 시점에서 데이터 타입에 대한 가정을 하지 않는다 (변수의 데이터 타입이 런타임시에 변경된다고 간주)
- Full Compiler 가 생성한 코드는 인라인 캐시 (ICs)를 사용하여 프로그램 실행시에 타입에 대한 정보를 구체화한다.
- 인라인 캐시는 타입 의존적인 코드들을 캐싱하여 타입을 효과적으로 처리한다.
- 코드가 실행시에, 타입 추정이 유효한지 확인한다. 그 후 인라인 캐시를 이용하여 동작을 단순화 한다.
- 여러 데이터 타입 처리시에는 성능이 떨어질 수 있음.
결론
- 다형적(polymorphic)연산 보다는 단형적(monomorphic)연산을 사용한다.
- 단형적 연산 : hidden class 가 항상 같다.
- 다형적 연산 : 그렇지 않고 값이 변한다.
-
아래 예제를 확인해보자
function add(x, y) { return x + y; } add(1, 2); // add 함수의 더하기는 단형적 연산입니다. add("a", "b"); // add 함수의 더하기는 다형적 연산이 됩니다.
- 위의 x, y를 보면 첫번째
add(1,2);
기본 number 타입에서 두번째add("a","b");
에서 일반 object 타입으로 변한다. - 이 경우 같은 함수에 대한 두번의 호출이 동일한 hidden class를 쓰는 것이 아니고, 달라지기 때문에 이건 다형적 연산이 된다.
The Optimizing Compiler
- Full Compiler와는 병렬로 처리된다.
- V8 엔진이 hot function(자주 실행되는 함수)을 Optimizing Compiler로 재컴파일 한다.
- 타입 피드백을 이용하여 컴파일된 코드를 빠르게 한다. (인라인캐쉬 (ICs)에서 얻은 타입을 사용한다.)
- 컴파일러 연산은 추론을 통해서 인라인 된다. (호출된 곳에 코드 위치)
- 이렇게 해서 실행속도를 빠르게 하고 다른 최적화도 가능하다.
- 단형적 함수와 생성자들은 완전히 인라인 될 수 있다 (단형적 연산이 V8에서 좋은 이유)
- 최적화 컴파일러는 현재
try {} catch {}
구문을 처리하지 않는다. - 최적화 코드 로그 보려면 V8 엔진에서
d8 --trace-opt primes.js
플래그 사용
결론
- try {} catch {} 사용시 성능에 민감한 코드는 아래 예제처럼 내장함수에 적용한다.
function perf_sensitive() {
// Do performance-sensitive work here
}
try {
perf_sensitive()
} catch (e) {
// Handle exceptions here
}
De-optimization
- 이 컴파일러는 추론을 이용하여 최적화를 한다.
- 최적화된 코드를 버리고, Full Compiler 코드의 맞는 위치에서 실행을 재개
- 최적화된 코드의 hidden class 를 변경시에 역최적화가 일어난다.
- 역최적화 코드 보려면 V8에서
d8 --trace-deopt primes.js
플래그 사용
결론
- 함수가 최적화 된 이후에 hidden class 변경을 자제한다