본문으로 건너뛰기

V8 메모리 소비 최적화

· 약 7분
V8 메모리 정화 엔지니어들 울란 데겐바에브, 마이클 리파우츠, 하네스 파이어, 그리고 툰 베르바에스트

메모리 소비는 자바스크립트 가상머신 성능상의 트레이드오프 공간에서 중요한 측면입니다. 지난 몇 달 동안 V8 팀은 현대 웹 개발 패턴을 대표하는 여러 웹사이트를 분석하고 메모리 사용량을 크게 줄였습니다. 이번 블로그 포스트에서는 우리가 분석에 사용한 작업과 도구를 소개하고, 가비지 컬렉터에서의 메모리 최적화를 개관하며, V8의 파서와 컴파일러에서 소비되는 메모리를 어떻게 줄였는지 보여줍니다.

벤치마크

V8를 프로파일링하고 가장 많은 사용자에게 영향을 미칠 최적화를 발견하기 위해선 재현 가능하고 의미 있으며 일반적인 실제 자바스크립트 사용 시나리오를 시뮬레이션할 수 있는 작업을 정의하는 것이 중요합니다. 이 작업을 위한 훌륭한 도구는 Telemetry이며, 이는 스크립트화된 웹사이트 상호작용을 Chrome에서 실행하고 모든 서버 응답을 기록하여 테스트 환경에서 예측 가능한 재생을 가능하게 합니다. 우리는 인기 있는 뉴스, 소셜, 미디어 웹사이트 세트를 선택하고 다음과 같은 공통 사용자 상호작용을 정의했습니다:

뉴스 및 소셜 웹사이트를 탐색하는 작업:

  1. 인기 있는 뉴스 또는 소셜 웹사이트(예: Hacker News)를 엽니다.
  2. 첫 번째 링크를 클릭합니다.
  3. 새 웹사이트가 로드될 때까지 기다립니다.
  4. 몇 페이지를 스크롤 다운합니다.
  5. 뒤로가기 버튼을 클릭합니다.
  6. 원래 웹사이트에서 다음 링크를 클릭하고 3-6 단계를 몇 번 반복합니다.

미디어 웹사이트를 탐색하는 작업:

  1. 인기 있는 미디어 웹사이트에서 항목(예: YouTube의 비디오)을 엽니다.
  2. 몇 초 동안 기다려 항목을 소비합니다.
  3. 다음 항목을 클릭하고 2-3 단계를 몇 번 반복합니다.

작업 방식이 캡처되면 이를 V8의 새 버전이 나올 때마다 Chrome의 개발 버전에 대해 필요한 만큼 재생할 수 있습니다. 재생하는 동안 V8의 메모리 사용량은 고정된 시간 간격으로 샘플링되어 의미 있는 평균을 얻을 수 있습니다. 벤치마크는 여기에서 확인할 수 있습니다.

메모리 시각화

성능 최적화에서 중요한 도전 과제 중 하나는 내부 VM 상태를 명확히 파악하여 진행 상황을 추적하거나 잠재적인 트레이드오프를 평가하는 것입니다. 메모리 소비를 최적화하려면 실행 중 V8의 메모리 소비를 정확히 추적하는 것이 중요합니다. 추적해야 할 메모리에는 V8의 관리 힙에 할당된 메모리와 C++ 힙에 할당된 메모리의 두 가지 범주가 있습니다. V8 힙 통계 기능은 V8 내부를 작업하는 개발자가 이 두 범주에 대해 깊은 통찰력을 얻도록 돕는 메커니즘입니다. Chrome(54 이상) 또는 d8 명령줄 인터페이스를 실행할 때 --trace-gc-object-stats 플래그가 지정되면 V8은 콘솔에 메모리 관련 통계를 덤프합니다. 우리는 V8 힙 시각화 도구를 제작하여 이 결과를 시각화했습니다. 이 도구는 관리 힙과 C++ 힙 모두에 대해 타임라인 기반의 뷰를 제공합니다. 또한 특정 내부 데이터 유형의 메모리 사용량과 각 유형에 대한 크기 기반 히스토그램에 대한 세부적인 분류도 제공합니다.

