最近、CppCon 2016でHerb Sutterによる「Leak Free C++ ...」についての素晴らしい講演を見ました。そこでは、スマートポインターを使用してRAII(リソースの取得は初期化)を実装することについて話しました。
今、私は疑問に思っていました。良いことのように思えるRAIIルールに厳密に従うとしたら、なぜC++でガベージコレクターを持っているのと違うのでしょうか? RAIIを使用すると、プログラマがリソースを再び解放するタイミングを完全に制御できることを知っていますが、いずれにしても、ガベージコレクターを使用するだけでは有益ですか?本当に効率が悪いのでしょうか?ガベージコレクターを使用すると、コード全体の小さなメモリを解放する代わりに、一度に大きなメモリチャンクを解放できるため、より効率的になると聞いたことがあります。
良いことのように思えるRAIIルールに厳密に従うと、なぜC++にガベージコレクターを置くのと違うのでしょうか?
どちらも割り当てを処理しますが、まったく異なる方法で処理します。 JavaのようなGCを参照している場合、独自のオーバーヘッドが追加され、リソース解放プロセスから決定性の一部が削除され、循環参照が処理されます。
ただし、特定のケースでは、パフォーマンス特性が大きく異なるGCを実装できます。ソケット接続を閉じるために、高性能/高スループットサーバーで1回実装しました(ソケットクローズAPIを呼び出すだけで時間がかかりすぎ、スループットパフォーマンスが低下しました)。これにはメモリは含まれませんが、ネットワーク接続と循環依存関係の処理は含まれません。
RAIIを使用すると、プログラマーがリソースを再び解放するタイミングを完全に制御できることを知っていますが、いずれにしても、ガベージコレクターを使用するだけで有益ですか?
この決定論は、GCが許可しない機能です。時々wantある時点の後、クリーンアップ操作(一時ファイルの削除、ネットワーク接続のクローズなど)が実行されたことを知ることができます。
そのような場合、GCはそれをカットしません。これがC#の理由です(たとえば)IDisposable
インターフェイスがあります。
ガベージコレクターを使用すると、コード全体の小さなメモリを解放する代わりに、一度に大きなメモリチャンクを解放できるため、効率が向上すると聞いたことがあります。
可能性は...実装に依存します。
ガベージコレクションは、RAIIでは解決できない特定のクラスのリソースの問題を解決します。基本的には、サイクルを手元で特定できない循環依存関係になります。
これには2つの利点があります。まず、RAIIでは解決できない特定の種類の問題が発生します。私の経験では、これらはまれです。
より大きなものは、プログラマーを怠zyにし、メモリリソースの寿命や遅延クリーンアップを気にしない特定のリソースについてnot careにすることです。特定の種類の問題を気にする必要がない場合は、more他の問題を気にすることができます。これにより、焦点を当てたい問題の部分に集中できます。
欠点は、RAIIがないと、寿命を制限したいリソースを管理するのが難しいことです。 GC言語では、基本的に、スコープがバインドされた非常にシンプルなライフタイムを持つか、Cのように手動でリソース管理を行う必要があります。それらのオブジェクトライフタイムシステムはGCに強く結びついており、大規模で複雑な(まだサイクルのない)システムの厳密なライフタイム管理にはうまく機能しません。
公平を期すために、C++のリソース管理は、このような大規模な(まだサイクルのない)システムで適切に行うために多くの作業を必要とします。 C#および同様の言語は、簡単なケースを簡単にするのと引き換えに、それを少し難しくします。
ほとんどのGC実装は、非局所性の本格的なクラスも強制します。一般的なオブジェクトの連続したバッファを作成したり、一般的なオブジェクトを1つの大きなオブジェクトに構成することは、ほとんどのGC実装で簡単にできることではありません。一方、C#では、機能が多少制限された値型struct
sを作成できます。現在のCPUアーキテクチャの時代では、キャッシュの使いやすさが重要であり、GCの局所性の欠如は大きな負担です。これらの言語の大部分はバイトコードランタイムを備えているため、理論的にはJIT環境は一般的に使用されるデータを一緒に移動できますが、多くの場合、C++と比較してキャッシュミスが頻繁に発生するため、パフォーマンスが均一に低下します。
GCの最後の問題は、割り当て解除が不確定であり、パフォーマンスの問題を引き起こす可能性があることです。最近のGCでは、これまでよりも問題が少なくなりました。
RAII はプログラミングのイディオムであり、 GC はメモリ管理手法です。そこで、リンゴとオレンジを比較しています。
ただし、RAIIをそのメモリ管理の側面 only に制限し、GCテクニックと比較できます。
いわゆるRAIIベースのメモリ管理手法(少なくともメモリリソースを考慮し、ファイルなどの他のリソースを無視する場合、実際には 参照カウント を意味する)と本物 ガベージコレクション テクニックは、 circular リファレンスの処理(for 巡回グラフ )。
参照カウントでは、それらのために特別にコーディングする必要があります( weak reference または他のものを使用して)。
多くの便利なケース(std::vector<std::map<std::string,int>>
のようなもの)では、参照カウントは暗黙的であり(0または1にしかできないため)、実際には省略されますが、contructorおよびdestructor関数(RAIIに必須)は、参照カウントビット(実際にはありません)。 std::shared_ptr
には、真の参照カウンターがあります。しかし、メモリはまだ暗黙的にmanually managed (new
およびdelete
がコンストラクターとデストラクター内でトリガーされます)が、 "暗黙的" delete
(デストラクタ内)は、自動メモリ管理の錯覚を与えます。ただし、new
およびdelete
への呼び出しは引き続き発生します(そして時間がかかります)。
ところで、GC implementation は、特別な方法で循環性を処理する場合がありますが、その負担はGCに任せます(たとえば、 Cheneyのアルゴリズムについて読む )。
一部のGCアルゴリズム(特に世代別コピーガベージコレクター)は、 individual オブジェクトのメモリを解放せず、コピー後に en masse を解放します。実際には、Ocaml GC(またはSBCLのもの)は、本物のC++ RAIIプログラミングスタイル( some ではなく、すべての種類のアルゴリズム)よりも高速です。
一部のGCは finalization (ファイルのような non-memory 外部リソースの管理に主に使用)を提供しますが、ほとんどの値を使用しないためメモリリソースのみを消費します)。欠点は、ファイナライズがタイミング保証を提供しないことです。実際には、ファイナライズを使用するプログラムはそれを最後の手段として使用しています(たとえば、ファイルのクローズは、ファイナライズの外でも、またそれらと共に、明示的に行われるべきです)。
GC(および少なくとも不適切に使用された場合はRAII)でも、メモリリークが発生する可能性があります。値が変数またはフィールドに保持されているが、将来使用されない場合。彼らはあまり頻繁に発生しません。
garbage collection handbook を読むことをお勧めします。
C++コードでは、 BoehmのGC または RavenbrookのMPS を使用するか、独自の トレースガベージコレクター をコーディングします。 。もちろん、GCの使用はトレードオフです(不確定性、タイミング保証の欠如など、いくつかの不便さがあります...)。
RAIIがすべての場合において記憶を処理する究極の方法だとは思いません。いくつかの場合、C++ 17で派手なRAIIスタイルでコーディングするよりも、純粋かつ効率的なGC実装(OcamlまたはSBCLのような)でプログラムをコーディングする方が簡単(開発)および高速(実行)になります。他の場合にはそうではありません。 YMMV。
例として、最も素晴らしいRAIIスタイルを使用してC++ 17でSchemeインタープリターをコーディングする場合、その内部に explicit GCをコーディング(または使用)する必要があります(スキームヒープには循環性があります)。そして、ほとんどの プルーフアシスタント は、多くの場合機能的なものであるGC対応言語でコーディングされています(C++でコーディングされているのは Lean だけです)正当な理由で。
ところで、私はそのようなC++ 17のSchemeの実装を見つけることに興味があります(しかし、自分でコーディングすることにあまり興味がありません)、できればマルチスレッド能力があります。
RAIIとGCは、まったく異なる方向で問題を解決します。一部の人が言うことにも関わらず、それらは完全に異なっています。
どちらも、リソースの管理が難しいという問題に対処します。ガベージコレクションは、開発者がこれらのリソースの管理にそれほど注意を払う必要がないようにすることでそれを解決します。 RAIIは、開発者がリソース管理に注意を払いやすくすることでそれを解決します。彼らが同じことをしていると言う人は誰でもあなたを売る何かを持っています。
言語の最近の傾向を見ると、率直に言って、パズルの両面が本当に必要なので、同じ言語で両方のアプローチが使用されていることがわかります。ほとんどのオブジェクトに注意を払う必要がないように、ガベージコレクションの種類を使用する多くの言語が表示されています。また、これらの言語は、本当にあなたのためにRAIIソリューション(pythonのwith
演算子など)も提供しますそれらに注意を払いたいです。
shared_ptr
を介してGCを提供します(refcountingとGCはどちらも寿命に注意を払う必要がないように設計されているため、同じクラスのソリューションであると主張できる場合)with
を介してRAIIを提供し、refcountingシステムとガベージコレクターを介してGCを提供しますIDisposable
およびusing
を介してRAIIを提供し、世代別ガベージコレクターを介してGCを提供しますパターンはあらゆる言語で現れています。
ガベージコレクターに関する問題の1つは、プログラムのパフォーマンスを予測することが難しいことです。
RAIIを使用すると、正確な時間にリソースが範囲外になり、メモリがクリアされ、時間がかかることがわかります。ただし、ガベージコレクター設定のマスターではない場合、クリーンアップがいつ行われるかを予測することはできません。
たとえば、大きなオブジェクトを解放できるため、GCを使用して小さなオブジェクトの束をより効率的に実行できますが、高速な操作ではなく、いつ発生するかを予測することは困難です。プロセッサ時間がかかり、プログラムのパフォーマンスに影響する可能性があります。
大ざっぱに言えば。 RAIIイディオムは、レイテンシーおよびジッターの方が優れている場合があります。ガベージコレクターは、システムのスループットの方が優れている場合があります。
ガベージコレクションとRAIIはそれぞれ、1つの一般的な構造をサポートしますが、他の構造は実際には適切ではありません。
ガベージコレクションシステムでは、コードは不変オブジェクト(文字列など)への参照を、そこに含まれるデータのプロキシとして効率的に処理できます。このような参照を渡すことは、「ダム」ポインタを渡すこととほぼ同じくらい安価で、所有者ごとにデータのコピーを個別に作成したり、データの共有コピーの所有権を追跡しようとするよりも高速です。さらに、ガベージコレクションシステムは、コンストラクターを一度変更する可能性のあるものへの参照をリークすることなく、可変オブジェクトを作成するクラスを記述し、必要に応じてデータを設定し、アクセサーメソッドを提供することにより、不変オブジェクトタイプを簡単に作成できます終了します。不変オブジェクトへの参照を広くコピーする必要があるが、オブジェクト自体はコピーしない場合、GCはRAIIに勝ちます。
一方、RAIIは、オブジェクトが外部エンティティから排他的なサービスを取得する必要がある状況の処理に優れています。多くのGCシステムでは、オブジェクトが「ファイナライズ」メソッドを定義し、破棄されたことがわかったときに通知を要求できますが、そのようなメソッドは、不要になった外部サービスをリリースすることがありますが、満足のいく方法を提供するのに十分な信頼性はほとんどありません外部サービスのタイムリーなリリースを保証します。代替不可能な外部リソースの管理については、RAIIがGCに勝っています。
GCが勝つ場合とRAIIが勝つ場合の主な違いは、GCは必要に応じて解放できる交換可能なメモリの管理に優れているが、代替不可能なリソースの処理には不向きであることです。 RAIIは、明確な所有権を持つオブジェクトの処理は得意ですが、含まれるデータ以外に実際のIDを持たない所有者のない不変のデータホルダーの処理は得意ではありません。
GCもRAIIもすべてのシナリオをうまく処理できないため、言語が両方のシナリオを適切にサポートすることは有益です。残念ながら、一方に焦点を当てた言語は、もう一方を後付けとして扱う傾向があります。
どちらが「有益」であるか「より効率的」であるかについての質問の主要部分は、多くのコンテキストを与え、これらの用語の定義について議論することなく答えることはできません。
それを超えて、基本的に古代の「JavaまたはC++はより良い言語ですか?」という緊張を感じることができます。コメントに火がつきます。この質問に対する「容認できる」答えはどのように見えるのだろうか、そして最終的にそれを見ることに興味があります。
しかし、おそらく重要な概念の違いに関する1つのポイントはまだ指摘されていません。RAIIでは、デストラクタを呼び出すスレッドに結び付けられています。アプリケーションがシングルスレッドの場合(そして、ハーブサッターが 無料昼食は終わりました :今日のほとんどのソフトウェアは事実上まだisシングル-スレッド化された)、単一のコアが実際のプログラムにもはや関係のないオブジェクトのクリーンアップの処理で忙しいかもしれません...
それとは対照的に、ガベージコレクターは通常、独自のスレッド、または複数のスレッドで実行されるため、他の部分の実行から(ある程度)切り離されます。
(注:いくつかの答えは、効率、パフォーマンス、遅延、およびスループットに言及したさまざまな特性を持つアプリケーションパターンをすでに指摘しようとしましたが、この特定のポイントはまだ言及されていませんでした)
「効率的」とは非常に広義の用語であり、開発努力の意味でRAIIは一般的にGCよりも効率的ではありませんが、パフォーマンスの点ではGCは通常RAIIよりも効率的ではありません。ただし、両方の場合にcontr-examplesを提供することは可能です。マネージ言語でリソース(割り当て)割り当てパターンが非常に明確な場合に汎用GCを処理するのは、RAIIを使用するコードが理由なくすべてにshared_ptr
を使用する場合に驚くほど効率が悪いように、かなり面倒です。
RAIIは、リソースとして記述可能なものをすべて一様に処理します。動的割り当てはそのようなリソースの1つですが、決して唯一のものではなく、おそらくnotが最も重要なリソースです。ファイル、ソケット、データベース接続、GUIフィードバックなどはすべて、RAIIで確定的に管理できるものです。
GCは動的割り当てのみを処理し、プログラムの存続期間中に割り当てられたオブジェクトの合計量を心配するプログラマーを解放します(ピーク時の同時割り当て量の調整のみを考慮する必要があります)
RAIIとガベージコレクションは、さまざまな問題を解決することを目的としています。
RAIIを使用する場合、オブジェクトをスタック上に残します。このオブジェクトの目的は、メソッドのスコープを離れるときに管理したいもの(ソケット、メモリ、ファイルなど)をクリーンアップすることだけです。これは、ガベージコレクションだけでなく、exception-safetyのためです。これが、ソケットのクローズやmutexの解放などについての応答を得る理由です。 (わかりましたので、私以外のミューテックスについては誰も言及しませんでした。)例外がスローされた場合、スタックの巻き戻しはメソッドによって使用されるリソースを自然にクリーンアップします。
ガベージコレクションはメモリのプログラムによる管理ですが、必要に応じて他の希少なリソースを「ガベージコレクション」することもできます。それらを明示的に解放することは、99%の時間の意味があります。ファイルやソケットなどにRAIIを使用する唯一の理由は、メソッドが戻ったときにリソースの使用が完了することを期待していることです。
ガベージコレクションは、heap-allocatedであるオブジェクトも処理します。たとえば、ファクトリーがオブジェクトのインスタンスを構築して返します。制御がスコープを離れる必要がある状況で永続オブジェクトを持つことは、ガベージコレクションを魅力的なものにします。ただし、ファクトリでRAIIを使用することもできます。そのため、戻る前に例外がスローされても、リソースをリークすることはありません。
ガベージコレクターを使用すると、コード全体の小さなメモリを解放する代わりに、一度に大きなメモリチャンクを解放できるため、より効率的になると聞いたことがあります。
これは完全に実行可能であり、実際、実際にはRAIIを使用して(または単純なmalloc/freeを使用して)実行されます。おわかりのように、必ずしもデフォルトのアロケーターを常に使用するとは限りません。特定のコンテキストでは、さまざまな種類の機能を持つカスタムアロケーターを使用します。アロケーターの中には、割り当てられた個々の要素を繰り返すことなく、すべてのアロケーター領域のすべてを一度に解放する機能が組み込まれています。
もちろん、その後、すべての割り当てをいつ解除するかという問題が発生します。これらのアロケーター(または関連付けられているスラブの使用)をRAII処理する必要があるかどうか、およびその方法はどうでしょうか。