1. ホーム
  2. javascript

[解決済み] Javascriptでガベージコレクタの活動を減らすためのベストプラクティス

2022-11-06 17:26:07

質問

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

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

このアプリは次のような「クラス」で構成されています。 John Resig のシンプルな JavaScript 継承 .

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

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


ガベージコレクターが行わなければならない仕事の量を減らすために、どのようなテクニックを使うことができますか?

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

どのように解決するのですか?

GC回転を最小化するために必要なことの多くは、他のほとんどのシナリオで慣用的なJSと考えられていることに反するので、私が与える助言を判断するときは文脈を念頭に置いてください。

最近のインタプリタでは、いくつかの場所で割り振りが行われます。

  1. オブジェクトを作成する際に new またはリテラル構文で [...] または {} .
  2. 文字列を連結するとき。
  3. 関数宣言を含むスコープに入ったとき。
  4. 例外を発生させるアクションを実行したとき。
  5. 関数式を評価したとき。 (function (...) { ... }) .
  6. のようにObjectに強制するような操作を行った場合。 Object(myNumber) または Number.prototype.toString.call(42)
  7. のように、これらのいずれかを行うビルトインを呼び出すと、そのビルトインは自動的に Array.prototype.slice .
  8. を使用する場合 arguments を使ってパラメータリストの上に反映させることができます。
  9. 文字列を分割したり、正規表現でマッチするとき。

これらを行わないようにし、可能な限りオブジェクトをプールして再利用しましょう。

具体的には、以下のような機会に気をつけましょう。

  1. クローズド オーバーの状態への依存がない、または少ない内部関数を、より高い、より長い寿命のスコープに引き出します。 (いくつかのコードミニファイアー、たとえば クロージャコンパイラ のようなコードミニファイラーは内部関数をインライン化することができ、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 . これだけを使用する複雑なフォーマットのパーサーは、読むのが大変になりそうですが。