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

はじめに

ウェブゲーム / ウェブアプリのパフォーマンスが一定時間経過後にどのように低下したかを示すメールが届き、Chrome のメモリ パフォーマンス ツールを開くまで、コードを掘り下げて、目立ったものは見られません。

メモリ タイムラインのスナップショット

同僚の一人が、メモリ関連のパフォーマンスの問題に直面して笑っていました。

メモリのグラフ表示を見ると、こののこぎり歯のパターンは、潜在的に重大なパフォーマンスの問題を示しています。メモリ使用量が増えると、タイムライン キャプチャのグラフ領域も拡大します。グラフが突然下降した場合は、ガベージ コレクタが実行され、参照先のメモリ オブジェクトがクリーンアップされたことを表します。

のこぎり歯の意味

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

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

JavaScript のメモリモデルは、ガベージ コレクタとして知られる技術上に構築されています。多くの言語では、システムのメモリヒープからのメモリの割り当てと解放を直接プログラマーが行います。しかし、ガベージ コレクタ システムは、プログラマーに代わってこのタスクを管理します。つまり、プログラマーが逆参照してもオブジェクトはメモリから直接解放されるのではなく、後で 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 がウェブに最適である理由の 1 つは、JavaScript が高速で楽しく、簡単に利用を開始できる言語だからです。その主な理由は、構文制限へのハードルが低いことと、ユーザーに代わってメモリの問題を処理することです。コーディング不要で、ダーティな処理は任せることができます。ただし、HTML5 ゲームのような高パフォーマンスのウェブ アプリケーションでは、GC が不可欠なフレームレートで減ってしまうことが多く、エンドユーザーのエクスペリエンスが低下します。計測とオブジェクト プールの採用を慎重に行うことで、フレームレートの負担を軽減し、その時間を優れた機能の開発に充てることができます。

ソースコード

オブジェクト プールの実装はウェブ上に多数存在するため、他の実装については説明しません。代わりに、これらを紹介しましょう。それぞれに具体的な実装の微妙な違いがあります。アプリケーションの使用ごとに固有の実装ニーズがあることを考慮すると、これは重要です。

参照