web-dev-qa-db-ja.com

Javascriptでガベージコレクターのアクティビティを減らすためのベストプラクティス

私は非常に複雑なJavascriptアプリを持っています。これには、1秒間に60回呼び出されるメインループがあります。多くのガベージコレクションが行われているようです(Chrome devツールのメモリタイムラインからの「のこぎり歯」出力に基づきます)-これは多くの場合、アプリケーションのパフォーマンスに影響します。

だから、私はガベージコレクターがしなければならない仕事の量を減らすためのベストプラクティスを研究しようとしています。 (私がウェブ上で見つけることができる情報のほとんどは、メモリリークの回避に関するものであり、これは少し異なる質問です-メモリが解放されます。ガベージコレクションが多すぎるというだけです。)ほとんどの場合、これは可能な限りオブジェクトを再利用することになりますが、もちろん悪魔は詳細にあります。

アプリは John ResigのSimple JavaScript Inheritance の行に沿って「クラス」で構成されています。

1つの問題は、いくつかの関数が1秒間に数千回呼び出される可能性があることです(メインループの各反復中に数百回使用されるため)、おそらくこれらの関数のローカル作業変数(文字列、配列など)問題かもしれません。

私はより大きな/より重いオブジェクトのオブジェクトプーリングを知っています(そしてこれをある程度使用します)が、特にタイトループで非常に頻繁に呼び出される機能に関連して、全面的に適用できる技術を探しています。

ガーベッジコレクターがしなければならない作業量を減らすためにどのようなテクニックを使用できますか?

そして、おそらく-どのオブジェクトが最もガーベッジコレクションされているかを特定するためにどのような手法を使用できますか? (これは非常に大きなコードベースであるため、ヒープのスナップショットを比較することはあまり有益ではありませんでした)

89
UpTheCreek

GCチャーンを最小限に抑えるために必要な多くのことは、他のほとんどのシナリオで慣用的なJSと見なされるものに反するため、アドバイスを判断する際にはコンテキストに留意してください。

割り当ては、いくつかの場所で現代の通訳で行われます:

  1. newまたはリテラル構文_[...]_または_{}_を介してオブジェクトを作成する場合。
  2. 文字列を連結するとき。
  3. 関数宣言を含むスコープを入力するとき。
  4. 例外をトリガーするアクションを実行するとき。
  5. 関数式を評価する場合:_(function (...) { ... })_。
  6. Object(myNumber)Number.prototype.toString.call(42)などのオブジェクトに強制する操作を実行するとき
  7. _Array.prototype.slice_のようなこれらのいずれかを内部で実行する組み込み関数を呼び出すとき。
  8. argumentsを使用してパラメーターリストを反映する場合。
  9. 文字列を分割するとき、または正規表現と一致するとき。

これらを避け、可能な場合はオブジェクトをプールして再利用します。

具体的には、次の機会を探してください。

  1. クローズドオーバー状態に依存しない、またはほとんど依存しない内部関数を、より高い、より長いスコープに引き出します。 ( Closure compiler などのコードミニファイヤの中には、内部関数をインライン化できるものがあり、GCのパフォーマンスが向上する場合があります。)
  2. 文字列を使用して構造化データを表すことや、動的なアドレス指定を避けます。特に、それぞれが複数のオブジェクト割り当てを必要とするため、splitまたは正規表現の一致を使用して繰り返し解析することは避けてください。これは、ルックアップテーブルと動的DOMノードIDへのキーで頻繁に発生します。たとえば、_lookupTable['foo-' + x]_とdocument.getElementById('foo-' + x)は、文字列の連結があるため、両方とも割り当てを伴います。多くの場合、キーを再連結する代わりに、長期間有効なオブジェクトに添付できます。サポートする必要のあるブラウザーによっては、 Map を使用して、オブジェクトを直接キーとして使用できる場合があります。
  3. 通常のコードパスで例外をキャッチしないでください。 try { op(x) } catch (e) { ... }の代わりに、if (!opCouldFailOn(x)) { op(x); } else { ... }を実行します。
  4. 文字列の作成を避けられない場合、例えばサーバーにメッセージを渡すには、_JSON.stringify_などのビルトインを使用します。これは、複数のオブジェクトを割り当てる代わりに、内部ネイティブバッファーを使用してコンテンツを蓄積します。
  5. 頻度の高いイベントにはコールバックを使用しないでください。可能な場合は、メッセージコンテンツから状態を再作成する長期間有効な関数(1を参照)をコールバックとして渡します。
  6. argumentsを使用する関数は、呼び出されたときに配列のようなオブジェクトを作成する必要があるため、使用しないでください。

