분류 javascript

더 가벼운 V8

컨텐츠 정보

  • 조회 323 (작성일 )

본문

2018 년 말에 V8의 메모리 사용량을 대폭 줄이려는 V8 Lite라는 프로젝트를 시작했습니다. 처음에 이 프로젝트는 처리량 실행 속도보다 메모리 사용량 감소에 더 관심이 있는 저 메모리 모바일 장치 또는 임베더 사용 사례를 목표로 V8의 별도 Lite 모드로 계획되었습니다. 

그러나 이 작업 과정에서 이 Lite 모드에 대해 수행 한 많은 메모리 최적화가 일반 V8로 전환되어 V8의 모든 사용자에게 도움이 될 수 있음을 깨달았습니다.


https://v8.dev/blog/v8-lite 


이 게시물에서는 개발 한 주요 최적화 중 일부와 실제 워크로드에서 제공 한 메모리 절약에 대해 중점적으로 다룹니다.


참고 : 기사를 읽는 것보다 프레젠테이션을 보려면 아래 비디오를 즐기십시오! 그렇지 않은 경우 비디오를 건너 뛰고 계속 읽으십시오.


라이트 모드 


V8의 메모리 사용량을 최적화 하려면 먼저 V8에서 메모리를 사용하는 방법과 V8의 힙 크기에 어떤 객체 유형이 영향을 미치는지 이해해야 했습니다. 우리는 V8의 메모리 시각화 도구를 사용하여 여러 일반적인 웹 페이지에서 힙 구성을 추적했습니다.


memory-categorization.svg 


이를 통해 V8 힙의 상당 부분이 JavaScript 실행에 필수적이지 않지만 JavaScript 실행을 최적화하고 예외적 인 상황을 처리하는 데 사용되는 객체 전용으로 결정되었습니다. 예를 들면 다음과 같습니다. 최적화 된 코드; 코드를 최적화 하는 방법을 결정하는 데 사용되는 유형 피드백; C ++와 JavaScript 객체 간의 바인딩을 위한 중복 메타 데이터; 스택 추적 기호화와 같은 예외적 인 상황에서만 필요한 메타 데이터; 그리고 페이지 로딩 중 몇 번만 실행되는 함수의 바이트 코드.


그 결과, 우리는 이러한 선택적 객체의 할당을 크게 줄임으로써 향상된 메모리 절약과 JavaScript 실행 속도를 상쇄하는 V8 Lite 모드 작업을 시작했습니다.

v8-lite.png 


V8의 TurboFan 최적화 컴파일러를 비활성화 하는 등 기존 V8 설정을 구성하면 다양한 라이트 모드를 변경할 수 있습니다. 그러나 다른 사람들은 V8에 더 많은 관련 변경을 요구했습니다.


특히 라이트 모드는 코드를 최적화 하지 않기 때문에 최적화 컴파일러에 필요한 유형 피드백 수집을 피할 수 있습니다. 점화 인터프리터에서 코드를 실행할 때 V8은 나중에 해당 유형에 맞게 최적화 하기 위해 다양한 연산 (예 : + 또는 o.foo)에 전달되는 피연산자 유형에 대한 피드백을 수집합니다. 이 정보는 피드백 벡터에 저장되어 V8 힙 메모리 사용량의 상당 부분을 차지합니다. 라이트 모드에서는 이러한 피드백 벡터를 할당하지 않아도 되지만 V8 인라인 캐시 인프라의 인터프리터 및 일부는 피드백 벡터를 사용할 수 있어야 하므로 이 피드백 없는 실행을 지원하려면 상당한 리팩터링이 필요했습니다.


