V8의 자바스크립트를 위한 성능 팁

Chris Wilson
Chris Wilson

소개

Daniel Clifford는 Google I/O에서 V8의 자바스크립트 성능을 개선하기 위한 도움말 및 유용한 정보를 훌륭하게 발표했습니다. Daniel은 '더 빠르게 요구'할 것을 권장했습니다. C++와 JavaScript의 성능 차이를 주의 깊게 분석하고 JavaScript의 작동 방식을 염두에 두고 코드를 작성하도록 하는 것입니다. 다니엘의 강연에서 가장 중요한 요점을 요약한 내용이 이 도움말에 나와 있으며, 실적 가이드가 변경될 때마다 이 도움말도 업데이트될 예정입니다.

가장 중요한 조언

성능 관련 조언을 컨텍스트에 반영하는 것이 중요합니다. 성능 관련 조언은 중독성이 있으며, 심층적인 조언에 먼저 집중하면 실제 문제에 집중하는 데 방해가 될 수 있습니다. 웹 애플리케이션의 성능을 전체적으로 살펴봐야 합니다. 성능 팁을 자세히 알아보기 전에 PageSpeed와 같은 도구로 코드를 분석하여 점수를 올려야 할 것 같습니다. 이렇게 하면 조기에 최적화를 방지할 수 있습니다.

웹 애플리케이션에서 좋은 성능을 얻기 위한 최선의 기본 조언은 다음과 같습니다.

  • 문제가 발생하기 전에 대비하기
  • 그런 다음 문제의 핵심을 파악하고 이해합니다.
  • 마지막으로, 중요한 사항 수정하기

이 단계를 수행하려면 V8이 JS를 최적화하는 방법을 이해하여 JS 런타임 디자인을 염두에 두고 코드를 작성할 수 있어야 합니다. 사용 가능한 도구와 이러한 도구가 어떻게 도움이 되는지 알아보는 것도 중요합니다. Daniel이 강연에서 개발자 도구를 사용하는 방법에 대해 좀 더 자세히 설명합니다. 이 문서는 V8 엔진 설계에서 가장 중요한 몇 가지 사항만 다룹니다.

이제 V8의 팁을 살펴보겠습니다.

숨겨진 수업

JavaScript에는 컴파일 시간 유형 정보가 제한되어 있습니다. 유형은 런타임에 변경될 수 있으므로 컴파일 시간에 JS 유형을 추론하는 데 비용이 많이 든다고 예상할 수 있습니다. 이로 인해 JavaScript 성능이 어떻게 C++에 가까울 수 있는지 의문이 생길 수 있습니다. 그러나 V8에는 런타임 시 객체에 대해 내부적으로 생성된 숨겨진 유형이 있습니다. 그러면 동일한 히든 클래스가 있는 객체는 동일하게 최적화된 생성된 코드를 사용할 수 있습니다.

예를 들면 다음과 같습니다.

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!```

객체 인스턴스 p2에 추가 멤버 '.z'가 추가될 때까지 p1과 p2는 내부적으로 동일한 숨겨진 클래스를 갖습니다. 따라서 V8은 p1 또는 p2를 조작하는 JavaScript 코드에 최적화된 단일 버전의 최적화된 어셈블리를 생성할 수 있습니다. 숨겨진 클래스가 분산되는 것을 더 많이 피할 수 있을수록 성능이 향상됩니다.

따라서

  • 생성자 함수의 모든 객체 멤버 초기화 (나중에 인스턴스 유형이 변경되지 않도록)
  • 항상 동일한 순서로 객체 멤버 초기화

숫자

V8은 유형이 변경될 수 있을 때 태깅을 사용하여 값을 효율적으로 나타냅니다. V8은 처리 중인 숫자 유형을 사용하는 값에서 추론합니다. V8은 이러한 추론이 끝나면 태그 지정을 사용하여 값을 효율적으로 나타냅니다. 이러한 유형이 동적으로 변경될 수 있기 때문입니다. 그러나 이러한 유형 태그를 변경하는 데 비용이 발생하는 경우도 있으므로 숫자 유형을 일관되게 사용하는 것이 가장 좋으며, 일반적으로 적절한 경우 31비트 부호 있는 정수를 사용하는 것이 가장 좋습니다.

예를 들면 다음과 같습니다.

var i = 42;  // this is a 31-bit signed integer
var j = 4.2;  // this is a double-precision floating point number```

따라서

  • 31비트 부호 있는 정수로 표시될 수 있는 숫자 값을 선호합니다.

