>  기사  >  웹 프론트엔드  >  JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

coldplay.xixi
coldplay.xixi앞으로
2020-12-08 17:10:562851검색

javascript칼럼에서는 심층적인 V8 엔진과 최적화된 코드 작성을 소개합니다

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

관련 무료 학습 권장사항: javascript(동영상)

개요

JavaScript 엔진은 JavaScript 코드를 실행하는 프로그램 또는 인터프리터입니다. JavaScript 엔진은 표준 인터프리터로 구현되거나 JavaScript를 바이트코드로 컴파일하는 일종의 JIT(Just-In-Time) 컴파일러 형태로 구현될 수 있습니다.

JavaScript 엔진을 구현하는 인기 프로젝트 목록:

  • V8 — Google에서 개발하고 C++로 작성된 오픈 소스
  • Rhino — Mozilla Foundation에서 관리, 오픈 소스, 전적으로 Java로 개발됨
  • SpiderMonkey — Netscape Navigator를 지원하는 최초의 JavaScript 엔진이며 현재 Firefox에서 사용됩니다.
  • JavaScriptCore — 오픈 소스, Nitro로 판매되고 Apple에서 Safari용으로 개발
  • KJS — KDE용 엔진으로 원래는 Harri Porten은 KDE 프로젝트의 Konqueror 웹 브라우저용
  • Chakra(JScript9) — Internet Explorer
  • Chakra(JavaScript) — Microsoft Edge
  • Nashorn을 Oracle Java 기반 OpenJDK의 일부로 개발합니다.
  • JerryScript가 작성한 언어 및 도구 세트 —— 사물 인터넷을 위한 경량 엔진

V8 엔진을 만드는 이유는 무엇입니까?

Google에서 만든 V8 엔진은 오픈 소스이며 C++로 작성되었습니다. 이 엔진은 Google Chrome에서 사용되지만 다른 엔진과 달리 V8은 널리 사용되는 Node.js에서도 사용됩니다.
JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

V8은 원래 웹 브라우저에서 JavaScript 실행 성능을 향상시키기 위해 설계되었습니다. 속도를 높이기 위해 V8은 인터프리터를 사용하는 대신 JavaScript 코드를 보다 효율적인 기계어 코드로 변환합니다. SpiderMonkey 또는 Rhino(Mozilla)와 같은 많은 최신 JavaScript 엔진과 마찬가지로 JIT(Just-In-Time) 컴파일러를 구현하여 JavaScript 코드를 실행 시간 기계 코드로 컴파일합니다. 여기서 가장 큰 차이점은 V8이 바이트코드나 중간 코드를 생성하지 않는다는 것입니다.

V8에는 두 개의 컴파일러가 있었습니다

V8 버전 5.9가 출시되기 전에 V8 엔진은 두 개의 컴파일러를 사용했습니다.

  • full-codegen — 단순하고 상대적으로 느린 기계어 코드를 생성하는 간단하고 매우 빠른 컴파일러입니다.
  • Crankshaft - 고도로 최적화된 코드를 생성하는 더욱 정교한(Just-In-Time) 최적화 컴파일러입니다.

V8 엔진은 내부적으로 여러 스레드를 사용합니다.

  • 메인 스레드는 여러분이 기대하는 작업을 수행합니다. 코드를 가져오고, 컴파일하고, 실행합니다.
  • 컴파일을 위한 별도의 스레드도 있으므로 메인 스레드는 다음 작업을 수행할 수 있습니다. 전자는 실행을 계속하면서 코드를 최적화합니다.
  • 크랭크샤프트가 최적화할 수 있도록 많은 시간을 소비했음을 런타임에 알려주는 프로파일러 스레드
  • 일부 스레드는 가비지 수집기를 처리합니다.

JavaScript 코드가 처음 실행될 때 V8은 전체를 활용합니다. -codegen 컴파일러는 변환 없이 구문 분석된 JavaScript를 기계어 코드로 직접 변환합니다. 이를 통해 기계 코드 실행을 매우 빠르게 시작할 수 있습니다. V8은 중간 바이트코드를 사용하지 않으므로 인터프리터가 필요하지 않습니다.

코드가 한동안 실행되면 분석 스레드는 어떤 방법을 최적화해야 하는지 결정하기에 충분한 데이터를 수집했습니다.

