객체 풀을 사용하는 정적 메모리 자바스크립트

Colt McAnlis
Colt McAnlis

소개

그래서 일정 시간이 지난 후 웹 게임 / 웹 앱의 성능이 어떻게 떨어지는지 알리는 이메일을 받습니다. 코드를 자세히 살펴보았는데 Chrome의 메모리 성능 도구를 열고 다음을 확인할 때까지 눈에 띄는 부분이 하나도 발견되지 않았습니다.

메모리 타임라인의 스냅샷

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

메모리 그래프 뷰에서 이 톱니 모양 패턴은 잠재적으로 심각한 성능 문제를 알려줍니다. 메모리 사용량이 증가함에 따라 타임라인 캡처의 차트 영역도 늘어납니다. 차트가 갑자기 떨어지면 가비지 컬렉터가 실행되고 참조된 메모리 객체를 정리한 인스턴스입니다.

톱니 모양의 의미

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

가비지 컬렉션 및 성능 비용

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

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

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

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

메모리 이탈 감소, 가비지 컬렉션 세금 절감

앞서 설명했듯이, 일련의 휴리스틱이 펄스가 유리할 만큼 충분한 비활성 객체가 있다고 판단하면 GC 펄스가 발생합니다. 따라서 가비지 컬렉터가 애플리케이션에서 처리하는 시간을 줄이기 위한 핵심은 최대한 많은 객체 생성 및 해제를 제거하는 것입니다. 객체를 생성하거나 해제하는 이 프로세스를 '메모리 급변'이라고 합니다. 애플리케이션이 실행되는 동안 메모리 급변을 줄일 수 있다면 GC 실행에 걸리는 시간도 줄일 수 있습니다. 즉, 생성 및 소멸된 객체의 수를 삭제하거나 줄여야 하며, 사실상 메모리 할당을 중지해야 합니다.

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

메모리 타임라인의 스냅샷

다음과 같이 변경합니다.

정적 메모리 자바스크립트

이 모델에서는 그래프에 더 이상 톱니형(Sawtooth) 같은 패턴이 나타나지 않고 초반에 크게 증가했다가 시간이 지남에 따라 서서히 증가함을 알 수 있습니다. 메모리 급변으로 인해 성능 문제가 발생하는 경우, 이 그래프 유형을 생성해야 합니다.

정적 메모리 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가 지적했듯이 몇 가지 단점이 있습니다.

결론

자바스크립트가 웹에 적합한 이유 중 하나는 빠르고, 재미있고, 사용하기 쉬운 언어이기 때문입니다. 이는 주로 구문 제한에 대한 낮은 장벽과 사용자를 대신하여 메모리 문제를 처리하기 때문입니다. 코딩을 하여 더러운 작업을 처리하게 할 수 있습니다. 하지만 HTML5 게임과 같은 고성능 웹 애플리케이션의 경우 GC는 종종 매우 필요한 프레임 속도를 소모하여 최종 사용자의 경험을 저하시킬 수 있습니다. 몇 가지 신중한 계측과 객체 풀을 채택하면 프레임 속도의 이러한 부담을 줄이고 더 멋진 작업을 위해 그 시간을 확보할 수 있습니다.

소스 코드

웹에는 개체 풀이 떠다니는 구현이 많기 때문에 다른 방법은 지루하지 않겠습니다. 대신 이러한 설정을 안내해 드리도록 하겠습니다. 각 애플리케이션에는 구체적인 구현 니즈가 있을 수 있다는 점을 감안하여 구현하는 것이 중요합니다.

참조