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


94

1秒あたり60回呼び出されるメインループを持つ、かなり複雑なJavaScriptアプリがあります。多くのガベージコレクションが行われているようです(Chrome開発ツールのメモリタイムラインからの「ノコギリ波」出力に基づく)。これは、アプリケーションのパフォーマンスに影響を与えることがよくあります。

そこで、ガベージコレクターが行う必要のある作業量を削減するためのベストプラクティスを調査しようとしています。(私がWebで見つけた情報のほとんどは、メモリリークを回避することを考慮しています。これは少し異なる質問です。メモリが解放されます。ガベージコレクションが多すぎるというだけです。)これは主にオブジェクトを可能な限り再利用することに帰着しますが、もちろん悪魔は詳細にあります。

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

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

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

ガベージコレクターが行う必要のある作業量を減らすために、どのようなテクニックを使用できますか?

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


2
あなたが私たちに示すことができるあなたのコードの例はありますか?質問の方が答えが簡単です(ただし、一般的ではない可能性もあるため、ここではわかりません)
John Dvorak

2
毎秒数千回関数の実行を停止するのはどうですか?これは本当にこれに取り組む唯一の方法ですか?この質問はXY問題のようです。あなたはXを記述したが、何が本当に求めているのY.を解決するされている
トラヴィスJ

2
@TravisJ:彼は毎秒60回しか実行しませんが、これは非常に一般的なアニメーションレートです。彼はより少ない仕事をするように求めませんが、どのようにそれをよりガベージコレクション効率的にするかを尋ねます。
ベルギ2013

1
@Bergi-「一部の関数は1秒あたり数千回呼び出される可能性があります」。これは1ミリ秒に1回です(おそらく悪い!)。それはまったく一般的ではありません。1秒あたり60回は問題になりません。この質問は曖昧すぎて、意見や推測を提示するだけです。
Travis J

4
@TravisJ-ゲームフレームワークでは、これは珍しいことではありません。
UpTheCreek 2013

回答:


127

他のほとんどのシナリオでは、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コンパイラなどの一部のコードミニファイアは、内部関数をインライン化でき、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です。ただし、それだけを使用する複雑な形式のパーサーは、読みにくいです。


JSON.parsedオブジェクトがメッセージ文字列よりも少ない(または等しい)スペースを割り当てると思いませんか?
Bergi 2013

@Bergi、それはプロパティ名に個別の割り当てが必要かどうかに依存しますが、解析ツリーの代わりにイベントを生成するパーサーは無関係な割り当てを行いません。
マイクサミュエル

素晴らしい答え、ありがとう!賞金の有効期限が切れたことについて多くの謝罪-そのとき私は旅行しており、何らかの理由で私のGmailアカウントでSOにログインできませんでした...:/
UpTheCreek

バウンティでの私の悪いタイミングを補うために、それを追加するためにさらに1つ追加しました(200は私が与えることができる最小値でした;)-何らかの理由で、それを授与する前に24時間待つ必要があります(たとえ「既存の回答に報酬を与える」を選択しました)。明日あなたのものになります...
UpTheCreek

@UpTheCreek、心配ありません。お役に立ててうれしいです。
マイクサミュエル

13

Chromeデベロッパーツールは、メモリの割り当てを追跡するために非常に便利な機能を持っています。それはメモリタイムラインと呼ばれています。 この記事では、いくつかの詳細について説明します。これが「のこぎり歯」について話していることだと思いますか?これは、ほとんどのGCランタイムの通常の動作です。割り当ては、使用量のしきい値に達してコレクションをトリガーするまで続行されます。通常、さまざまなしきい値でさまざまな種類のコレクションがあります。

Chromeのメモリタイムライン

ガベージコレクションは、その期間と共に、トレースに関連付けられたイベントリストに含まれます。私のかなり古いノートでは、一時的なコレクションが約4Mbで発生し、30msかかります。これは60Hzループの2回です。これがアニメーションの場合、30msのコレクションが原因でスタッターが発生している可能性があります。ここで、環境で何が行われているのかを確認する必要があります。つまり、コレクションのしきい値がどこにあるか、およびコレクションにかかる時間です。これは、最適化を評価するための参照ポイントを提供します。ただし、割り当て間隔を遅くしてコレクション間の間隔を長くすることにより、スタッターの頻度を減らすよりは効果的です。

次のステップは、プロファイルを使用することです。レコードタイプ別の割り当てのカタログを生成するためのレコードヒープ割り当て機能。これにより、トレース期間中に最も多くのメモリを消費しているオブジェクトタイプがすばやく表示されます。これは割り当て率に相当します。レートの降順でこれらに焦点を当てます。

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

ごみを少なくするために、ショットガンでアプリケーション全体を通して通常のコーディングスタイルを変更しないことをお勧めします。これは、速度を早めに最適化してはならないのと同じ理由によります。あなたの努力のほとんどに加えて、追加された複雑さとコードのあいまいさの多くは無意味になります。


そう、それがノコギリの意味です。私は常に何らかの鋸歯状のパターンがあることを知っていますが、私の懸念は、私のアプリでは鋸歯状の周波数と「崖」が非常に高いことです。興味深いことに、GCイベントは私のタイムライン上に表示されません- 「記録」枠(中央1)に表示される唯一のイベントは、次のとおりですrequest animation frameanimation frame firedcomposite layers。なぜ私がGC Eventあなたのように見えないのか分かりません(これはChromeの最新バージョンであり、カナリアでもあります)。
UpTheCreek 2013

4
私は「レコードヒープ割り当て」でプロファイラーを使用してみましたが、これまでのところ、あまり役に立たないことに気づきました。使い方が分からないからかもしれません。@342342andのように、私にとって何の意味もない参照がいっぱいあるようcode relocation infoです。
UpTheCreek 2013

9

一般的な原則として、ループを実行するたびに、できるだけ多くのキャッシュを作成し、作成と破棄を最小限に抑えます。

私の頭に浮かぶ最初のことは、メインループ内の匿名関数(ある場合)の使用を減らすことです。また、他の関数に渡されるオブジェクトを作成および破棄するという罠に陥るのも簡単です。私は決して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秒(アニメーションなど)スムーズに実行する必要があり、処理に時間がかかりますか?これが当てはまる場合、アニメーション全体でガベージコレクションが通常行われるオブジェクトを取得し、いくつかのグローバルオブジェクトでそれらへの参照を保持するのを見ることができます。その後、アニメーションが終了したら、すべての参照をクリアして、ガベージコレクターに処理を任せることができます。

これがすでに試して考えたものと比べて少しでも簡単な場合は、申し訳ありません。


この。さらに、他の関数(IIFEではない)内で言及されている関数も、大量のメモリを焼き、見逃しやすい一般的な不正使用です。
エサイリヤ2013

クリス、ありがとう!残念ながらダウンタ​​イムはありません:/
UpTheCreek

4

global scope(ガベージコレクターがオブジェクトにアクセスできないことが確実な場合)に1つまたはいくつかのオブジェクトを作成し、ローカル変数を使用する代わりに、ソリューションをリファクタリングして、これらのオブジェクトを使用してジョブを実行します。

もちろん、コードのどこでも実行できるわけではありませんが、一般的には、ガベージコレクターを回避するための私の方法です。

PSそれはコードのその特定の部分を少し保守しにくくするかもしれません。


GCは私のグローバルスコープ変数を一貫して取り出します。
VectorVortec、2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.