우리의 최적화 작업 진행 동안의 일반적인 워크플로는 타임라인 뷰에서 힙의 큰 부분을 차지하는 인스턴스 유형을 선택하는 것입니다. 그림 1에 묘사된 바와 같이 한 인스턴스 유형을 선택하면 도구는 해당 인스턴스 유형의 사용 분포를 보여줍니다. 이 예제에서는 V8의 내부 FixedArray 데이터 구조를 선택했습니다. 이는 VM의 다양한 장소에서 보편적으로 사용되는 타입 없는 벡터형 컨테이너입니다. 그림 2는 일반적인 FixedArray 분포를 나타냅니다. 여기서 우리는 대부분의 메모리가 특정 FixedArray 사용 시나리오에 기인할 수 있음을 확인할 수 있습니다. 이 경우 FixedArray는 희소 자바스크립트 배열(DICTIONARY_ELEMENTS)의 백업 저장소로 사용됩니다. 이러한 정보를 바탕으로 실제 코드로 돌아가 이러한 분포가 실제로 예상되는 동작인지 또는 최적화 기회가 존재하는지 확인할 수 있습니다. 우리는 도구를 사용하여 여러 내부 유형에서의 비효율성을 식별했습니다.

그림 1: 관리 힙과 오프 힙 메모리의 타임라인 뷰

그림 2: 인스턴스 유형 분포

그림 3은 C++ 힙 메모리 소비를 보여줍니다. 이는 주로 V8에서 사용되는 일시적인 메모리 영역인 존 메모리로 구성됩니다. 존 메모리는 V8 파서 및 컴파일러에 의해 가장 광범위하게 사용되므로, 메모리 소비 스파이크는 구문 분석 및 컴파일 이벤트와 일치합니다. 메모리를 더 이상 필요로 하지 않을 때 곧바로 해제되는 잘 동작하는 실행은 스파이크만을 포함합니다. 반면에 플래토(즉, 더 긴 시간 동안 높은 메모리 소비)가 나타나는 경우 최적화 여지가 있음을 나타냅니다.

그림 3: 존 메모리

얼리 어답터는 Chrome의 추적 인프라에 통합된 기능을 시도해볼 수도 있습니다. 이를 위해서는 --track-gc-object-stats 플래그를 사용하여 최신 Chrome Canary를 실행한 다음 추적을 캡처하고 카테고리에 v8.gc_stats를 포함해야 합니다. 데이터는 이후 V8.GC_Object_Stats 이벤트 하에 표시됩니다.

JavaScript 힙 크기 감소

가비지 컬렉션 처리량, 지연 시간, 메모리 소비 간에는 본질적인 트레이드오프가 존재합니다. 예를 들어, 가비지 컬렉션 지연 시간(사용자에게 지연 현상이 보임)은 자주 가비지 컬렉션을 발생시키는 것을 피하기 위해 더 많은 메모리를 사용하여 줄일 수 있습니다. 메모리가 적은 모바일 장치, 즉 RAM 용량이 512MB 이하인 장치에서는 지연 시간과 처리량을 메모리 소비보다 우선시하면 메모리 부족 충돌 및 Android에서 탭이 중단되는 결과를 초래할 수 있습니다.

이러한 메모리가 적은 모바일 장치에 맞는 적절한 트레이드오프를 더 잘 균형 잡기 위해, JavaScript 가비지 컬렉션 힙 메모리 소비를 줄이도록 여러 가비지 컬렉션 휴리스틱을 조정하는 특별 메모리 감소 모드를 도입했습니다.

  1. 전체 가비지 컬렉션이 끝날 때, V8의 힙 성장 전략은 일정한 여유 공간과 함께 라이브 오브젝트 양을 기반으로 다음 가비지 컬렉션 시점을 결정합니다. 메모리 감소 모드에서는 V8이 여유 공간을 덜 사용하므로 더 자주 가비지 컬렉션이 발생하여 메모리 소비가 줄어듭니다.
  2. 또한, 이 추정치는 엄격한 한도로 간주되며, 미완성 증분 마킹 작업이 메인 가비지 컬렉션 중지 동안 완료되도록 강요합니다. 메모리 감소 모드가 아닐 경우, 미완성 증분 마킹 작업으로 인해 이 한도를 임의로 초과하여 마킹이 완료되었을 때에만 메인 가비지 컬렉션 중지가 발생할 수 있습니다.
  3. 메모리 조각화는 더 공격적으로 메모리 압축을 수행함으로써 더욱 줄어듭니다.