V8 v7.3에서 시작된 Lite 모드는 피드백 벡터를 할당하지 않고 거의 실행되지 않는 바이트 코드의 에이징을 수행하지 않고 코드 최적화를 비활성화 함으로써 V8 v7.1에 비해 일반적인 웹 페이지 힙 크기를 22 % 줄였습니다 (아래 설명 참조). 이는 더 나은 메모리 사용을 위해 성능을 명시 적으로 절충하려는 응용 프로그램에 좋은 결과입니다. 그러나 이 작업을 수행하는 과정에서 V8을 더 게으름으로써 성능에 영향을 미치지 않으면 서 Lite 모드의 메모리를 최대한 절약 할 수 있음을 깨달았습니다.


지연 피드백 할당 


피드백 벡터 할당을 완전히 비활성화 하면 V8의 TurboFan 컴파일러로 코드를 최적화 할 수 있을 뿐만 아니라 V8이 Ignition 인터프리터의 객체 속성로드와 같은 일반적인 작업에 대해 인라인 캐싱을 수행 할 수 없습니다. 따라서 이렇게 하면 일반적인 대화 형 웹 페이지 시나리오에서 V8의 실행 시간이 크게 단축되어 페이지 로드 시간이 12 % 단축되고 V8에서 사용되는 CPU 시간이 120 % 증가합니다.


이러한 회귀 없이 대부분의 절감 효과를 일반 V8로 가져 오기 위해 함수가 특정 양의 바이트 코드 (현재 1KB)를 실행 한 후 피드백 벡터를 느리게 할당하는 방식으로 이동했습니다. 대부분의 함수는 자주 실행되지 않으므로 대부분의 경우 피드백 벡터 할당을 피하지만 성능 회귀를 피하고 코드를 최적화 하기 위해 필요한 곳에 신속하게 할당합니다.


이 접근법의 한 가지 추가적인 문제는 피드백 벡터가 트리를 형성하고 내부 함수에 대한 피드백 벡터가 외부 함수의 피드백 벡터에 항목으로 유지된다는 사실과 관련이 있습니다. 이것은 새로 생성 된 함수 클로저가 동일한 함수에 대해 생성 된 다른 모든 클로저와 동일한 피드백 벡터 배열을 수신하기 위해 필요합니다. 피드백 벡터의 지연 할당에서는 피드백 벡터를 사용하여 이 트리를 구성 할 수 없습니다. 내부 함수가 수행 할 때 외부 함수가 피드백 벡터를 할당 할 것이라는 보장이 없기 때문입니다. 이 문제를 해결하기 위해이 트리를 유지하기 위해 새로운 ClosureFeedbackCellArray를 만든 다음 함수의 ClosureFeedbackCellArray를 뜨거워지면 전체 FeedbackVector로 교체합니다.


lazy-feedback.svg 

우리의 실험실 실험과 현장에서의 원격 측정은 데스크탑에서의 지연된 피드백에 대한 성능 회귀를 보여주지 않았으며 모바일 플랫폼에서는 실제로 가비지 수집 감소로 인해 저가형 장치에서 성능이 향상되었습니다. 따라서, 우리는 원래의 피드백 없는 할당 방식에 비해 약간의 메모리 회귀가 실제 성능의 향상에 의해 보상 되는 라이트 모드를 포함하여 모든 V8 빌드에서 지연된 피드백 할당을 가능하게 했습니다.


지연 소스 위치 


JavaScript에서 바이트 코드를 컴파일 할 때 바이트 코드 시퀀스를 JavaScript 소스 코드 내의 문자 위치에 묶는 소스 위치 테이블이 생성됩니다. 그러나 이 정보는 예외를 상징하거나 디버깅과 같은 개발자 작업을 수행 할 때만 필요하므로 거의 사용되지 않습니다.


이 낭비를 피하기 위해 소스 위치를 수집하지 않고 바이트 코드를 컴파일합니다 (디버거나 프로파일러가 연결되지 않은 경우). 소스 위치는 스택 추적이 실제로 생성 된 경우 (예 : Error.stack을 호출하거나 예외의 스택 추적을 콘솔에 인쇄하는 경우)에만 수집됩니다. 소스 위치를 생성하려면 기능을 재분석하고 컴파일 해야 하므로 비용이 많이 들지만 대부분의 웹 사이트는 프로덕션 환경에서 스택 추적을 상징하지 않으므로 성능에 영향을 미치지 않습니다.


