オブジェクト プールを使用した静的メモリ JavaScript

はじめに

ウェブゲームやウェブアプリのパフォーマンスが一定時間経過後に低下しているというメールを受け取ったとします。コードを調べても、特に目立った問題は見つかりません。しかし、Chrome のメモリ パフォーマンス ツールを開くと、次のような画面が表示されます。

思い出のタイムラインのスナップショット

同僚の一人が、メモリ関連のパフォーマンスの問題が発生していることに気づき、苦笑しています。

メモリグラフ ビューでは、こののこぎり歯のようなパターンは、パフォーマンスに重大な問題がある可能性を示しています。メモリ使用量が増加すると、タイムライン キャプチャのグラフ領域も増加します。グラフが急に下がっているのは、ガベージ コレクタが実行され、参照されているメモリ オブジェクトがクリーンアップされたときです。

ノコギリ波の意味

このようなグラフでは、ガベージ コレクション イベントが多数発生していることがわかります。これは、ウェブアプリのパフォーマンスに悪影響を及ぼす可能性があります。この記事では、メモリ使用量を管理してパフォーマンスへの影響を軽減する方法について説明します。

ガベージ コレクションとパフォーマンス コスト

JavaScript のメモリモデルは、ガベージ コレクタと呼ばれるテクノロジーを基盤としています。多くの言語では、プログラマーがシステムのメモリヒープからメモリを割り当て、解放する責任を直接負います。しかし、ガベージ コレクタ システムは、プログラマーに代わってこのタスクを管理します。つまり、プログラマーがオブジェクトの参照を解除しても、オブジェクトはメモリから直接解放されません。代わりに、GC のヒューリスティックが解放することが有益であると判断したときに解放されます。この判断プロセスでは、GC がアクティブなオブジェクトと非アクティブなオブジェクトに対して統計分析を実行する必要があります。これには、ある程度の時間がかかります。

ガベージ コレクションは、手動メモリ管理の対極として描かれることがよくあります。手動メモリ管理では、プログラマーが割り当てを解除してメモリシステムに戻すオブジェクトを指定する必要があります。

GC がメモリを再利用するプロセスは無料ではありません。通常、GC は作業を行うために一定の時間を要するため、利用可能なパフォーマンスが低下します。また、システム自体が実行のタイミングを決定します。このアクションを制御することはできません。GC パルスはコード実行中の任意のタイミングで発生する可能性があり、完了するまでコード実行がブロックされます。このパルスの持続時間は一般的に不明です。プログラムが特定の時点でメモリをどのように利用しているかによって、実行に時間がかかることがあります。

高パフォーマンス アプリケーションは、一貫したパフォーマンス境界に依存して、ユーザーにスムーズなエクスペリエンスを提供します。ガベージ コレクタ システムは、ランダムなタイミングでランダムな期間実行されるため、アプリケーションがパフォーマンスの目標を達成するために必要な時間を消費し、この目標を短絡させる可能性があります。

メモリチャーンを減らし、ガベージ コレクションの負荷を軽減する

前述のように、一連のヒューリスティックによって、パルスが有益となる十分な数の非アクティブなオブジェクトがあると判断されると、GC パルスが発生します。そのため、ガベージ コレクタがアプリケーションから奪う時間を減らす鍵は、オブジェクトの過剰な作成と解放をできる限り排除することにあります。オブジェクトを頻繁に作成、解放するこのプロセスは「メモリチャーン」と呼ばれます。アプリのライフタイム中にメモリチャーンを減らすことができれば、GC が実行に要する時間も短縮できます。つまり、作成および破棄されるオブジェクトの数を減らすか、なくす必要があります。実質的には、メモリの割り当てを停止する必要があります。

このプロセスにより、メモリグラフは次のように変化します。

思い出のタイムラインのスナップショット

の行を以下のように変更します。

静的メモリ JavaScript

このモデルでは、グラフに鋸歯状のパターンがなくなり、最初は大幅に増加し、その後は時間の経過とともに緩やかに増加していることがわかります。メモリチャーンが原因でパフォーマンスの問題が発生している場合は、このタイプのグラフを作成することをおすすめします。

静的メモリ JavaScript への移行

静的メモリ JavaScript は、アプリの開始時に、アプリのライフサイクルに必要なすべてのメモリを事前割り当てし、オブジェクトが不要になったときに実行中にそのメモリを管理する手法です。この目標を達成するには、次の簡単な手順を行います。

  1. アプリケーションを計測して、さまざまな使用シナリオで必要なライブメモリ オブジェクトの最大数(タイプ別)を特定します。
  2. コードを再実装して、その最大量を事前に割り当て、メインメモリにアクセスするのではなく、手動でフェッチ / リリースします。

実際には、1 を達成するには 2 を少し行う必要があるため、そこから始めましょう。

オブジェクト プール

簡単に言うと、オブジェクト プーリングとは、同じ型の未使用のオブジェクトのセットを保持するプロセスのことです。コードに新しいオブジェクトが必要な場合、システムのメモリヒープから新しいオブジェクトを割り当てるのではなく、プールから未使用のオブジェクトをリサイクルします。外部コードがオブジェクトを使い終わると、メインメモリに解放されるのではなく、プールに戻されます。オブジェクトはコードから参照解除(削除)されないため、ガベージ コレクションの対象になりません。オブジェクト プールを利用すると、メモリの制御をプログラマーの手に戻し、ガベージ コレクタがパフォーマンスに与える影響を軽減できます。

アプリケーションが保持するオブジェクト タイプは異種混合であるため、オブジェクト プールを適切に使用するには、アプリケーションの実行時にチャーン率が高いタイプごとに 1 つのプールを用意する必要があります。

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 が重要なフレームレートを低下させ、エンドユーザーのエクスペリエンスを損なうことがよくあります。慎重な計測とオブジェクト プールの採用により、フレームレートへの負担を軽減し、より素晴らしいもののために時間を有効活用できます。

ソースコード

オブジェクト プールの実装はウェブ上に多数存在するため、ここでは別の実装を紹介することはしません。代わりに、それぞれに固有の実装ニュアンスがある以下のリソースをご紹介します。アプリケーションの使用方法ごとに固有の実装ニーズがある可能性があることを考えると、これは重要なことです。

参照