HN 日本語サマリー

← 一覧へ戻る
プログラミング

C++のための高性能ガベージコレクション

High-performance garbage collection for C++ (v8.dev)

47 pointsby motownphilly6 コメント

要約

Chromiumプロジェクトでは、JavaScriptだけでなくC++も多用されており、JavaScriptオブジェクトと密接に関連するC++オブジェクトのメモリ管理のためにOilpanというガベージコレクタが開発されました。OilpanはC++で書かれたガベージコレクタで、クロスコンポーネントトレーシングを用いてC++とJavaScriptのオブジェクトグラフを単一のヒープとして扱います。この記事では、Oilpanの基本原理、C++ API、そして特にスイーパーによるオブジェクトの並行回収について解説します。

全文翻訳

過去に、JavaScript、ドキュメントオブジェクトモデル(DOM)、そしてそれらがV8でどのように実装・最適化されているかについて書いてきました。しかし、Chromiumの全てがJavaScriptで書かれているわけではありません。ブラウザの大部分とその中のV8が埋め込まれているBlinkレンダリングエンジンはC++で書かれています。JavaScriptは、レンダリングパイプラインによって処理されるDOMと対話するために使用できます。 DOMを取り巻くC++オブジェクトグラフはJavaScriptオブジェクトと密接に絡み合っているため、Chromiumチームは数年前に、この種のメモリを管理するためにOilpanと呼ばれるガベージコレクタに切り替えました。Oilpanは、クロスコンポーネントトレーシングを使用してV8と接続できる、C++メモリを管理するためのC++で書かれたガベージコレクタであり、絡み合ったC++/JavaScriptオブジェクトグラフを単一のヒープとして扱います。 この記事は、Oilpanのコア原則とそのC++ APIの概要を提供するOilpanブログ記事シリーズの最初のものです。この記事では、サポートされている機能の一部を取り上げ、それらがガベージコレクタのさまざまなサブシステムとどのように相互作用するかを説明し、スイーパーによるオブジェクトの並行回収について深く掘り下げます。 最もエキサイティングなのは、Oilpanは現在Blinkに実装されていますが、ガベージコレクションライブラリの形式でV8に移行していることです。目標は、すべてのV8埋め込み者、そしてより一般的にはすべてのC++開発者がC++ガベージコレクションを容易に利用できるようにすることです。 背景 Oilpanはマーク・スイープガベージコレクタを実装しており、ガベージコレクションは2つのフェーズに分かれています。マークフェーズでは、管理対象ヒープをスキャンしてライブオブジェクトを見つけ、スイープフェーズでは、管理対象ヒープ上のデッドオブジェクトを回収します。 V8での並行マークの導入時に、マークの基本についてすでに説明しました。要約すると、すべてのオブジェクトをスキャンしてライブオブジェクトを見つけることは、グラフ探索と見なすことができ、オブジェクトはノード、オブジェクト間のポインタはエッジとなります。探索は、レジスタ、ネイティブ実行スタック(以下、スタックと呼びます)、およびその他のグローバルから開始されます。 C++はこの点においてJavaScriptと違いはありません。しかし、JavaScriptとは対照的に、C++オブジェクトは静的に型付けされており、実行時にその表現を変更することはできません。Oilpanを使用して管理されるC++オブジェクトは、この事実を活用し、ビジターパターンを介して他のオブジェクトへのポインタ(グラフのエッジ)の説明を提供します。Oilpanオブジェクトの基本的なパターンは次のとおりです。 class LinkedNode final : public GarbageCollected<LinkedNode> { public: LinkedNode(LinkedNode* next, int value) : next_(next), value_(value) {} void Trace(Visitor* visitor) const { visitor->Trace(next_); } private: Member<LinkedNode> next_; int value_; }; LinkedNode* CreateNodes() { LinkedNode* first_node = MakeGarbageCollected<LinkedNode>(nullptr, 1); LinkedNode* second_node = MakeGarbageCollected<LinkedNode>(first_node, 2); return second_node; } 上記の例では、LinkedNodeはGarbageCollected<LinkedNode>を継承していることからわかるように、Oilpanによって管理されています。ガベージコレクタがオブジェクトを処理する際、オブジェクトのTraceメソッドを呼び出すことで、そこから出るポインタを発見します。Member型は、Oilpanによって提供され、マーク中にグラフを探索する際に一貫した状態を維持するために使用される、std::shared_ptrなどに構文的に似たスマートポインタです。これらすべてにより、Oilpanは管理対象オブジェクト内のポインタがどこに存在するかを正確に把握できます。 熱心な読者はおそらく気づき、恐れているかもしれませんが、上記の例ではfirst_nodeとsecond_nodeはスタック上の生のC++ポインタとして保持されています。Oilpanはスタックの操作のための抽象化を追加せず、ルートを処理する際に管理対象ヒープへのポインタを見つけるために、保守的なスタック走査にのみ依存しています。これは、スタックを単語ごとに反復処理し、それらの単語を管理対象ヒープへのポインタとして解釈することによって機能します。これは、Oilpanがスタック割り当てオブジェクトへのアクセスにパフォーマンスペナルティを課さないことを意味します。代わりに、コストをガベージコレクションの時間に移動させ、そこでスタックを保守的に走査します。レンダラーに統合されたOilpanは、興味深いスタックがないことが保証される状態に達するまでガベージコレクションを遅延させようとします。Webはイベントベースであり、実行はイベントループでタスクを処理することによって駆動されるため、そのような機会は豊富にあります。 OilpanはBlinkで使用されており、これは大量の成熟したコードを持つ大規模なC++コードベースであり、したがって次のようなものもサポートしています。 ミックスインとそのミックスインへの参照(内部ポインタ)による複数継承。 コンストラクタの実行中にガベージコレクションをトリガーすること。 Persistentスマートポインタ(ルートとして扱われる)を介して、管理対象外メモリからオブジェクトを生存させること。 コレクションのバックエンドのコンパクションを伴う、シーケンシャル(例: vector)および連想(例: set、map)コンテナをカバーするコレクション。 弱参照、弱コールバック、エフェメラ。 個々のオブジェクトを回収する前に実行されるファイナライザコールバック。 C++のためのスイープ Oilpanでのマークの仕組みの詳細については、別のブログ記事にご期待ください。この記事では、マークが完了し、OilpanがTraceメソッドの助けを借りてすべての到達可能なオブジェクトを発見したと仮定します。マーク後、すべての到達可能なオブジェクトにはマークビットが設定されています。 スイープは、デッドオブジェクト(マーク中に到達不能だったオブジェクト)を回収し、それらの基盤となるメモリをオペレーティングシステムに返却するか、後続の割り当てに利用可能にするフェーズです。以下では、Oilpanのスイーパーが、使用法と制約の観点から、そして高い回収スループットをどのように達成するかを示します。 スイーパーは、ヒープメモリを反復処理し、マークビットをチェックすることによってデッドオブジェクトを見つけます。C++セマンティクスを維持するために、スイーパーはメモリを解放する前に各デッドオブジェクトのデストラクタを呼び出す必要があります。非自明なデストラクタはファイナライザとして実装されます。 プログラマの観点からは、スイーパーが使用する反復処理は構築順序を考慮しないため、デストラクタが実行される順序は定義されていません。これは、ファイナライザが他のオンヒープオブジェクトに触れることが許可されないという制約を課します。これは、管理対象言語がファイナライゼーションセマンティクス(例: Java)で順序をサポートしていないことが多いため、ファイナライゼーション順序を必要とするユーザーコードを作成する上での一般的な課題です。Oilpanは、Clangプラグインを使用して、オブジェクトの破棄中にヒープオブジェクトにアクセスされないことなどを静的に検証します。 class GCed : public GarbageCollected<GCed> { public: void DoSomething(); void Trace(Visitor* visitor) { visitor->Trace(other_); } ~GCed() { other_->DoSomething(); // error: Finalizer '~GCed' accesses // potentially finalized field 'other_'. } private: Member<GCed> other_; }; 好奇心旺盛な方のために: Oilpanは、オブジェクトが破棄される前にヒープにアクセスする必要がある複雑なユースケースのために、プリファイナライザコールバックを提供します。ただし、そのようなコールバックは、各ガベージコレクションサイクルでデストラクタよりも多くのオーバーヘッドを課すため、Blinkでは控えめに使用されています。 インクリメンタルおよびコンカレントスイープ 管理対象C++環境でのデストラクタの制約について説明したので、Oilpanがスイープフェーズをどのように実装し、最適化するかをより詳細に見ていきましょう。 詳細に入る前に、一般的にWeb上でプログラムがどのように実行されるかを思い出すことが重要です。JavaScriptプログラムだけでなく、ガベージコレクションなど、あらゆる実行は、イベントループでタスクをディスパッチすることによってメインスレッドから駆動されます。レンダラーは、他のアプリケーション環境と同様に、メインスレッドの作業を支援するために、メインスレッドと並行して実行されるバックグラウンドタスクをサポートしています。 単純に始めると、Oilpanは当初、アプリケーションのメインスレッドでの実行を中断するガベージコレクションの最終一時停止の一部として実行されるストップ・ザ・ワールド・スイープを実装していました。 ストップ・ザ・ワールド・スイープ ソフトリアルタイム制約を持つアプリケーションでは、ガベージコレクションに対処する際の決定要因はレイテンシです。ストップ・ザ・ワールド・スイープは、ユーザーに見えるアプリケーションレイテンシを引き起こす可能性のある大幅な一時停止時間を引き起こす可能性があります。アプリケーションがソフトリアルタイム制約を持つ場合、ガベージコレクションの決定要因はレイテンシです。ストップ・ザ・ワールド・スイープは、ユーザーに見えるアプリケーションのレイテンシを引き起こす可能性のある大幅な一時停止時間を引き起こす可能性があります。アプリケーションがソフトリアルタイム制約を持つ場合、ガベージコレクションの決定要因はレイテンシです。ストップ・ザ・ワールド・スイープは、ユーザーに見えるアプリケーションのレイテンシを引き起こす可能性のある大幅な一時停止時間を引き起こす可能性があります。