다음으로 Crankshaft는 다른 스레드에서 최적화를 시작합니다. 이는 JavaScript 추상 구문 트리를 Hydrogen이라는 높은 수준의 정적 단일 할당(SSA) 표현으로 변환하고 Hydrogen 그래프를 최적화하려고 시도하며 대부분의 최적화는 이 수준에서 수행됩니다.

인라인 코드

첫 번째 최적화는 미리 가능한 많은 코드를 인라인하는 것입니다. 인라인은 호출 사이트(함수를 호출하는 코드 줄)를 호출된 함수의 본문으로 바꾸는 프로세스입니다. 이 간단한 단계를 통해 다음과 같은 최적화를 더 효과적으로 수행할 수 있습니다.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

숨겨진 클래스

JavaScript는 프로토타입 기반 언어입니다. 클래스와 개체는 복제 프로세스를 사용하여 생성되지 않습니다. JavaScript는 동적 프로그래밍 언어이기도 합니다. 즉, 인스턴스화 후 개체에서 속성을 쉽게 추가하거나 제거할 수 있습니다.

대부분의 JavaScript 인터프리터는 사전과 같은 구조(해시 함수 기반)를 사용하여 객체 속성 값의 위치를 ​​메모리에 저장합니다. 이 구조는 JavaScript에서 속성 값을 검색하는 것과 같은 비동적 프로그래밍을 더 빠르게 만듭니다. Java 또는 C# 계산 비용은 언어에서 더 높습니다.