이 작업에서 해결해야 할 한 가지 문제는 이전에 보장되지 않았던 반복 가능한 바이트 코드 생성을 요구하는 것이었습니다. V8이 원래 코드와 비교하여 소스 위치를 수집 할 때 다른 바이트 코드를 생성하면 소스 위치가 정렬되지 않고 스택 추적이 소스 코드에서 잘못된 위치를 가리킬 수 있습니다.


특정 상황에서 V8은 함수의 초기 열성 구문 분석과 나중에 지연 컴파일간에 일부 구문 분석기 정보가 손실되어 함수가 열성적으로 컴파일 되었는지 또는 지연 컴파일 되었는지에 따라 다른 바이트 코드를 생성 할 수 있습니다. 이러한 불일치는 예를 들어 변수가 불변이고 따라서 최적화 할 수 없다는 사실을 잃어 버리는 등 대부분 양성이었습니다. 그러나 이 작업으로 밝혀지지 않은 일부 불일치는 특정 상황에서 잘못된 코드 실행을 야기 할 가능성이있었습니다. 결과적으로, 우리는 이러한 불일치를 수정하고 검사 및 스트레스 모드를 추가하여 함수의 열성 및 지연 컴파일이 항상 일관된 출력을 생성하도록 하여 V8 파서 및 프리 파서의 정확성과 일관성에 대해 더 큰 확신을 제공합니다.


바이트코드 플러싱 


JavaScript 소스에서 컴파일 된 바이트 코드는 관련 메타 데이터를 포함하여 일반적으로 약 15 %의 V8 힙 공간을 차지합니다. 초기화 중에 만 실행되거나 컴파일 후에 거의 사용되지 않는 많은 기능이 있습니다.


결과적으로 컴파일 된 바이트 코드가 최근에 실행되지 않은 경우 가비지 수집 중에 함수에서 플러시 되는 지원을 추가했습니다. 이를 위해 함수의 바이트 코드 사용 기간을 추적하여 주요 (소형) 가비지 콜렉션 마다 사용 기간을 늘리고 함수 실행시 0으로 재설정 합니다. 에이징 임계 값을 넘는 모든 바이트 코드는 다음 가비지 수집에 의해 수집 될 수 있습니다. 수집 된 후 나중에 다시 실행되면 다시 컴파일 됩니다.


바이트 코드가 더 이상 필요하지 않을 때만 플러시 되도록 하는 기술적 인 문제가 있었습니다. 예를 들어, 함수 A가 다른 장기 실행 함수 B를 호출하면 함수 A는 여전히 스택에 있는 동안 에이징 될 수 있습니다. 장기 실행 함수 B가 리턴 될 때 리턴해야 하므로 에이징 임계 값에 도달하더라도 함수 A의 바이트 코드를 비우고 싶지 않습니다. 따라서 우리는 바이트 코드가 노화 임계 값에 도달 할 때 함수에서 약하게 유지되지만 스택 또는 다른 곳에서 참조에 의해 강력하게 유지되는 것으로 취급합니다. 강력한 링크가 남아 있지 않은 경우에만 코드를 플러시 합니다.


플러시 바이트 코드 외에도 이러한 플러시 함수와 관련된 피드백 벡터를 플러시 합니다. 그러나 바이트 코드와 동일한 GC 사이클 동안 동일한 객체에 의해 유지되지 않기 때문에 피드백 벡터를 플러시 할 수 없습니다. 바이트 코드는 기본 컨텍스트 독립적 인 SharedFunctionInfo에 의해 유지되는 반면, 피드백 벡터는 기본 컨텍스트에 따라 유지됩니다. JS 함수. 결과적으로 후속 GC 사이클에서 피드백 벡터를 플러시 합니다.


bytecode-flushing.svg 


추가 최적화 


