JavaScriptでメモリ解放を制御しようとしています





JavaScriptにはメモリを割り当てる数千の方法がありますが、言語開発者はメモリを解放する権利を私たちから奪いました。 これは、ガベージコレクター(GC)によって行われます。GCには管理機能もありません。 ほとんどの場合、それは良い仕事をしますが、大量のデータが毎秒メガバイトのオーダーでプログラム内で連続的に解放されると、ガベージコレクターが愚かになる可能性があります。 この記事では、メモリの解放を高速化するためのいくつかの汚いトリックを紹介します。



問題の詳細



例としては、 ChromeFirefoxの拡張機能があります。これは、ビデオ(ライブブロードキャスト)がネットワークから継続的にダウンロードし、数メガバイトのサイズのバイナリデータの配列を処理およびリリースします。 拡張機能が機能するブラウザプロセスのワーキングセットメモリを見てください。 緑はChrome 57、赤はFirefox 52です。グラフ自体はperfmon2.msc perfmon2.msc



によって提供されperfmon2.msc













Firefoxでガベージコレクターがかなり良い仕事をすると、Chromeでは明らかに仕事から離職し、解雇を求めます。 1年前に写真が反対だったのは面白いです! ブラウザ、特にガベージコレクターのアルゴリズムは常に変化していますが、必ずしも良いとは限りません。 では、ブラウザの新しいバージョンのリリース後にコードを書き直しますか?



彼らは、私たちの時代には半分のギガバイトが種であり、スマートフォンであってもより多くのメモリがあることに反対するかもしれません。 まず、空きメモリ(ある場合)を使用して、明らかに不要なゴミではなく、オペレーティングシステムのキャッシュなどの有用なものを保存することを好みます。 第二に、ほとんどのブラウザはまだ32ビットです。つまり、アドレス空間が4ギガバイトよりも著しく小さいことを意味します。 拡張機能のいくつかの実行中のコピーは、すぐにそれを使い果たし、プロセスの「クラッシュ」またはビデオ再生の問題につながります。



ソリューションの検索



データはArrayBuffer



保存されArrayBuffer



。 このオブジェクトは、大量のバイナリデータを保存および操作するために特別に作成されました。 ただし、バッファに割り当てられたメモリを解放する機能、または少なくともバッファのサイズを変更する機能はありません。 2014年に、MozillaはArrayBuffer.transfer()



メソッドを追加することを提案しました。これにより、とりわけ、メモリを解放し、オブジェクトを切り離された状態のままにすることができました。 関数の簡単な実装にもかかわらず、他のブラウザの開発者は追加を拒否しました。 幸福はとても近かった...



ArrayBuffer.transfer()



は、主にasm.jsと連携して動作するように提案されました。 現在のバージョンのasm.jsの子孫であるWebAssemblyで、メモリ管理の状況を確認しました。 まさか、メモリ管理は計画内にあるだけです。



前述したように、オブジェクトに割り当てられたメモリを解放した後、このオブジェクトは切り離された状態に移行します。 実際にはどのように見えますか? シシュニクは、おそらくすぐにそれがヌルに置き換えられていると考えていました。 いいえ、「ダミーオブジェクト」は、 byteLength



プロパティが0の置換として機能し、バッファーのコンテンツにアクセスしようとすると(Firefoxで) TypeError: attempting to access detached ArrayBuffer



例外がスローされますTypeError: attempting to access detached ArrayBuffer



。 このようなダミーはメモリをほとんど使用しないため、ガベージコレクターはその処理にうまく対処します。



最新のブラウザにはすべて、関数postMessage()



ArrayBuffer



を分離状態に変換ArrayBuffer



ます。 確かに、バッファは解放されませんが、別のコンテキスト(iframeやワークフローなど)に渡されるため、メモリを解放するには追加の手順が必要です。 次に、異なる方法でpostMessage()



を呼び出す2つのトリックを示します。



メッセージチャンネルでトリック



MessageChannel



、コンテキスト間でデータを転送するように設計されています。 彼には2つのポートがあります。1つにデータを送信し、もう1つから受け入れます。 興味深い機能は受信ポートを閉じる機能です。 この場合、送信されるデータはどうなりますか? 次の2つのオプションがあります。





最初のオプションは間違いなく私たちに適しています。 確かに、「送信」という言葉は「すぐにリリースする」という意味ではなく、「不要としてマークする」という意味です。 後者の場合、リリースはガベージコレクターの次のサイクル中に行われますが、ガベージコレクターの開始時期は不明です。



実際には、混乱と揺れがあります。 Chrome 55およびFirefox 50では、メモリの割り当て解除が高速になります。 Firefox 51以降では、メモリはすぐに解放されます。 データがチャネル内に残っているため、Chrome 56はこのトリックを使用できません。



トリックのソースコードは次のとおりです。



 // HACK Firefox 49:   ,    asm.js //   ,     out of memory. const _ = (function() { let _ = null; function () { if (typeof  !== 'object' ||  === null) { return; } if (.buffer) {  = .buffer; } if (.byteLength) { console.log(`[]  ${.byteLength} `); if (!_) { _ = new MessageChannel(); _.port2.close(); } //  transferable   disentangled . _.port1.postMessage(, []); } } return {}; })();
      
      





