web-dev-qa-db-ja.com

C#に参照カウント+ガベージコレクションがないのはなぜですか?

私はC++の経歴を持っていて、C#で約1年働いています。他の多くの人と同じように、なぜ決定論的なリソース管理が言語に組み込まれていないのかについて、私は頭がいっぱいです。確定的なデストラクタの代わりに、disposeパターンを使用します。 人々は疑問に思い始めます IDisposable癌を彼らのコードを通して広めることは努力の価値があるかどうか。

私のC++バイアスの脳では、確定的デストラクタで参照カウントスマートポインタを使用することが、IDisposableを実装して非メモリリソースをクリーンアップするためにdisposeを呼び出す必要があるガベージコレクタからの主要なステップアップのようです。確かに、私はあまり頭が良くありません...だから、物事がどうあるべきかをよりよく理解したいという欲求から純粋にこれを求めています。

C#が次のように変更された場合:

オブジェクトは参照カウントされます。オブジェクトの参照カウントが0になると、リソースクリーンアップメソッドがオブジェクトに対して確定的に呼び出され、オブジェクトにガベージコレクションのマークが付けられます。ガベージコレクションは、将来の非決定的な時点で発生し、その時点でメモリが解放されます。このシナリオでは、IDisposableを実装したり、Disposeを呼び出したりする必要はありません。解放する非メモリリソースがある場合は、リソースクリーンアップ関数を実装するだけです。

  • なぜそれが悪い考えなのですか?
  • それはガベージコレクターの目的を無効にするでしょうか。
  • そのようなことを実装することは可能でしょうか?

編集:これまでのコメントから、これは悪い考えです

  1. GCは参照カウントなしで高速です
  2. オブジェクトグラフのサイクルを処理する問題

1番目は有効だと思いますが、2番目は弱参照を使用して簡単に処理できます。

速度の最適化はあなたの短所を上回ります:

  1. 非メモリリソースを適時に解放しない可能性があります
  2. 非メモリリソースをすぐに解放する可能性があります

リソースのクリーンアップメカニズムが確定的であり、言語に組み込まれている場合は、それらの可能性を排除できます。

55
Skrymsli

Brad Abramsが投稿しました Brian Harryからの電子メール .Netフレームワークの開発中に書かれました。初期の優先事項の1つが参照カウントを使用するVB6とのセマンティックの同等性を維持することであった場合でも、参照カウントが使用されなかった理由の多くを詳しく説明します。一部のタイプの参照をカウントして他のタイプのカウントをしない(IRefCounted!)、または特定のインスタンスの参照をカウントするなどの可能性と、これらのソリューションがどれも受け入れられないと見なされた理由を調べます。

[リソース管理と確定的なファイナライズの問題]は非常にデリケートなトピックであるため、私はできる限り正確かつ完全な説明を心がけています。メールの長さをお詫び申し上げます。このメールの最初の90%は、問題が本当に難しいことを納得させようとしています。その最後の部分では、私たちがやろうとしていることについて話しますが、これらのオプションを検討している理由を理解するには、最初の部分が必要です。

...

ソリューションは自動参照カウントの形式をとる(プログラマーが忘れられないようにする)と、サイクルを自動的に検出して処理するためのその他のものを想定しました。 ...私たちはこれは一般的なケースでは機能しないと最終的に結論しました。

...

要約すれば:

  • プログラマーにこれらの複雑なデータ構造の問題の理解、追跡、設計を強いることなくサイクルの問題を解決するが非常に重要であると感じています。
  • 高性能(スピードとワーキングセットの両方)のシステムがあることを確認したいのですが、分析ではシステム内のすべてのオブジェクトの参照カウントではこの目標を達成できないと示されています。
  • 構成やキャストの問題を含むさまざまな理由により、参照カウントが必要なオブジェクトだけを対象にする単純な透過的なソリューションはありませんがあります。
  • 他の言語との相互運用性を阻害するため、単一言語/コンテキストの確定的なファイナライズを提供し、言語固有のバージョンを作成することによってクラスライブラリの分岐を引き起こすソリューションを選択しないことにしました。
49
Lucas

