객체 풀이 있는 정적 메모리 자바스크립트

Colt McAnlis
Colt McAnlis

소개

일정 시간이 지난 후 웹 게임/웹 앱의 성능이 저하되었다는 이메일을 받고 코드를 살펴보지만, Chrome의 메모리 성능 도구를 열고 다음을 확인할 때까지는 눈에 띄는 것이 없습니다.

추억 타임라인의 스냅샷

동료 중 한 명이 메모리 관련 성능 문제가 있다는 것을 깨닫고 웃습니다.

메모리 그래프 뷰에서 이 톱니 패턴은 잠재적으로 심각한 성능 문제를 나타냅니다. 메모리 사용량이 증가하면 타임라인 캡처에서 차트 영역도 증가합니다. 차트가 갑자기 하락하는 것은 가비지 컬렉터가 실행되어 참조된 메모리 객체를 정리한 경우입니다.

톱니의 의미

이와 같은 그래프에서는 가비지 컬렉션 이벤트가 많이 발생하여 웹 앱의 성능에 악영향을 미칠 수 있음을 확인할 수 있습니다. 이 도움말에서는 메모리 사용량을 제어하여 성능에 미치는 영향을 줄이는 방법을 설명합니다.

가비지 컬렉션 및 성능 비용

JavaScript의 메모리 모델은 가비지 컬렉터라는 기술을 기반으로 합니다. 많은 언어에서 프로그래머는 시스템의 메모리 힙에서 메모리를 할당하고 해제하는 작업을 직접 담당합니다. 하지만 가비지 컬렉터 시스템은 프로그래머를 대신하여 이 작업을 관리합니다. 즉, 프로그래머가 객체를 역참조할 때 객체가 메모리에서 직접 확보되는 것이 아니라 나중에 GC의 휴리스틱이 그렇게 하는 것이 유익하다고 판단할 때 확보됩니다. 이 결정 프로세스에서는 GC가 활성 및 비활성 객체에 관한 통계 분석을 실행해야 하며, 이 작업을 실행하는 데는 시간이 걸립니다.

가비지 컬렉션은 프로그래머가 할당 해제하고 메모리 시스템에 반환할 객체를 지정해야 하는 수동 메모리 관리와 반대되는 개념으로 묘사되는 경우가 많습니다.

GC가 메모리를 회수하는 프로세스는 무료가 아니며, 일반적으로 작업을 수행하는 데 일정 시간을 소요하여 사용 가능한 성능을 저하시킵니다. 이와 함께 시스템 자체에서 실행 시기를 결정합니다. 이 작업은 제어할 수 없으며, 코드 실행 중 언제든지 GC 펄스가 발생할 수 있으며, 완료될 때까지 코드 실행이 차단됩니다. 이 펄스의 지속 시간은 일반적으로 알 수 없으며, 특정 시점에 프로그램이 메모리를 활용하는 방식에 따라 실행하는 데 어느 정도 시간이 걸립니다.

고성능 애플리케이션은 일관된 성능 경계를 사용하여 사용자에게 원활한 환경을 제공합니다. 가비지 컬렉터 시스템은 임의의 시간에 임의의 기간 동안 실행될 수 있으므로 이 목표를 단축하여 애플리케이션이 실적 목표를 달성하는 데 필요한 가용 시간을 소모할 수 있습니다.

메모리 급변 감소, 가비지 컬렉션 세금 감소

앞서 언급했듯이, 휴리스틱 집합에서 비활성 객체가 충분히 많아 GC 펄스가 유용하다고 판단하면 GC 펄스가 발생합니다. 따라서 가비지 컬렉터가 애플리케이션에서 소요하는 시간을 줄이는 핵심은 과도한 객체 생성 및 해제 사례를 최대한 많이 제거하는 것입니다. 객체를 자주 생성하고 해제하는 이 프로세스를 '메모리 급변'이라고 합니다. 애플리케이션의 전체 기간 동안 메모리 급변을 줄일 수 있다면 GC가 실행에서 차지하는 시간도 줄일 수 있습니다. 즉, 생성 및 소멸되는 객체의 수를 삭제/감소해야 하며, 사실상 메모리 할당을 중지해야 합니다.

이 프로세스는 메모리 그래프를 다음에서 이동합니다.

추억 타임라인의 스냅샷

다음과 같이 바꿉니다.

정적 메모리 JavaScript

이 모델에서는 그래프에 더 이상 톱니 모양의 패턴이 없으며, 처음에는 크게 증가하다가 시간이 지남에 따라 천천히 증가하는 것을 볼 수 있습니다. 메모리 급변으로 인해 성능 문제가 발생하는 경우 이 유형의 그래프를 만들어야 합니다.