そしてその使用:



 //  . //   ,   XMLHttpRequest, fetch  .. var  = new ArrayBuffer(1e6); //       ... //    . //          . _.();  = null;
      
      





このトリックが拡張機能の動作にどのように影響するかを見ていきます。 記事の冒頭の赤いグラフと比較してください。







最大メモリ消費量が100 MB減少しました。 退職の良い増加。 さらに、ビデオのビットレートやファイルのダウンロード頻度の増加などにより、メモリ消費が制御不能に増加しないという保証があります。



上記の互換性の問題のため、私はこのトリックが好きではありません。 それにもかかわらず、拡張機能でしばらく使用されています。



仕事の流れのトリック



Worker( Worker



)は、ページ(メインスレッド)のJavaScriptコードと並行して実行されるJavaScriptコードです。 バッファはWorker.postMessage()



メソッドWorker.postMessage()



移動されWorker.postMessage()



。 ただし、一人で移動するだけでは十分ではありません。 バッファーはワークフロー内を動き回り、ガベージコレクターの手がバッファーに到達するのを待ちます。 私の観察によると、ワークフローのガベージコレクターはページ上よりも怠laだからです。



利益を得るには、ワークフローを完了する必要があります 。 この手順の間、ブラウザはストリームに割り当てられたすべてのメモリをすぐに解放します。 これが標準で記述されているかどうかはわかりません。 比較的古いバージョンのChromeでこのトリックのパフォーマンスをテストしたことはありませんが、不快な驚きは期待していません。



CPU時間はワークフローの作成と完了に費やされるため、最適化の目的で、大量のデータ(私の場合は約10メガバイト)を蓄積した後にフローを完了する必要があります。



トリックソースコード:



 // HACK Firefox 49:   ,    asm.js //   ,     out of memory. const _ = (function() { const _ = 10e6; let _ = ''; let _ = null; let _ = 0; function () { if (typeof  !== 'object' ||  === null) { return; } if (.buffer) {  = .buffer; } if (.byteLength) { console.log(`[]  ${.byteLength} `); if (!_) { if (!_) { _ = URL.createObjectURL(new Blob( [` 'use strict'; self.onmessage = function() { if (!.data) { self.close(); } }; `], {type: 'application/javascript'} )); } _ = new Worker(_); } _ += .byteLength; _.postMessage(, []); if (_ > _) { (); } } } function () { if (_) { console.log(`[]  ${_} `); // terminate()  ,     //    . _.postMessage(null); _ = null; _ = 0; } } return {, }; })();
      
      





()



関数は、使用後にゴミ箱をクリアするために呼び出すことができます。 拡張機能では、関数はブロードキャストが完了した後に呼び出されます。



トリックを適用した後の結果を見てみましょう:







Firefoxの最大メモリ消費量は70 MB減少し、Chromeでは-310 MB減少しました。 コメントはありません。



更新: Firefoxのこのトリックにより、仮想アドレススペースリークが発生します。



性能



このような高速で移動するプロセスの時間を測定することは簡単な作業ではありません。 JavaScriptプロファイラーの機能は、精度が低く、テストされたコードがさまざまなコンテキストに広がり、その一部はすぐに破壊されるため、不十分です。 まず、質問に興味がありました。メモリを解放するためにコードを追加した後、拡張ランタイムが何パーセント増加するかです。



テストは次のように実施されました。 プロセッサは、省電力(CステートおよびIntelの周波数を下げる)をオフにしました。 拡張機能は最小化されたウィンドウで起動されました。 ビデオカードがビデオのデコードに関与しているため、ほとんどの時間プロセッサはアイドル状態でした。 Process Explorerで40分後に、拡張機能が実行されているプロセスは、消費されたプロセッサーサイクル数(CPUサイクル)をチェックしました。



どちらのトリックでも、測定誤差内で測定の数が変更されているため、速度について心配する必要はありません。 Firefoxの模擬テストでは、 MessageChannel



トリックはWorker



トリックよりも数倍遅いことがMessageChannel



しました。 まず、パフォーマンスは、同じプロセス内のコンテキスト間のデータ転送のブラウザーでの実装に依存します。 ところで、Chromeでは、 MessageChannel



パフォーマンスが最近上昇しました



結論



ご覧のとおり、かなり特殊な条件下ではありますが、説明したトリックは役立ちます。 JavaScriptを使用するほとんどの人は、幸いなことにJavaScriptを決して使用しません。



そして、この問題に興味がある人のために、もう1つヒントをあげます。「厚い」バッファをできるだけ少なく解放するようにしてください。 たとえば、拡張機能では、使用済みのバッファは捨てず、「バルコニー」に置きます。 データにメモリを割り当てる必要がある場合は、バルコニーが最初に検索され、可能であれば、サイズが必要以上に大きい場合でも、そこに見つかったバッファが使用されます。 私の場合、バルコニーは上記のトリックを使用せずにメモリ消費をほぼ半分に削減しました。



ソースのキリル文字について
  • ロシア語は私の母国語です。
  • 私は外国語が好きではありません(キャセロールも好きではありません)。
  • コードは私のために書かれました。お金のために、少なくともスワヒリ語で書きます。
  • 私は誰にも何も課しません。
  • まともな人は味について議論しません。
  • 私は1Cについての最初のきらめくジョークを待っています。





All Articles