ガベージコレクターは、定義したeveryクラス/タイプのDisposeメソッドを記述する必要はありません。クリーンアップのために何かを明示的に行う必要がある場合にのみ、1つを定義します。ネイティブリソースを明示的に割り当てた場合。ほとんどの場合、オブジェクトに対してnew()のような処理を行っても、GCはメモリを再利用します。

GCは参照カウントを行いますが、「到達可能」なオブジェクトを見つけることで別の方法でカウントします(Ref Count > 0コレクションを実行するたびに...整数のカウンターの方法では実行しません。 。到達できないオブジェクトが収集されます(Ref Count = 0)。このようにして、オブジェクトが割り当てられたり解放されたりするたびにランタイムがハウスキーピング/テーブルを更新する必要がなくなります...高速になるはずです。

C++(確定的)とC#(非確定的)の主な違いは、オブジェクトがいつクリーンアップされるかだけです。オブジェクトがC#で収集される正確な瞬間を予測することはできません。

空いているプラ​​グ:GCの動作に本当に関心がある場合は、GCに関するJeffrey Richterのスタンドアップの章を CLR via C# で読むことをお勧めします。

31
Gishu

参照カウントはC#で試行されました。私は、Rotor(ソースが利用可能になったCLRのリファレンス実装)をリリースした人々は、世代別のものと比較するために参照カウントベースのGCを行ったと思います。結果は驚くべきものでした。「ストック」GCは非常に高速で、面白くもありませんでした。これをどこで聞いたのか正確には覚えていませんが、Hanselmuntesポッドキャストの1つだったと思います。 C++がC#と比較して基本的にパフォーマンスが低下するのを見たい場合は、Google Raymond Chenの中国語辞書アプリです。彼はC++バージョンを実行し、次にRico MarianiがC#バージョンを実行しました。レイモンドは6回イテレーションを行ってようやくC#バージョンに勝ったと思いますが、そのときまでに、C++のすべてのニースオブジェクト指向を削除し、win32 APIレベルに到達する必要がありました。すべてがパフォーマンスハックに変わりました。同時に、C#プログラムは1度だけ最適化され、最終的にはまともなOOプロジェクトのように見えました

23
user8032

C++スタイルのスマートポインター参照カウントと参照カウントガベージコレクションには違いがあります。 私のブログ でも違いについて話しましたが、ここに簡単な要約があります:

C++スタイルの参照カウント:

  • 減分時の無制限のコスト:大規模なデータ構造のルートがゼロに減分される場合、すべてのデータを解放するための無制限のコストがあります。

  • 手動サイクルコレクション:循環データ構造がメモリをリークするのを防ぐために、プログラマーは、サイクルの一部を弱いスマートポインターに置き換えることにより、潜在的な構造を手動で解除する必要があります。これは、潜在的な欠陥のもう1つの原因です。

参照カウントガベージコレクション

  • Deferred RC:オブジェクト参照カウントへの変更は、スタックおよびレジスタ参照では無視されます。代わりに、GCがトリガーされると、これらのオブジェクトはルートセットを収集することによって保持されます。参照カウントへの変更は、遅延してバッチで処理できます。これは より高いスループット をもたらします。

  • 結合:書き込みバリアを使用すると、参照カウントを coalesce 変更できます。これにより、オブジェクト参照カウントのほとんどの変更を無視して、頻繁に変更される参照のRCパフォーマンスを向上させることができます。

  • サイクル検出:完全なGC実装では、サイクル検出器も使用する必要があります。ただし、インクリメンタルな方法でサイクル検出を実行することは可能です。つまり、制限付きのGC時間を意味します。

基本的に、JavaのJVMや.net CLRランタイムなどのランタイム用に、高性能なRCベースのガベージコレクターを実装することが可能です。

トレースコレクターは、歴史的な理由で部分的に使用されていると思います。参照カウントの最近の改善の多くは、JVMと.netランタイムの両方がリリースされた後に行われました。研究作業は、生産プロジェクトへの移行にも時間がかかります。

確定的なリソースの破棄

これはかなり別の問題です。 .netランタイムは、以下の例のように、IDisposableインターフェイスを使用してこれを可能にします。私も好きです Gishu's 答え。


@ Skrymsli 、これが " sing "キーワードの目的です。例えば。:

public abstract class BaseCriticalResource : IDiposable {
    ~ BaseCriticalResource () {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this); // No need to call finalizer now
    }

    protected virtual void Dispose(bool disposing) { }
}