배열

큰 배열과 희소 배열을 처리하기 위해 내부적으로 두 가지 유형의 배열 저장소가 있습니다.

  • Fast Elements: 소형 키 세트를 위한 선형 저장소
  • 사전 요소: 그렇지 않으면 해시 테이블 저장소

배열 저장소가 한 유형에서 다른 유형으로 바뀌지 않도록 하는 것이 가장 좋습니다.

따라서

  • 배열에 0부터 시작하는 연속 키 사용
  • 큰 배열 (예: 64K 초과 요소)을 최대 크기에 미리 할당하지 마세요.대신 크기가 늘어날 수 있습니다.
  • 배열 요소(특히 숫자 배열)를 삭제하지 마세요.
  • 초기화되지 않았거나 삭제된 요소를 로드하지 마세요.
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Much better! 2x faster.
}

또한 double의 배열은 더 빠릅니다. 배열의 숨겨진 클래스가 요소 유형을 추적하고 double만 포함하는 배열은 박스를 해제하여 숨겨진 클래스가 변경됩니다.그러나 배열을 부주의하게 조작하면 권투 및 언박싱으로 인해 추가 작업이 발생할 수 있습니다. 예:

var a = new Array();
a[0] = 77;   // Allocates
a[1] = 88;
a[2] = 0.5;   // Allocates, converts
a[3] = true; // Allocates, converts```

효율성이 떨어집니다.

var a = [77, 88, 0.5, true];

첫 번째 예에서 개별 할당이 차례로 수행되고 a[2]를 할당하면 배열이 박스를 개봉하지 않은 double의 배열로 변환되지만 a[3]를 할당하면 모든 값 (숫자 또는 객체)을 포함할 수 있는 배열로 다시 변환되기 때문입니다. 두 번째 경우 컴파일러가 리터럴에 있는 모든 요소의 유형을 알고 있으므로 숨겨진 클래스를 미리 결정할 수 있습니다.

  • 크기가 고정된 작은 배열에 배열 리터럴을 사용하여 초기화
  • 사용하기 전에 작은 배열(64k 미만)을 올바른 크기로 미리 할당하세요.
  • 숫자 배열에 숫자가 아닌 값 (객체)을 저장하지 않음
  • 리터럴 없이 초기화하는 경우 작은 배열을 다시 변환하지 않도록 주의하세요.

JavaScript 컴파일

JavaScript는 매우 동적인 언어이며 원래 구현은 인터프리터였지만, 최신 JavaScript 런타임 엔진은 컴파일을 사용합니다. V8 (Chrome의 자바스크립트)에는 실제로 다음과 같은 두 개의 JIT (Just-In-Time) 컴파일러가 있습니다.

  • 모든 JavaScript에 적합한 코드를 생성할 수 있는 'Full' 컴파일러
  • 최적화 컴파일러 - 대부분의 JavaScript를 위한 훌륭한 코드를 생성하지만 컴파일하는 데 시간이 더 오래 걸립니다.

전체 컴파일러

V8에서는 Full 컴파일러가 모든 코드에서 실행되고 최대한 빨리 코드 실행을 시작하므로 양질의 코드는 빠르게 생성합니다. 이 컴파일러는 컴파일 시 유형에 관해 거의 아무것도 가정하지 않으며, 변수 유형이 런타임에 변경될 수 있고 변경될 것으로 예상합니다. Full 컴파일러에서 생성된 코드는 인라인 캐시 (IC)를 사용하여 프로그램이 실행되는 동안 유형에 관한 지식을 미세 조정하므로 즉석에서 효율성을 개선합니다.

인라인 캐시의 목표는 작업의 유형에 종속된 코드를 캐시하여 유형을 효율적으로 처리하는 것입니다. 코드가 실행될 때 먼저 유형 가정을 검증한 다음 인라인 캐시를 사용하여 작업을 단축합니다. 그러나 여러 유형을 허용하는 작업의 성능이 저하됩니다.

따라서

  • 다형성 작업보다 단일화된 연산 사용이 선호됨

입력의 히든 클래스가 항상 동일하면 연산은 단형입니다. 그렇지 않으면 다형성입니다. 즉, 연산에 대한 여러 호출에서 인수의 유형이 변경될 수 있습니다. 예를 들어, 이 예에서 두 번째 add() 호출은 다형성을 야기합니다.

function add(x, y) {
  return x + y;
}

add(1, 2);      // + in add is monomorphic
add("a", "b");  // + in add becomes polymorphic```