그림 4는 Chrome 53 이후, 메모리가 적은 장치에서 이루어진 몇 가지 개선 사항을 보여줍니다. 특히, 모바일 뉴욕타임즈 벤치마크의 평균 V8 힙 메모리 소비가 약 66% 줄었습니다. 전반적으로 이 벤치마크 세트에서 평균 V8 힙 크기는 50% 감소했습니다.

그림 4: Chrome 53 이후 저메모리 장치에서 V8 힙 메모리 감소

최근 도입된 또 다른 최적화는 저메모리 장치뿐 아니라 메모리가 넉넉한 모바일 및 데스크탑에서도 메모리를 줄일 수 있습니다. V8 힙 페이지 크기를 1MB에서 512kB로 줄이면 라이브 오브젝트가 많지 않을 경우 더 작은 메모리 풋프린트를 가지며, 전반적으로 최대 2배 낮은 메모리 조각화를 제공합니다. 또한 메모리 압축 스레드가 더 많은 일을 수행할 수 있도록 더 작은 작업 청크로 인해 더 많은 압축 작업을 수행할 수 있게 됩니다.

존 메모리 감소

JavaScript 힙과 더불어 V8은 내부 VM 작업을 위해 힙 외 메모리도 사용합니다. 가장 큰 메모리는 _존_이라고 불리는 메모리 영역을 통해 할당됩니다. 존은 모든 존 할당 메모리가 존이 삭제될 때 한꺼번에 해제되는 고속 할당 및 대량 해제를 가능하게 하는 지역 기반 메모리 할당자 유형입니다. 존은 V8의 파서와 컴파일러 전반에서 사용됩니다.

Chrome 55의 주요 개선 사항 중 하나는 백그라운드 구문 분석 중 메모리 소비를 줄이는 것입니다. 백그라운드 구문 분석은 페이지가 로드되는 동안 V8이 스크립트를 구문 분석할 수 있게 해줍니다. 메모리 시각화 도구를 통해 배경 구문 분석기가 코드가 이미 컴파일된 이후에도 전체 존을 계속 활성 상태로 유지한다는 사실을 발견했습니다. 컴파일 후 즉시 존을 해제함으로써 존의 수명을 크게 줄였고, 평균 및 최대 메모리 사용량이 줄어들었습니다.

또 다른 개선 사항은 파서가 생성한 추상 구문 트리 노드의 필드를 더 잘 압축하는 데서 비롯됩니다. 이전에는 가능한 경우 필드를 묶어 압축하는 작업을 C++ 컴파일러에 의존했습니다. 예를 들어, 두 개의 불(boolean)은 단지 두 비트만 필요하며 하나의 워드 내에 위치하거나 이전 워드의 사용되지 않은 부분 내에 존재할 수 있습니다. C++ 컴파일러는 항상 가장 압축된 패킹을 찾지는 못하기 때문에 대신 수동으로 비트를 패킹합니다. 이는 정점 메모리 사용량 감소뿐만 아니라 파서 및 컴파일러 성능 개선을 가져옵니다.

그림 5는 Chrome 54 이후 약 40%의 평균 감소를 기록하며 측정된 웹사이트에서 피크 존 메모리 개선을 보여줍니다.

그림 5: 데스크톱용 Chrome 54 이후 V8 피크 존 메모리 감소

앞으로 몇 달 동안 V8의 메모리 풋프린트를 줄이기 위한 작업을 계속 진행할 예정입니다. 파서에 대한 추가적인 존 메모리 최적화를 계획하고 있으며 512 MB – 1 GB 메모리를 가진 기기에 집중할 계획입니다.

업데이트: 위에서 논의된 모든 개선 사항은 Chrome 55가 _저메모리 장치_에서 Chrome 53에 비해 전체 메모리 소비를 최대 35%까지 줄이는 데 도움이 됩니다. 다른 기기 세그먼트는 존 메모리 개선만 혜택을 받습니다.