次に、重要なリソースを持つクラスを追加します。

public class ComFileCritical : BaseCriticalResource {

    private IntPtr nativeResource;

    protected override Dispose(bool disposing) {
        // free native resources if there are any.
        if (nativeResource != IntPtr.Zero) {
            ComCallToFreeUnmangedPointer(nativeResource);
            nativeResource = IntPtr.Zero;
        }
    }
}

次に、それを使用するのは簡単です:

using (ComFileCritical fileResource = new ComFileCritical()) {
    // Some actions on fileResource
}

// fileResource's critical resources freed at this point

IDisposableを正しく実装する も参照してください。

14
Luke Quinane

私はC++の経歴を持っていて、C#で約1年働いています。他の多くの人と同じように、なぜ決定論的なリソース管理が言語に組み込まれていないのかについて、私は頭がいっぱいです。

using構造は「確定的」なリソース管理を提供し、C#言語に組み込まれています。 「確定的」とは、Disposeブロックが実行を開始した後のコードの前にusingが呼び出されていることが保証されていることを意味します。これは「決定論的」という言葉が意味するものではないが、誰もがこの文脈でそのようにそれを悪用しているように見えることにも注意してください。

私のC++バイアスの脳では、確定的デストラクタで参照カウントスマートポインタを使用することが、IDisposableを実装して非メモリリソースをクリーンアップするためにdisposeを呼び出す必要があるガベージコレクタからの主要なステップアップのようです。

ガベージコレクターは、IDisposableを実装する必要はありません。実際、GCは完全にそれに気づいていません。

確かに、私はあまり頭が良くありません...だから、物事がどうあるべきかをよりよく理解したいという欲求から純粋にこれを求めています。

ガベージコレクションのトレースは、無限のメモリマシンをエミュレートするための高速で信頼性の高い方法であり、プログラマが手動のメモリ管理の負担から解放されます。これにより、いくつかのクラスのバグが解消されました(宙ぶらりんのポインター、すぐに解放されない、2倍解放される、解放するのを忘れた)。

C#が次のように変更された場合:

オブジェクトは参照カウントされます。オブジェクトの参照カウントがゼロになると、リソースクリーンアップメソッドがオブジェクトに対して確定的に呼び出されます。

2つのスレッド間で共有されるオブジェクトを考えます。スレッドは、参照カウントをゼロにデクリメントするために競合します。 1つのスレッドがレースに勝ち、もう1つのスレッドがクリーンアップを担当します。それは非決定的です。参照カウントは本質的に決定論的であるという信念は神話です。

もう1つの一般的な神話は、参照カウントはオブジェクトをプログラムの最も早い時点で解放するというものです。そうではありません。デクリメントは常に、通常はスコープの終わりまで延期されます。これにより、オブジェクトが必要以上に長く存続し、いわゆる「浮遊ゴミ」が残ります。特に、一部のトレースガベージコレクタは、スコープベースの参照カウント実装よりも前にオブジェクトをリサイクルできます。

次に、オブジェクトにガベージコレクションのマークが付けられます。ガベージコレクションは、将来の非決定的な時点で発生し、その時点でメモリが解放されます。このシナリオでは、IDisposableを実装したり、Disposeを呼び出したりする必要はありません。

とにかくガベージコレクションされたオブジェクトに対してIDisposableを実装する必要はないので、これはメリットがありません。

解放する非メモリリソースがある場合は、リソースクリーンアップ関数を実装するだけです。

なぜそれが悪い考えなのですか?

単純な参照カウントは非常に遅く、リークサイクルがあります。たとえば、 Boost's shared_ptr C++では、OCamlのトレースGCよりも最大10倍遅くなります 。単純なスコープベースの参照カウントでさえ、マルチスレッドプログラム(ほとんどすべての最新のプログラム)が存在する場合、非決定的です。