이러한 대규모 프로젝트 외에도 두 가지 비 효율성을 발견하고 해결했습니다.


첫 번째는 FunctionTemplateInfo 객체의 크기를 줄이는 것이었습니다. 이 객체는 Chrome과 같은 임베더가 JavaScript 코드로 호출 할 수 있는 함수의 C ++ 콜백 구현을 제공 할 수 있도록 하는 데 사용되는 FunctionTemplates에 대한 내부 메타 데이터를 저장합니다. Chrome은 DOM 웹 API를 구현하기 위해 많은 FunctionTemplate을 도입하므로 FunctionTemplateInfo 객체가 V8의 힙 크기에 기여했습니다. FunctionTemplates의 일반적인 사용법을 분석 한 결과, FunctionTemplateInfo 객체의 11 개 필드 중 3 개만 기본값이 아닌 값으로 설정되었음을 발견했습니다. 따라서 희소 필드가 필요할 때만 할당되는 사이드 테이블에 저장되도록 FunctionTemplateInfo 객체를 분할합니다.


두 번째 최적화는 TurboFan 최적화 코드에서 최적화 해제하는 방법과 관련이 있습니다. TurboFan은 추론 최적화를 수행하기 때문에 특정 조건이 더 이상 유지되지 않으면 해석기로 넘어 가야 합니다 (비 최적화). 각 Deopt 포인트에는 런타임이 바이트 코드에서 인터프리터에서 실행을 반환해야 하는 위치를 결정할 수 있는 id가 있습니다. 이전에는 이 ​​ID는 최적화 된 코드가 큰 점프 테이블 내의 특정 오프셋으로 점프하여 올바른 id를 레지스터에로드 한 다음 런타임으로 점프하여 역 최적화를 수행함으로써 계산되었습니다. 이는 각 Deopt 포인트에 대해 최적화 된 코드에서 단일 점프 명령 만 필요로 하는 이점이 있었습니다. 그러나 최적화 해제 점프 테이블은 사전 할당되어 전체 최적화 해제 ID 범위를 지원할 수 있을 정도로 커야 했습니다. 대신 최적화 된 코드에서 Deopt 포인트가 런타임에 호출하기 전에 Deopt ID를 직접로드하도록 TurboFan을 수정했습니다. 이를 통해 최적화 된 코드 크기가 약간 증가하면서 이 큰 점프 테이블을 완전히 제거 할 수 있었습니다.


결과 


V8의 마지막 7 개 릴리스에 대해 위에서 설명한 최적화를 릴리스했습니다. 일반적으로 라이트 모드에서 처음 시작된 후 나중에 기본 구성 V8로 설정되었습니다.


savings-by-release.svg 


breakdown-by-page.svg 


이 기간 동안 다양한 일반 웹 사이트에서 V8 힙 크기를 평균 18 % 줄였습니다. 이는 저가형 AndroidGo 모바일 장치의 평균 1.5MB 감소에 해당합니다. 이는 벤치 마크 또는 실제 웹 페이지 상호 작용에서 측정 된 JavaScript 성능에 큰 영향을 미치지 않으면 서 가능했습니다.


라이트 모드는 함수 최적화를 비활성화 하여 일부 비용으로 JavaScript 실행 처리량에 대한 추가 메모리 절약을 제공 할 수 있습니다. 평균 라이트 모드에서는 메모리가 22 % 절약되고 일부 페이지는 최대 32 % 감소합니다. 이는 AndroidGo 디바이스에서 V8 힙 크기가 1.8MB 감소한 것에 해당합니다.


breakdown-by-optimization.svg 


각 개별 최적화의 영향으로 나눠 질 때 각 페이지마다 이러한 최적화에서 서로 다른 비율의 혜택이 도출됩니다. 앞으로도 JavaScript 최적화시 V8의 메모리 사용량을 줄이면서도 빠른 속도를 유지할 수 있는 잠재적 인 최적화를 계속 식별 할 것입니다.



V8