빨간색코딩

V8 javascript 엔진 (Hidden Class, 인라인캐싱, 메모리구조, 호출스택, heap, GC) 본문

Web

V8 javascript 엔진 (Hidden Class, 인라인캐싱, 메모리구조, 호출스택, heap, GC)

빨간색소년 2017. 7. 21. 18:29

 


자바스크립트 엔진은 javascript로 작성된 코드를 해석하고 실행하는 인터프리터다. js엔진은 브라우저 벤더별로 다양하다. Mozilla의 Monkey시리즈, 자바의 바이트코드로 컴파일해주는 Rhino, 구글 크롬의 V8, Safari의 JavascriptCore, Explorer의 Chakra 등이 있다.

여기서 nodejs는 구글의 V8 JavaScript 엔진을 기반으로 동작한다. 우리가 자주쓰는 크롬브라우저도 V8엔진을 쓴다. 성능좋은 코드를 위해 V8을 알아보자

1. Hidden Class로 빠른 프로퍼티 접근과 정적 룩업

다른 JavaScript Engine이 프로퍼티를 저장하기 위해서 사전식 데이터 구조를 이용하지만, V8은 hidden class를 이용한다. 이 둘의 차이는 단순하게 이야기해서 Hashing과 Pointer의 차이라고 할 수 있다.

V8은 객체에 새로운 프로퍼티를 추가할 때 hidden class를 생성하고, hidden class에 프로퍼티의 정적인 위치(offset)를 저장함으로써 실제 데이터가 저장되어 이는 위치에 대한 Pointer를 제공한다. 이로 인해 런타임에 데이터접근이 필요 없어지고, 고전적인 클래스 기반의 최적화를 할 수 있다. (위치 정보 해석할 필요가 없어져서 빨라진다)

매번 프로퍼티를 추가할 때마다 새로운 hidden class를 생성하는 방식은 상당히 비효율적이지만, 다음 번에 같은 객체를 생성할 때 이전에 생성했던 hidden class를 재사용함으로써 객체 생성 비용을 줄일 수 있다.

따라서, 아래와 같이 한다면 hidden class가 달라지지 않으니 성능에 최적화된다.

  • 모든 객체 멤버를 생성자 함수 안에서 초기화 (나중에 멤버 타입 변경X)
  • 항상 같은 순서로 객체 멤버를 초기화

2. 배열

V8은 배열 처리를 위해 2가지 유형을 사용한다. 이 두가지 유형이 서로 다른 유형이 되지 않는게 중요하다.

  1. 선형 타입 : 키 값이 빈틈없이 채워진 경우
  2. 해쉬 타입 : 그렇지 않은 경우는 해쉬 테이블에 저장

따라서 인덱스는 0부터 순차적으로 쓰고, 배열크기를 선언하고 쓰지말고 사용하면서 늘려가는 게 좋다. 예를들어 순차적으로 잘쓰고 있었는데, 중간 요소 날려버리면 1타입에서 2타입으로 전환되니 성능에 영향을 준다.

또한, double형태 배열은 별도의 타입인데, 이것이 일반 배열과 타입변환이 일어나는 것도 성능에 영향을 준다.

예를 들어,

// 비효율적인 코드
var arr = [];
arr[0] = 77;   // 할당
arr[1] = 88;
arr[2] = 0.5;   // 할당, double배열 타입으로 변환
arr[3] = true; // 할당, 일반배열 타입으로 변환
 
// 효율적인 코드
var arr = [77, 88, 0.5, true];

즉, V8엔진에서 컴파일러에게 미리 알려줘서 추론할 수 있게 해주는게 제일 좋다. (가능하다면..)

3. 컴파일러 + 인라인 캐싱을 통한 동적인 기계어 코드 생성

V8은 JavaScript 소스 코드를 처음 컴파일 할 때 bytecode가 아닌 기계어 코드로 직접 변환한다. 따라서 중간에 bytecode를 기계어로 변환해 줄 인터프리터가 필요 없다. js는 동적인 언어라서 보통 엔진들은 인터프리터 방식으로 구현되지만, V8은 2가지 방식의 jit 컴파일러를 갖고 병렬적으로 사용한다.

  • 전체 컴파일러 : 모든 js코드를 기계어로 변환
  • 최적 컴파일러 : 대부분의 js코드를 컴파일, 시간이 더 소요