それはガベージコレクターの目的を無効にするでしょうか。

まったく違います。実際、1960年代に発明され、参照カウントが一般的なケースでは駄目だと結論づけて、今後54年間熱心な学術研究を受けたのは悪い考えです。

そのようなことを実装することは可能でしょうか?

もちろんです。初期のプロトタイプ.NETとJVMは参照カウントを使用していました。彼らはまた、GCを追跡することを優先して、それを吸い込んで落としたことも発見しました。

編集:これまでのコメントから、これは悪い考えです

GCは参照カウントなしで高速です

はい。カウンターのインクリメントとデクリメントを延期することで参照カウントを大幅に高速化できますが、それによって、求めている決定論が非常に犠牲になり、今日のヒープサイズでGCをトレースするよりも遅いことに注意してください。ただし、参照カウントは漸近的に高速であるため、ヒープが非常に大きくなる将来のある時点で、プロダクション自動化メモリ管理ソリューションでRCを使用し始める可能性があります。

オブジェクトグラフのサイクルを処理する問題

トライアル削除は、参照カウントシステムでサイクルを検出および収集するために特別に設計されたアルゴリズムです。ただし、それは遅く、非決定的です。

1番目は有効だと思いますが、2番目は弱参照を使用して簡単に処理できます。

弱い参照を「簡単」と呼ぶことは、現実に対する希望の勝利です。彼らは悪夢です。それらは予測不可能で設計が難しいだけでなく、APIを汚染します。

速度の最適化はあなたの短所を上回ります:

非メモリリソースを適時に解放しない可能性があります

usingは非メモリリソースを適時に解放しませんか?

非メモリリソースをすぐに解放する可能性がありますリソースのクリーンアップメカニズムが確定的であり、言語に組み込まれている場合、それらの可能性を排除できます。

using構造は確定的であり、言語に組み込まれています。

あなたが本当に聞きたい質問は、なぜリファレンスカウントをIDisposable使用しないのかだと思います。私の応答は逸話です。私は18年間ガベージコレクションされた言語を使用しており、参照カウントに頼る必要はありませんでした。結果として、私は、弱い参照のような偶発的な複雑さで汚染されない、より単純なAPIをはるかに好みます。

6
Jon Harrop

ガベージコレクションについて何か知っています。完全な説明はこの質問の範囲を超えているため、ここに短い要約を示します。

.NETは、コピーおよび圧縮世代別ガベージコレクタを使用します。これは参照カウントよりも高度であり、直接またはチェーンを通じて自分自身を参照するオブジェクトを収集できるという利点があります。

参照カウントはサイクルを収集しません。参照カウントはスループットも低くなりますが(全体的には遅くなります)、トレースコレクターよりも高速な休止(最大の休止は小さい)という利点があります。

5
Unknown

ここには多くの問題があります。まず、マネージメモリの解放と他のリソースのクリーンアップを区別する必要があります。前者は非常に高速ですが、後者は非常に遅い場合があります。 .NETでは2つが分離されているため、管理対象メモリのクリーンアップを高速化できます。これは、クリーンアップする管理メモリを超える何かがある場合にのみDispose/Finalizerを実装する必要があることも意味します。

.NETは、オブジェクトへのルートを探してヒープをトラバースするマークアンドスイープテクニックを採用しています。ルート化されたインスタンスは、ガベージコレクションの間存続します。それ以外のすべては、メモリを解放するだけでクリーンアップできます。 GCは時々メモリを圧縮する必要がありますが、それ以外のメモリの再利用は、複数のインスタンスを再利用する場合でも簡単なポインタ操作です。これをC++のデストラクタへの複数の呼び出しと比較してください。

4
Brian Rasmussen

確定的な非メモリリソース管理は言語の一部ですが、デストラクタでは行われません。

