티스토리 뷰

자바스크립트 엔진

  • 컴퓨터가 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 컴파일러는 프로그램이 실행되는 도중 코드를 컴파일 한다.
      • 프로그램이 실행되기 전에 코드 전체를 컴파일하는 정적 컴파일과는 다른 방식이다.
    • 성능 최적화
      • 프로그램 실행 중 수집된 런타임 정보를 활용하여 최적화된 기계어 코드를 생성할 수 있다.
      • 프로그램의 실행 성능을 향상시킬 수 있다.
  • 동작
    1. 코드를 토큰화 하여 AST(Abstract Syntax Tree, 추상 구문 트리)를 생성
    2. 인터프리터를 사용하여 AST 기반 코드를 실행
    3. 인터프리터는 코드 실행 과정 프로파일링 정보 수집
    4. 프로파일링 정보를 기반으로 JIT 컴파일러가 IR(Intermediate Representation, 중간 표현)을 생성
    5. 터보팬에서는 IR를 분석하여 최적화 시도
    6. 인라인 캐싱, 히든 클래스 등 최적화
    7. 최적화된 IR코드가 기계어로 컴파일 됨
    8. 기계어를 실행함

히든클래스

  • 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에서 코드를 작성할 때 항상 같은 순서로 초기화 하도록 한다.
    • 동일한 객체의 구조를 사용하여 인라인 캐싱이 되도록 한다.
    • 동일한 메서드를 반복적으로 수행하는 코드가 서로 다른 메소드를 한번씩 수행하는 것 보다 빠르게 동작한다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함