3-1. 전체 컴파일러

js의 모든 코드를 기계어로 변환한다. 다만 컴파일 시점에서 데이터 타입에 대해서는 아무것도 건드리지 않는다. 데이터 타입은 런타임 시 변경될 수 있기 때문이다. 그렇기 때문에 인라인 캐싱을 이용해서 런타임 도중에 즉석에서 데이터 타입을 바꾼다. 그러나 여러 타입을 처리해야하는 경우, 오히려 성능이 떨어질 수도 있다.

3-2. 최적 컴파일러

전체 컴파일러와 병렬적으로 처리한다. 특히 여러번 호출되는 함수를 재컴파일하는데, 컴파일된 코드를 더 빠르게 만들기 위해서 타입 피드백(인라인 캐싱을 통해 얻은 정보들을 이용)을 이용한다. 즉 추론을 통해서 최적화를 수행한다.

4. 메모리 영역

V8의 메모리공간을 Resident Set이라 부른다. 메모리 세그먼트는 아래와 같이 나뉜다.

  • Code : 실행될 코드들
  • Stack : heap에 있는 object를 참조하는 포인터, 원시타입들이 있다.
  • Heap : object, string, 클로저와 같은 레퍼런스 타입을 저장한다.

4-1. Heap 영역

  1. New space
    • 새 할당이 발생하는 영역, 대부분의 객체들이 여기에 있다. 잦은 GC가 발생하기 때문에 빠르게 GC될수 있도록 설계되었다.
    • 메모리 크긴느 1~8MB 사이이며, Young Generation이라고도 부른다.
    • 20% 정도가 old space로 장기화된다.
  2. Old-pointer space
    • 다른 객체에 대한 포인터를 가진 대부분의 객체들이 여기에 있다.
  3. Old-data space
    • Old-pointer space 아닌 객체들이 있다.
  4. Large-object space
    • 다른 space의 크기 제약보다 큰 크기를 가지는 객체들이 저장된다.
    • 각 객체들은 고유의 memory-mapped 영역을 가지며, Garbage collector에 의해 수집되지 않는다.
  5. Code space
    • Just-In-Time 컴파일된 인스트럭션을 포함하는 코드 객체들이 저장된다.
    • 실행 가능한 메모리를 가지는 유일한 space이다.
  6. Cell space, Property-cell space, Map space
    • 각각 Cells, Property-Cells, Maps가 저장된다.
    • 이 공간들에 위치한 객체들은 모두 그 크기와 타입이 같아서 GC가 쉽다.

5. Call Stack

V8엔진은 하나의 호출스택만을 쓴다. 요청이 들어오면 순차적으로 push하고 끝나면 pop한다.

function h(z) {
    console.log(new Error().stack); // 의도적인 에러로 호출스택 확인
}
function g(y) {
    h(y + 1);
}
function f(x) {
    g(x + 1); 
}
f(36);

위 코드에서 f(36) 전에는 스택이 비어있고 이때가 전역스코프이다. 이후엔 각 함수스코프가 쌓인다.

6. Garbage collector

GC는 자바에서도 그렇지만 객체가 어디에서도 참조되고 있지 않을 때 대상이 된다. 어떤 Garbage collector라도 풀어야 하는 임무는 힙 영역에서 포인터와 데이터를 구분해 내는 것이다. 살아있는 오브젝트를 찾아내기 위해서는 GC가 포인터를 따라갈 수 있어야 한다.

이를 위해 V8은 Tagged pointer를 사용한다. 데이터 끝에 약간의 비트를 마련하고 여기에 포인터인지 데이터인지 태깅하는 방식이다.

이후에 구체적으로 어떻게 동작하는지는.. New space 끝에 도달하면 트리거로 Scavenge되고 마킹되고 등등 아주 복잡하다;;

아무튼 이런 방식으로 객체와 포인터가 메모리상에 어디에 위치해 있는지 정확히 관리하여 메모리 누수를 피한다.


'Web' 카테고리의 다른 글

JSONP  (0) 2017.07.25
크로스도메인 이슈 (SOP, CORS)  (0) 2017.07.25
JQuery 기초 (JQuery 객체, 조작)  (0) 2017.07.21
Client(브라우저) Javascript (DOM API, data-, event, ajax)  (0) 2017.07.21
RESTful 아키텍처, 서비스  (0) 2017.07.13
Comments