정적 메모리 JavaScript로 전환

정적 메모리 자바스크립트는 앱 시작 시 앱의 전체 수명 주기에 필요한 모든 메모리를 미리 할당하고, 실행 중에 더 이상 필요하지 않은 객체의 메모리를 관리하는 기법입니다. 몇 가지 간단한 단계를 통해 이 목표에 접근할 수 있습니다.

  1. 애플리케이션을 계측하여 다양한 사용 시나리오에 필요한 최대 라이브 메모리 객체 수(유형별)를 확인합니다.
  2. 최대 크기를 미리 할당하도록 코드를 다시 구현한 다음 기본 메모리로 이동하는 대신 수동으로 가져오거나 해제합니다.

실제로 1번을 달성하려면 2번을 어느 정도 수행해야 하므로 여기에서 시작해 보겠습니다.

객체 풀

간단히 말해 객체 풀링은 유형을 공유하는 사용되지 않는 객체 집합을 유지하는 프로세스입니다. 코드에 새 객체가 필요할 때 시스템 메모리 힙에서 새 객체를 할당하는 대신 풀에서 사용되지 않는 객체 중 하나를 재활용합니다. 외부 코드가 객체 사용을 완료하면 객체를 기본 메모리로 해제하는 대신 풀로 반환합니다. 객체가 코드에서 역참조(삭제)되지 않으므로 가비지 컬렉션되지 않습니다. 객체 풀을 활용하면 메모리 제어권을 프로그래머에게 다시 넘겨주어 가비지 컬렉터가 성능에 미치는 영향을 줄일 수 있습니다.

애플리케이션이 유지관리하는 객체 유형은 이기종 집합이므로 객체 풀을 적절하게 사용하려면 애플리케이션 런타임 중에 이탈률이 높은 유형별로 하나의 풀이 있어야 합니다.

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... do some stuff with the object that we need to do

gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference

대부분의 애플리케이션에서는 결국 새 객체를 할당해야 하는 수준에 도달하게 됩니다. 애플리케이션을 여러 번 실행하면 이 상한선이 어느 정도인지 파악할 수 있으며 애플리케이션 시작 시 해당 개수의 객체를 미리 할당할 수 있습니다.

객체 사전 할당

프로젝트에 객체 풀링을 구현하면 애플리케이션 런타임 중에 필요한 객체 수의 이론적 최댓값을 얻을 수 있습니다. 다양한 테스트 시나리오를 통해 사이트를 실행하면 필요한 메모리 요구사항 유형을 파악하고, 해당 데이터를 어딘가에 분류하고, 분석하여 애플리케이션의 메모리 요구사항 상한을 파악할 수 있습니다.

그런 다음 앱의 출시 버전에서 초기화 단계를 설정하여 모든 객체 풀을 지정된 양으로 미리 채울 수 있습니다. 이렇게 하면 모든 객체 초기화가 앱의 앞으로 푸시되고 실행 중에 동적으로 발생하는 할당량이 줄어듭니다.

function init() {
  //preallocate all our pools. 
  //Note that we keep each pool homogeneous wrt object types
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

선택하는 양은 애플리케이션의 동작과 많은 관련이 있습니다. 이론적 최댓값이 최선의 옵션이 아닐 때도 있습니다. 예를 들어 평균 최댓값을 선택하면 고급 사용자가 아닌 경우 메모리 사용량이 줄어들 수 있습니다.

만병통치약과는 거리가 먼

정적 메모리 증가 패턴이 유리할 수 있는 앱 분류가 있습니다. 하지만 Chrome DevRel Renato Mangini가 지적한 것처럼 몇 가지 단점이 있습니다.

결론

JavaScript가 웹에 적합한 이유 중 하나는 빠르고 재미있으며 쉽게 시작할 수 있는 언어이기 때문입니다. 이는 주로 문법 제한에 대한 장벽이 낮고 메모리 문제를 대신 처리하기 때문입니다. 코딩에만 집중하고 번거로운 작업은 Gemini에 맡기세요. 하지만 HTML5 게임과 같은 고성능 웹 애플리케이션의 경우 GC가 매우 중요한 프레임 속도를 잠식하여 최종 사용자의 경험을 저하시킬 수 있습니다. 신중한 계측과 객체 풀 채택을 통해 프레임 속도에 대한 부담을 줄이고 더 멋진 작업을 위해 시간을 확보할 수 있습니다.

소스 코드

웹에는 객체 풀의 구현이 많이 있으므로 또 다른 구현으로 지루하게 만들지 않겠습니다. 대신 각 애플리케이션 사용에 따라 특정 구현 요구사항이 있을 수 있으므로 구현 세부사항이 각각 다른 다음 리소스를 안내해 드리겠습니다.

참조