あなたの意見は、C++のバックグラウンドから来て、 [〜#〜] raii [〜#〜] デザインパターンを使用しようとする人々の間で一般的です。 C++では、例外がスローされた場合でも、一部のコードがスコープの最後で実行されることを保証できる唯一の方法は、オブジェクトをスタックに割り当て、クリーンアップコードをデストラクタに配置することです。

他の言語(C#、Java、Python、Ruby、Erlangなど)では、代わりにtry-finally(またはtry-catch-finally)を使用して、クリーンアップコードが常に実行されるようにすることができます。

// Initialize some resource.
try {
    // Use the resource.
}
finally {
    // Clean-up.
    // This code will always run, whether there was an exception or not.
}

C#の場合、 sing 構文を使用することもできます。

using (Foo foo = new Foo()) {
    // Do something with foo.
}
// foo.Dispose() will be called afterwards, even if there
// was an exception.

したがって、C++プログラマーにとっては、「クリーンアップコードの実行」と「メモリの解放」を2つの別個のものとして考えると役立つ場合があります。クリーンアップコードをfinallyブロックに入れ、GCに任せてメモリを処理します。

1

ユーザーがDisposeを明示的に呼び出さない場合、IDisposableを実装するオブジェクトは、GCによって呼び出されるファイナライザーも実装する必要があります。MSDNの IDisposable.Disposeを参照してください

IDisposableの重要な点は、GCが非決定論的な時間に実行されていることです。貴重なリソースを保持していて、決定論的な時間に解放したいので、IDisposableを実装します。

したがって、あなたの提案はIDisposableに関して何も変更しません。

編集:

ごめんなさい。提案を正しく読みませんでした。 :-(

ウィキペディアには、参照がカウントされたリファレンスの 欠点の簡単な説明があります

1
mlarsen

参照カウント

参照カウントを使用するコストは2つあります。まず、すべてのオブジェクトに特別な参照カウントフィールドが必要です。通常、これは、各オブジェクトに追加のストレージワードを割り当てる必要があることを意味します。次に、ある参照が別の参照に割り当てられるたびに、参照カウントを調整する必要があります。これにより、割り当てステートメントにかかる時間が大幅に増加します。

。NETのガベージコレクション

C#は、オブジェクトの参照カウントを使用しません。代わりに、スタックからのオブジェクト参照のグラフを維持し、ルートから移動して、参照されているすべてのオブジェクトを覆います。グラフ内の参照されるすべてのオブジェクトはヒープ内で圧縮され、将来のオブジェクトで連続メモリを使用できるようになります。ファイナライズする必要のない、参照されていないすべてのオブジェクトのメモリが解放されます。参照されていないがファイナライザーが実行されるものは、f-reachableキューと呼ばれる別のキューに移動され、ガベージコレクターがバックグラウンドでファイナライザーを呼び出します。

上記に加えて、GCは世代の概念を使用して、より効率的なガベージコレクションを実現します。これは、次の概念に基づいています。1。管理ヒープ全体の場合よりも管理ヒープの一部の方がメモリをコンパクト化する方が高速です。2。新しいオブジェクトのライフタイムは短く、古いオブジェクトのライフタイムは長くなります。3.新しいオブジェクトは、互いに関連し、アプリケーションが同時にアクセスする

マネージヒープは、3つの世代(0、1、2)に分かれています。新しいオブジェクトはgen 0に格納されます。GCのサイクルによって再利用されないオブジェクトは、次の世代に昇格します。したがって、世代0にある新しいオブジェクトがGCサイクル1を生き延びた場合、それらは世代1に昇格します。GCサイクル2を生き残ったオブジェクトは、世代2に昇格します。ガベージコレクターは3世代しかサポートしないため、世代2のオブジェクトはコレクションは、将来のコレクションで到達不可能であると判断されるまで、ジェネレーション2に残ります。

ガベージコレクターは、ジェネレーション0がいっぱいで、新しいオブジェクトのメモリを割り当てる必要がある場合にコレクションを実行します。世代0のコレクションが十分なメモリを回収しない場合、ガベージコレクタは世代1のコレクションを実行し、次に世代0を実行できます。これが十分なメモリを回収しない場合、ガベージコレクタは世代2、1、および0のコレクションを実行できます。 。

したがって、GCは参照カウントよりも効率的です。

1
Rashmi Pandit