_JSON.stringify_を使用して送信ネットワークメッセージを作成することをお勧めします。 _JSON.parse_を使用した入力メッセージの解析には、明らかに割り当てが含まれ、大きなメッセージの場合はその多くが含まれます。着信メッセージをプリミティブの配列として表現できる場合、多くの割り当てを節約できます。割り当てないパーサーを構築できる他の唯一の組み込み関数は_String.prototype.charCodeAt_です。ただし、それだけを使用する複雑な形式のパーサーは、読みにくいです。

122
Mike Samuel

Chrome開発者ツールには、メモリ割り当てをトレースするための非常に素晴らしい機能があります。これは、メモリタイムラインと呼ばれます。 この記事 いくつかの詳細を説明します。 「ノコギリ」の話?これは、ほとんどのGC実行時の通常の動作です。使用量のしきい値に達してコレクションをトリガーするまで、割り当ては続行されます。通常、異なるしきい値には異なる種類のコレクションがあります。

Memory Timeline in Chrome

ガベージコレクションは、トレースに関連付けられたイベントリストにその期間とともに含まれます。私のかなり古いノートブックでは、一時的なコレクションが約4MBで発生し、30msかかります。これは、60Hzループの反復の2つです。これがアニメーションである場合、30msのコレクションがおそらくスタッターを引き起こしています。ここから始めて、環境で何が起こっているのかを確認する必要があります。コレクションのしきい値はどこにあり、コレクションにかかっている時間です。これにより、最適化を評価するための参照ポイントが得られます。ただし、割り当てレートを遅くしてコレクション間の間隔を長くすることにより、スタッターの頻度を減らすよりも良い結果はおそらく得られません。

次のステップは、プロファイルを使用することです|レコードヒープ割り当て機能は、レコードタイプごとに割り当てのカタログを生成します。これにより、トレース期間中にどのオブジェクトタイプが最もメモリを消費しているかがすぐにわかります。これは割り当てレートに相当します。レートの降順にこれらに注目してください。

技術はロケット科学ではありません。ボックス化されていないオブジェクトでできる場合は、ボックス化されたオブジェクトを避けてください。繰り返しごとに新しいオブジェクトを割り当てるのではなく、グローバル変数を使用して、単一のボックス化されたオブジェクトを保持および再利用します。共通のオブジェクトタイプを放棄するのではなく、フリーリストにプールします。キャッシュ文字列の連結結果は、将来の反復で再利用される可能性があります。代わりに囲みスコープで変数を設定することにより、関数の結果を返すためだけに割り当てを避けてください。最適な戦略を見つけるには、各オブジェクトタイプを独自のコンテキストで考慮する必要があります。詳細についてサポートが必要な場合は、見ている課題の詳細を説明する編集を投稿してください。

散弾銃のごみの削減を試みて、アプリケーション全体で通常のコーディングスタイルを変更しないことをお勧めします。これは、速度を早めに最適化するべきではないのと同じ理由です。あなたの努力の大部分に加えて、追加された複雑さとコードの不明瞭さの多くは意味がありません。

12
Gene

一般的な原則として、できる限りキャッシュし、ループの実行ごとに作成と破棄をできるだけ行わないようにします。

頭に浮かぶ最初のことは、メインループ内での匿名関数(もしあれば)の使用を減らすことです。また、他の関数に渡されるオブジェクトを作成および破棄するというtrapに陥りやすいでしょう。私は決してjavascriptの専門家ではありませんが、私はこれを想像します:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

これよりもはるかに高速に実行されます。

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

プログラムのダウンタイムはありますか?たぶん、あなたはそれを1、2秒(例えば、アニメーションのために)スムーズに実行する必要があり、それから処理するためのより多くの時間がありますか?この場合、通常アニメーション全体でガベージコレクションされるオブジェクトを取得し、グローバルオブジェクトでそれらへの参照を保持することがわかります。その後、アニメーションが終了したら、すべての参照をクリアして、ガベージコレクターに作業を任せることができます。

これは、あなたがすでに試し、考えたことに比べて少し些細なことでしたらごめんなさい。

9
Chris B

global scope(ガベージコレクターがそれらに触れることを許可していないと確信している)で1つまたはいくつかのオブジェクトを作成し、それらのオブジェクトを使用してジョブを完了するためにソリューションをリファクタリングしようとします。ローカル変数を使用する代わりに。

もちろん、コードのどこでもそれを行うことはできませんでしたが、一般的にはガベージコレクターを避けるための私の方法です。

追伸コードの特定の部分の保守性が少し低下する可能性があります。

4
Mahdi