Java에서 모든 개체 속성은 컴파일 전에 고정된 개체 레이아웃에 의해 결정되며 런타임에 동적으로 추가하거나 제거할 수 없습니다(물론 C#에는 또 다른 주제인 동적 입력이 있습니다).

따라서 속성 값(또는 이러한 속성에 대한 포인터)은 각 버퍼 사이에 고정된 오프셋을 사용하여 연속된 버퍼로 메모리에 저장될 수 있습니다. 오프셋의 길이는 속성 유형에 따라 쉽게 결정될 수 있지만 런타임에는 그렇습니다. JavaScript에서는 불가능한 속성 유형을 변경할 수 있습니다.

사전을 사용하여 메모리에서 객체 속성의 위치를 ​​찾는 것은 매우 비효율적이므로 V8은 숨겨진 클래스라는 다른 접근 방식을 사용합니다. 숨겨진 클래스는 런타임에 생성된다는 점을 제외하면 Java와 같은 언어에서 사용되는 고정 객체(클래스)와 유사하게 작동합니다. 이제 실제 예를 살펴보겠습니다.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

"new Point(1,2)" 호출이 발생하면 V8은 "C0"이라는 숨겨진 클래스를 생성합니다.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

아직 Point에 대해 정의된 속성이 없으므로 "C0"은 비어 있습니다.

첫 번째 문 "this.x = x"가 ("Point" 함수 내에서) 실행되면 V8은 "C0"을 기반으로 하는 "C1"이라는 두 번째 히든 클래스를 생성합니다. "C1"은 속성 x를 찾을 수 있는 메모리 내 위치(객체 포인터 기준)를 설명합니다.

이 경우 "x"는 오프셋 0에 저장됩니다. 즉, 메모리의 포인트 개체를 연속 버퍼로 간주할 때 첫 번째 오프셋은 "x" 속성에 해당합니다. V8은 또한 속성 "x"가 포인트 객체에 추가되면 숨겨진 클래스가 "C0"에서 "C1"로 전환되어야 함을 나타내는 "클래스 변환"으로 "C0"을 업데이트합니다. 아래 포인트 개체의 숨겨진 클래스는 이제 "C1"입니다.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

객체에 새 속성이 추가될 때마다 이전 히든 클래스는 새 히든 클래스를 가리키는 변환 경로로 업데이트됩니다. 히든 클래스 캐스트는 동일한 방식으로 생성된 객체 간에 히든 클래스를 공유할 수 있도록 하기 때문에 중요합니다. 두 객체가 히든 클래스를 공유하고 동일한 속성이 여기에 추가되면 변환을 통해 두 객체 모두 동일한 새 히든 클래스와 그에 따른 모든 최적화 코드를 받도록 보장됩니다.

"this.y = y" 문이 실행될 때("Point" 함수 내부, "this.x = x" 문 뒤) 동일한 프로세스가 반복됩니다.

"C2"라는 새로운 히든 클래스가 생성됩니다. "y" 속성이 이미 "x" 속성을 포함하고 있는 Point 개체에 추가되면 클래스 변환이 "C1"에 추가됩니다. 히든 클래스는 "C2"로 변경되어야 하며 포인트 객체의 히든 클래스는 "C2"로 업데이트됩니다.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

숨겨진 클래스 변환은 속성이 객체에 추가되는 순서에 따라 달라집니다. 아래 코드 조각을 살펴보세요.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

이제 p1과 p2에 대해 동일한 숨겨진 클래스와 변환이 사용된다고 가정합니다. 따라서 "p1"의 경우 먼저 "a" 속성을 추가한 다음 "b" 속성을 추가합니다. 그러나 "p2"에는 "b"가 먼저 할당된 다음 "a"가 할당됩니다. 따라서 "p1"과 "p2"는 변환 경로가 다르기 때문에 숨겨진 카테고리가 달라집니다. 이 경우 히든 클래스를 재사용할 수 있도록 동적 속성을 동일한 순서로 초기화하는 것이 훨씬 좋습니다.

인라인 캐싱

V8은 인라인 캐싱이라는 동적으로 입력된 언어를 최적화하기 위한 또 다른 기술을 활용합니다. 인라인 캐싱은 동일한 메소드에 대한 반복 호출이 동일한 유형의 객체에서 발생하는 경향이 있다는 관찰에 의존합니다. 인라인 캐싱에 대한 자세한 설명은 여기에서 확인할 수 있습니다.

인라인 캐싱의 일반적인 개념은 다음에서 논의됩니다(위의 심층 튜토리얼을 진행할 시간이 없는 경우).

그렇다면 V8은 최근 메소드 호출에서 인수로 전달된 객체 유형의 캐시를 유지하고 이 정보를 사용하여 향후 인수로 전달된 객체 유형을 예측합니다. V8이 메소드에 전달된 객체의 유형을 충분히 잘 예측할 수 있다면 객체의 속성에 액세스하는 방법에 대한 프로세스를 우회하고 대신 객체의 숨겨진 클래스에 대한 이전 조회에서 저장된 정보를 사용할 수 있습니다.

그렇다면 히든 클래스와 인라인 캐싱의 개념은 어떤 관련이 있나요? 특정 개체에 대해 메서드가 호출될 때마다 V8 엔진은 해당 개체의 숨겨진 클래스를 검색하여 특정 속성에 액세스할 오프셋을 결정해야 합니다. 동일한 히든 클래스를 두 번 성공적으로 호출한 후 V8은 히든 클래스의 조회를 생략하고 단순히 객체 포인터 자체에 속성의 오프셋을 추가합니다. 이 메소드에 대한 모든 다음 호출에서 V8 엔진은 히든 클래스가 변경되지 않았다고 가정하고 이전 조회에서 저장된 오프셋을 사용하여 특정 속성의 메모리 주소로 직접 점프합니다. 이로 인해 실행 속도가 크게 향상됩니다.

인라인 캐싱은 동일한 유형의 객체가 히든 클래스를 공유하는 것이 중요한 이유이기도 합니다. 동일한 유형과 다른 히든 클래스의 두 객체를 생성하는 경우(이전 예제에서 했던 것처럼) V8은 인라인 캐싱을 사용할 수 없습니다. 두 객체가 동일한 유형이더라도 해당 히든 클래스는 해당 객체이기 때문입니다. 속성에는 다른 오프셋이 할당됩니다.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

두 개체는 기본적으로 동일하지만 "a" 및 "b" 속성이 다른 순서로 생성됩니다.

기계 코드로 컴파일

수소 그래프가 최적화되면 크랭크샤프트는 이를 리튬이라는 하위 수준 표현으로 줄입니다. 대부분의 리튬 구현은 아키텍처에 따라 다릅니다. 레지스터 할당은 종종 이 수준에서 발생합니다.

마지막으로 리튬은 기계어 코드로 컴파일됩니다. 그런 다음 OSR: 스택 내 교체가 있습니다. 명시적인 장기 실행 방법을 컴파일하고 최적화하기 전에 스택 교체를 실행할 수 있습니다. V8은 단지 천천히 스택 교체를 수행하고 다시 최적화를 시작하지 않습니다. 대신 실행 중에 최적화된 버전으로 전환하기 위해 우리가 가지고 있는 모든 컨텍스트(스택, 레지스터)를 변환합니다. 다른 최적화 중에서 V8이 처음에 코드를 인라인한다는 점을 고려하면 이는 매우 복잡한 작업입니다. V8만이 그것을 할 수 있는 유일한 엔진은 아닙니다.

엔진이 유효하지 않다고 가정하고 반대 변환을 수행하고 최적화되지 않은 코드를 반환하는 역최적화라는 안전 조치가 있습니다.

Garbage Collection

가비지 수집을 위해 V8은 전통적인 표시 및 청소 알고리즘을 사용하여 이전 세대를 정리합니다. 표시 단계에서는 JavaScript 실행을 중지해야 합니다. GC 비용을 제어하고 실행을 보다 안정적으로 만들기 위해 V8은 증분 표시를 사용합니다. 즉, 전체 힙을 탐색하고 가능한 모든 객체를 표시하는 대신 힙의 일부만 탐색한 다음 정상적인 실행을 다시 시작합니다. 다음 GC 중지는 이전 힙 워크가 중단된 지점에서 계속됩니다. 이는 이전에 언급한 대로 스캔 단계가 별도의 스레드에 의해 처리되는 정상적인 실행 중에 매우 짧은 일시 중지를 허용합니다.

최적화된 JavaScript 작성 방법

  1. 객체 속성 순서: 숨겨진 클래스와 이후에 최적화된 코드를 공유할 수 있도록 항상 동일한 순서로 객체 속성을 인스턴스화하세요.
  2. 동적 속성: 인스턴스화 후 객체에 속성을 추가하면 히든 클래스가 강제로 변경되고 이전에 히든 클래스에 의해 최적화된 모든 메서드의 실행 속도가 느려지므로 객체의 모든 속성을 생성자에 할당하세요.
  3. Methods: 동일한 메서드를 반복적으로 실행하는 코드는 여러 다른 메서드를 한 번만 실행하는 코드보다 더 빠르게 실행됩니다(인라인 캐싱으로 인해).
  4. Arrays: 키 값이 자동 증가 숫자가 아닌 희소 배열을 피하고 모든 요소를 ​​저장하지 않는 희소 배열은 해시 테이블입니다. 이러한 배열의 요소에 액세스하는 데는 비용이 많이 듭니다. 또한 대규모 배열을 사전 할당하지 마십시오. 수요에 따라 성장하는 것이 더 좋습니다. 마지막으로, 배열에서 요소를 삭제하지 마세요. 삭제하면 키가 희박해지기 때문입니다.
  5. 태그 값: V8은 32비트를 사용하여 개체와 값을 나타냅니다. 값이 31비트이므로 1비트를 사용하여 객체(flag = 1)인지 SMI(SMall Integer)(flag = 0)라는 정수인지 구분합니다. 그런 다음 숫자가 31자리보다 크면 V8은 숫자를 상자에 넣어 double로 바꾸고 숫자를 보관할 새 개체를 만듭니다. JS 객체에 대한 비용이 많이 드는 박싱 작업을 피하려면 가능할 때마다 31비트 부호 있는 숫자를 사용하십시오.

Ignition and TurboFan

2017년 초 V8 5.9가 출시되면서 새로운 실행 파이프라인이 도입되었습니다. 이 새로운 파이프라인은 실제 JavaScript 애플리케이션에서 더 큰 성능 향상과 상당한 메모리 절약을 가능하게 합니다.

새로운 실행 흐름은 Ignition(V8의 인터프리터) 및 TurboFan(V8의 최신 최적화 컴파일러)을 기반으로 구축되었습니다.

V8 5.9 출시 이후 V8 팀은 새로운 JavaScript 언어 기능과 이러한 기능에 필요한 최적화를 따라잡기 위해 고군분투했기 때문에 V8 팀은 더 이상 전체 코드 생성 및 크랭크샤프트(이후 V8 기술을 제공하고 있음)를 사용하지 않습니다. 2010).

이는 V8이 전반적으로 더 간단하고 유지 관리하기 쉬운 아키텍처를 갖게 된다는 것을 의미합니다.

JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.

이러한 개선 사항은 시작에 불과합니다. 새로운 Ignition 및 TurboFan 파이프라인은 JavaScript 성능을 향상시키고 앞으로 몇 년 동안 Chrome 및 Node.js에서 V8의 공간을 축소할 추가 최적화를 위한 길을 열어줍니다.

관련 무료 학습 권장 사항: php 프로그래밍(동영상)

위 내용은 JavaScript 작동 방식을 이해하고, V8 엔진을 자세히 알아보고, 최적화된 코드를 작성하세요.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제