최적화 컴파일러

전체 컴파일러와 동시에 V8은 최적화 컴파일러를 사용하여 '핫' 함수 (즉, 여러 번 실행되는 함수)를 다시 컴파일합니다. 이 컴파일러는 유형 피드백을 사용하여 컴파일된 코드를 더 빠르게 만듭니다. 실제로 방금 설명한 IC에서 가져온 유형을 사용합니다!

최적화 컴파일러에서 작업은 추측에 따라 인라인 처리됩니다 (호출되는 위치에 직접 배치됨). 이렇게 하면 실행 속도가 빨라지지만 (메모리 사용 공간 희생) 다른 최적화도 가능합니다. 단형 함수와 생성자는 완전히 인라인 처리될 수 있습니다. 이는 V8에서 단일 형태가 좋은 아이디어인 또 다른 이유입니다.

V8 엔진의 독립형 'd8' 버전을 사용하여 최적화된 항목을 기록할 수 있습니다.

d8 --trace-opt primes.js

(최적화된 함수의 이름을 stdout에 로깅합니다.)

그러나 모든 함수를 최적화할 수 있는 것은 아닙니다. 일부 기능은 지정된 함수에서 최적화 컴파일러가 실행되지 않도록 합니다('bail-out'). 특히, 최적화 컴파일러는 현재 try {} catch {} 블록을 사용하여 함수를 시작합니다.

따라서

  • {} catch {} block을 시도한 경우 perf에 민감한 코드를 중첩된 함수에 넣습니다. ```js function perf_sensitive() { // 여기에서 성능에 민감한 작업을 수행합니다. }

try { perf_sensitive() } catch (e) { // 여기에서 예외 처리 } ```

최적화 컴파일러에서 try/catch 블록을 사용 설정하므로 향후 이 지침이 변경될 수 있습니다. 위와 같이 d8에 '--trace-opt' 옵션을 사용하여 최적화 컴파일러가 함수에 대해 베이스아웃하는 방식을 검사할 수 있습니다. 이를 통해 어떤 함수가 비활성화되었는지 자세히 알아볼 수 있습니다.

d8 --trace-opt primes.js

최적화 해제

마지막으로, 이 컴파일러가 실행하는 최적화는 추측입니다. 최적화가 실행되지 않는 경우도 있기 때문에 작업을 중단합니다. '역최적화' 과정에서 최적화된 코드가 삭제되고 '전체' 컴파일러 코드의 올바른 위치에서 실행을 재개합니다. 재최적화는 나중에 다시 트리거될 수 있지만 단기적으로는 실행 속도가 느려집니다. 특히 함수를 최적화한 후 변수의 숨겨진 클래스를 변경하면 이러한 탈최적화가 발생합니다.

따라서

  • 최적화 후 함수에서 숨겨진 클래스 변경 방지

다른 최적화와 마찬가지로 로깅 플래그를 사용하여 V8이 최적화해야 했던 함수의 로그를 가져올 수 있습니다.

d8 --trace-deopt primes.js

기타 V8 도구

시작 시 Chrome에 V8 추적 옵션을 전달할 수도 있습니다.

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```

개발자 도구 프로파일링 외에도 d8을 사용하여 프로파일링할 수도 있습니다.

% out/ia32.release/d8 primes.js --prof

여기에서는 밀리초마다 샘플을 가져와서 v8.log를 작성하는 기본 제공 샘플링 프로파일러를 사용합니다.

요약

고성능 자바스크립트를 빌드하기 위해서는 V8 엔진이 코드와 함께 작동하는 방식을 파악하고 이해하는 것이 중요합니다. 다시 한번 말씀드리자면, 기본적인 조언은 다음과 같습니다.

  • 문제가 발생하기 전에 대비하기
  • 그런 다음 문제의 핵심을 파악하고 이해합니다.
  • 마지막으로, 중요한 사항 수정하기

즉, 먼저 PageSpeed와 같은 다른 도구를 사용하여 자바스크립트에 문제가 있는지 확인해야 합니다. 아마도 측정항목을 수집하기 전에 순수 자바스크립트 (DOM 없음)로 줄인 다음, 해당 측정항목을 사용하여 병목 현상을 찾고 중요한 항목을 제거해야 합니다. Daniel의 강연과 이 도움말을 통해 V8이 JavaScript를 실행하는 방법을 더 잘 이해하실 수 있기를 바랍니다. 이때 알고리즘의 최적화에 집중하십시오!

참조