はじめに
そうすると、ウェブゲーム / ウェブアプリのパフォーマンスが一定時間経過した後で届いたというメールが届きました。コードをいろいろ見て、Chrome のメモリ パフォーマンス ツールを開くと、目立ったものが何もありませんでした。結果は次のようになります。
同僚の 1 人が、メモリ関連のパフォーマンスの問題があることに気づいて笑う。
メモリグラフビューでは、この鋸歯状のパターンは、重大なパフォーマンスの問題の可能性を示す非常に有用な情報です。メモリ使用量が増加すると、タイムライン キャプチャのグラフ領域も拡大します。チャートが急激に落ち込む場合は、Garbage Collector が実行され、参照されているメモリ オブジェクトがクリーンアップされたインスタンスです。
このようなグラフでは、ウェブアプリのパフォーマンスに悪影響を及ぼす可能性がある、ガベージ コレクション イベントが多数発生していることがわかります。この記事では、メモリ使用量を管理してパフォーマンスへの影響を軽減する方法について説明します。
ガベージ コレクションとパフォーマンスの費用
JavaScript のメモリモデルは、ガベージ コレクタと呼ばれる技術に基づいています。多くの言語では、システムのメモリヒープからメモリの割り当てと解放をプログラマが直接行います。一方、ガベージ コレクタ システムは、プログラマーに代わってこのタスクを管理します。つまり、プログラマーがオブジェクトの参照を解除したときにオブジェクトがメモリから直接解放されるのではなく、GC のヒューリスティックが解放が有益であると判断した後で解放されます。この意思決定プロセスでは、GC がアクティブ オブジェクトと非アクティブ オブジェクトに対して統計分析を実行する必要があります。この処理には時間がかかります。
ガベージ コレクションは、手動メモリ管理の反対として説明されることがよくあります。手動メモリ管理では、プログラマがどのオブジェクトを割り当て解除してメモリ システムに戻すかを指定する必要があります。
GC がメモリを再利用するためのプロセスは無料ではなく、通常は処理を行うために一定の時間を要するため、利用可能なパフォーマンスが低下します。また、実行するタイミングはシステム自体が決定します。このアクションは制御できません。GC パルスはコード実行中にいつでも発生する可能性があり、完了するまでコード実行がブロックされます。このパルスの所要時間は通常不明です。実行には、プログラムが特定の時点でメモリをどのように使用しているかによって、ある程度の時間がかかります。
高パフォーマンス アプリケーションは、一貫したパフォーマンス境界に依存して、ユーザーにスムーズなエクスペリエンスを提供します。ガベージ コレクタ システムは、ランダムな時間にランダムな時間実行されるため、この目標を短絡する可能性があります。これにより、アプリケーションがパフォーマンス目標を達成するために必要な使用可能な時間が短縮されます。
メモリの切り替えを減らし、ガベージ コレクションの負荷を軽減する
前述のように、GC パルスは、一連のヒューリスティクスによって、パルスが有益なほど十分な数の非アクティブ オブジェクトが存在すると判断された場合に発生します。そのため、ガベージ コレクタがアプリケーションで費やす時間を短縮する鍵は、オブジェクトの過剰な作成と解放をできるだけ排除することです。オブジェクトの作成と解放を頻繁に行うこのプロセスは「メモリチャーン」と呼ばれます。アプリケーションの存続期間中にメモリチャーンを削減できれば、GC の実行にかかる時間も短縮できます。つまり、作成および破棄されるオブジェクトの数を削除または減らす必要があります。つまり、メモリの割り当てを停止する必要があります。
このプロセスにより、メモリグラフは次のように移動します。
の行を以下のように変更します。
このモデルでは、グラフに鋸歯状のパターンはなく、最初は急激に増加し、その後は時間の経過とともにゆっくりと増加していることがわかります。メモリチャーンが原因でパフォーマンスの問題が発生している場合は、この種類のグラフを作成します。
静的メモリ JavaScript への移行
静的メモリ JavaScript は、アプリの起動時に、アプリの存続期間に必要なすべてのメモリを事前に割り当てし、オブジェクトが不要になったときに実行中にそのメモリを管理する手法です。この目標を達成するには、次の簡単な手順を行います。
- さまざまなユースケースで必要なライブメモリ オブジェクトの最大数(タイプごと)を特定するように、アプリを計測します。
- コードを再実装して、その最大量を事前に割り当て、メインメモリにアクセスせずに手動でフェッチ / 解放します。
実際には、#1 を達成するには #2 を少し行う必要があります。まずは #2 から始めましょう。
オブジェクト プール
簡単に言うと、オブジェクト プーリングは、タイプを共有する未使用のオブジェクトのセットを保持するプロセスです。コードに新しいオブジェクトが必要な場合は、システムのメモリヒープから新しいオブジェクトを割り当てるのではなく、未使用のオブジェクトの 1 つをプールからリサイクルします。外部コードがオブジェクトの処理を完了すると、メインメモリに解放されるのではなく、プールに返されます。オブジェクトはコードから参照解除(削除)されることがないため、ガベージ コレクションは行われません。オブジェクト プールを使用すると、メモリの制御をプログラマに戻すことができ、ガベージ コレクタがパフォーマンスに与える影響を軽減できます。
アプリが保持するオブジェクトタイプは異種であるため、オブジェクト プールを適切に使用するには、アプリの実行中に頻繁に変更されるタイプごとに 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 によって重要なフレームレートが低下し、エンドユーザー エクスペリエンスが低下することがあります。慎重な計測とオブジェクト プールの導入により、フレームレートに対するこの負荷を軽減し、その時間をより優れたものに活用できます。
ソースコード
オブジェクト プールの実装はウェブ上にたくさん公開されているので、ここでは別の実装を説明しません。代わりに、それぞれの実装の微妙な違いについてご説明します。アプリケーションの使用ごとに異なる実装ニーズがあることを考慮すると、これは重要です。
- Gamecore.js のオブジェクト プール
- Beej のオブジェクト プール
- Emehrkay の超シンプルなオブジェクト プール
- Steven Lambert のゲームに特化したオブジェクトプール
- RenderEngine の objectPool の設定