Catalog
  1. 1. V8의 JavaScript 최적화
    1. 1.1. JavaScript 컴파일
    2. 1.2. 히든클래스
    3. 1.3. 인라이닝
    4. 1.4. Number 타입
    5. 1.5. 배열처리
    6. 1.6. V8엔진에 최적화된 코드 작성을 위한 팁 정리
  2. 2. 참조
V8에서 JavaScript 개발팁

V8의 JavaScript 최적화

V8 엔진이 어떻게 JS를 최적화하는지 이해하고 이를 기반으로 성능적인 면에서 좋은 코드를 작성할 수 있는 방법을 알아보자.

JavaScript 컴파일

  • V8는 두가지 컴파일러를 사용한다고 한다.

    • Full Compiler - 일반적인 Javascript를 좋은 코드로 변환

      • 여러 데이터 처리시 성능이 떨어질 수 있다.(하나의 type유지)
      • 따라서 다형적인 연산보다 단형적인 연산을 지향한다.
      • 단형적인 연산을 함으로써 하나의 Hidden Class를 공유할 수 있다.
        1
        function add (x, y) {
        2
          return x + y
        3
        }
        4
        5
        add(1, 2); //type이 number
        6
        add("a", "b"); //type이 문자열로 바뀌었다. => 다형적 연산
    • Optimizing Compiler - 대부분의 Javascript들을 뛰어난 코드로 변환하지만 시간 더 오래걸린다.

      • Full Compiler와는 병렬로 동작하며 V8엔진이 자주 실행하는 함수를 재컴파일한다.
      • 단형적 함수와 생성자들은 완전히 인라인될 수 있다.
      • try {} catch {} 구문을 처리하지 않으므로 성능에 예민한 코드는 메소드로 선언하여 호출하는 방식을 권장한다.

히든클래스

  • V8은 런타임시 객체의 내부적인 처리를 위해 히든클래스를 생성하여 사용한다.
  • 객체의 히든클래스가 동일한 경우 하나로 공유할 수 있다.
  • 따라서 객체를 이용할 경우 되도록 객체가 생성된 시점에서 속성을 추가하지 않도록 한다.
  • 추가하더라도 인스턴스를 여러개 사용할 경우 속성의 값 할당순서를 일치시키도록 한다.
    1
    function Point(x, y) {
    2
      this.x = x;
    3
      this.y = y;
    4
    }
    5
    6
    var p1 = new Point(11, 22);
    7
    var p2 = new Point(33, 44);
    8
    // 여기에서 p1과 p2는 hidden class를 공유
    9
    10
    p2.z = 55; // p1과 p2는 이제 다른 hidden class를 갖게된다
    11
    p1.z = 60; // p1에 p2와 같은 속성을 추가하면서 p1과 p2는 다시 같은 hidden class를 공유한다

인라이닝

  • 함수의 호출 지점에 해당 함수의 내용으로 바꾸어 주는 역할을 한다.
  • 인라인캐싱의 영향으로 동일한 메소드를 반복적으로 수행하는 코드가 서로 다른 메소드를 한 번씩만 수행하는 코드보다 더 빠르게 동작한다.
  • 반복코드는 재사용하는것이 좋다.

Number 타입

  • V8은 데이터 타입 변환시 값을 효율적으로 나타내는 태그를 사용한다. 사용자가 사용하는 값을 추론하여 number의 타입을 추론한다.
  • 31비트 부호있는 정수를 할당할 경우 number, 부동 소수를 사용할 경우 double 타입으로 변하게 된다.
  • 따라서 되도록 number타입을 사용하고 데이터가 부동소수로 변하지 않게 유의하도록 한다.
    1
    var i = 10;  // 31비트 부호있는 정수,
    2
    var j = 10.2;  // 이 값은 double 타입의 부동 소수점 숫자 데이터이다.

배열처리

  • Normal Array 유형
    • 일반적인 배열처리를 위해 두가지 유형의 배열 저장소가 존재한다.
    • Fast Elements - 키 값이 순서대로 채워진 경우 사용되는 선형 저장소
    • Dictionary Elements - 나머지 경우에 사용되는 해쉬 테이블 저장소
    • 가능하면 한 유형에서 다른유형으로 전환하지 않도록 유자하도록 한다. 따라서 키값이 순서대로 채워진 배열상태를 유지하는 것을 지향한다.
      • 인덱스를 0부터 시작한다.
      • 배열 선언시 처음부터 큰 값을 할당하지 않고 차츰 늘려가는 방식으로 작업한다.
      • 숫자 배열의 요소를 삭제하지 않는다.
      • 초기화하지 안한 요소는 호출하지 않는다.
  • double Array 유형
    • 더블타입 배열이 일반 배열보다 빠르다.(일반타입은 요소별로 타입을 검사하여 히든클래스를 변경하는 작업이 있었으나, 더블타입은 여기서 제외된다.)
      1
      // 비효율적인 코드
      2
      var a = new Array();
      3
      a[0] = 77;    // 할당
      4
      a[1] = 88;
      5
      a[2] = 0.5;   // 할당, 배열 타입 변환 (일반배열 -> double 배열)
      6
      a[3] = true;  // 할당, 배열 타입 변환 (double 배열 -> 일반배열)
      7
      8
      // 효율적인 코드
      9
      var a = [77, 88, 0.5, true];
    • 배열요소 추가작업이 성능에 영향을 줄 수 있다. 따라서, 한번에 배열을 할당하게 되면 컴파일러가 모든 요소를 알고 히든클래스를 미리 정할 수 있다.

V8엔진에 최적화된 코드 작성을 위한 팁 정리

  1. 단형적인 연산을 지향하고 되도록 타입이 변하지 않도록 한다.
  2. 최적화 컴파일러는 try {} catch {}구문을 처리하지 않으므로, 성능에 예민한 코드는 함수화 하여 호출하도록 한다.
  3. 객체를 생성하여 사용하는 경우 동일한 히든클래스를 공유하도록 하자.
    • 객체 속성의 순서: 객체 속성을 항상 같은 순서로 초기화해서 히든클래스 및 이후에 생성되는 최적화 코드가 공유될 수 있도록 한다.
    • 동적 속성: 객체 생성 이후에 속성을 추가하는 것은 히든 클래스가 변하도록 강제하고 이전의 히든클래스를 대상으로 최적화되었던 모든 메소드를 느리게 만든다. 대신에 모든 객체의 속성을 생성자에서 할당하도록 하자.
  4. 메소드를 재사용을 지향하자. 동일한 메소드를 반복적으로 수행하는 코드가 서로 다른 메소드를 한 번씩만 수행하는 코드 보다 더 빠르게 동작한다.
  5. Number타입은 가능한한 정수(31비트의 숫자)를 사용하자.
  6. 배열
    • 값이 띄엄띄엄 있어서 키가 계속해서 증가하는 숫자가 되지 않는 배열은 피하자.
    • 모든 요소를 가지지는 않는 배열의 요소들은 접근하기에 많은 비용이 든다.
    • 커다란 배열을 미리 할당하지 않도록 한다. 사용하면서 크기가 커지도록 하는 게 낫습니다.
    • 마지막으로 배열의 요소를 삭제하지 않는다. 그 배열의 키가 띄엄띄엄 배치되지 않도록 하기 위함.
    • 배열의 요소별로 여러 타입의 데이터가 할당이 될 경우 한번에 배열에 할당하는것이 성능적으로 좋다.
  7. 선언한 변수의 타입이 되도록 변형되지 않도록 주의하자.

alt text

참조


Comment