64

私はC++のバックグラウンドを持っており、C#を使用して約1年になります。他の多くの人と同じように、決定論的なリソース管理が言語に組み込まれていない理由について私は困惑しています。決定論的デストラクタの代わりに、disposeパターンがあります。人々は、IDisposable癌を自分たちのコードを通して広めることが、努力する価値があるかどうか疑問に思い始めます。

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

C#が次のように変更された場合はどうなりますか?

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

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

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

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

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

したがって、速度の最適化は、次のような短所を上回ります。

  1. 非メモリリソースをタイムリーに解放できない場合があります
  2. 非メモリリソースをすぐに解放する可能性があります

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

4

10 に答える 10

54

Brad Abramsは、.NetFrameworkの開発中に書かれたBrianHarryからの電子メールを投稿しました。初期の優先事項の1つが、参照カウントを使用するVB6との意味的同等性を維持することであった場合でも、参照カウントが使用されなかった理由の多くを詳しく説明します。一部のタイプの参照をカウントし、他のタイプをカウントしない(IRefCounted!)、または特定のインスタンスの参照をカウントするなどの可能性と、これらのソリューションのいずれも許容できないと見なされた理由を調べます。

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

..。

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

..。

要約すれば:

  • プログラマーにこれらの複雑なデータ構造の問題を理解し、追跡し、設計することを強いることなく、サイクルの問題を解決することが非常に重要であると私たちは感じています。
  • 高性能(速度とワーキングセットの両方)システムがあることを確認したいと思います。分析によると、システム内のすべてのオブジェクトに参照カウントを使用しても、この目標を達成することはできません
  • 構成やキャストの問題など、さまざまな理由から、参照カウントを必要とするオブジェクトだけを取得するための単純で透過的な解決策はありません
  • 他の言語との相互運用を禁止し、言語固有のバージョンを作成することでクラスライブラリの分岐を引き起こすため、単一の言語/コンテキストに対して決定論的なファイナライズを提供するソリューションを選択しないことを選択しました。
于 2009-05-26T04:52:36.340 に答える
31

ガベージ コレクターでは、定義するすべてのクラス/型に対して Dispose メソッドを記述する必要はありません。cleanup に対して何かを明示的に行う必要がある場合にのみ定義します。ネイティブ リソースを明示的に割り当てた場合。ほとんどの場合、オブジェクトに対して new() のようなことをしただけでも、GC はメモリを回収するだけです。

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

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

何度目かのプラグイン: GC の仕組みに本当に興味がある場合は、Jeffrey Richter のC# を介した CLRの GC に関するスタンドアップの章を読むことをお勧めします。

于 2009-05-15T05:48:14.593 に答える
23

参照カウントは C# で試しました。Rotor (ソースが利用可能になった CLR の参照実装) をリリースした人々は、それが世代のものとどのように比較されるかを確認するためだけに、カウントベースの GC を参照したと思います。結果は驚くべきものでした。「ストック」の GC は非常に高速で、まったくおかしなことではありませんでした。これをどこで聞いたか正確には覚えていませんが、Hanselmuntes のポッドキャストの 1 つだったと思います。C++ が基本的に C# と比較してパフォーマンスが低下するのを見たい場合は、Raymond Chen の中国語辞書アプリをグーグルで検索してください。彼は C++ バージョンを作成し、次に Rico Mariani が C# バージョンを作成しました。Raymond が最終的に C# バージョンを打ち負かすのに 6 回の反復が必要だったと思いますが、その時までに、彼は C++ の優れたオブジェクト指向をすべて捨てて、win32 API レベルに降りなければなりませんでした。全体がパフォーマンスハックに変わりました。

于 2009-05-15T05:51:29.873 に答える
15

C++ スタイルのスマート ポインター参照カウントと参照カウント ガベージ コレクションには違いがあります。違いについてはブログでも説明しましたが、簡単にまとめると次のとおりです。

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

  • デクリメントの無制限のコスト:大きなデータ構造のルートがゼロにデクリメントされた場合、すべてのデータを解放するための無制限のコストがあります。

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

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

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

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

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

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

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

決定論的なリソースの処分

これはほとんど別の問題です。.net ランタイムは、IDisposable インターフェイスを使用してこれを可能にします。以下の例をご覧ください。ギシュの答えも好きです。


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

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 の正しい実装も参照してください。

于 2009-05-26T03:20:22.100 に答える
8

私は C++ のバックグラウンドを持っており、約 1 年間 C# を使用しています。他の多くの人と同じように、決定論的リソース管理が言語に組み込まれていない理由について、私は困惑しています。

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

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

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

確かに、私はあまり頭がよくありません...なぜ物事がそのようになっているのかをよりよく理解したいという純粋な願望からこれを求めています.

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

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

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

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

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

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

IDisposableいずれにせよ、ガベージ コレクションされたオブジェクトを実装する必要はないため、メリットはありません。

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

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

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

それはガベージコレクターの目的を無効にしますか?

全然違います。実際、それは 1960 年代に発明され、その後 54 年間にわたって集中的な学術研究が行われ、参照カウントは一般的なケースでは最悪であると結論付けられた悪い考えです。

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

絶対。初期のプロトタイプ .NET と JVM は参照カウントを使用していました。彼らはまた、GC のトレースを支持して、それがひどいものであることを発見し、それを落としました。

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

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

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

オブジェクトグラフのサイクルを扱う問題

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

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

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

速度の最適化は、次のような短所を上回ります。

タイムリーにメモリ以外のリソースを解放しない可能性があります

usingタイムリーにメモリ以外のリソースを解放しませんか?

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

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

あなたが本当に聞きたい質問は、IDisposable参照カウントを使用しない理由だと思います。私の回答は逸話的です: 私は 18 年間、ガベージ コレクション言語を使用してきましたが、参照カウントに頼る必要はありませんでした。したがって、弱い参照のような付随的な複雑さで汚染されていない、より単純な API を好みます。

于 2014-12-19T16:50:51.623 に答える
5

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

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

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

于 2009-05-15T05:53:22.130 に答える
4

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

.NET は、オブジェクトへのルートを探してヒープを横断するマーク アンド スイープ手法を採用しています。ルート化されたインスタンスは、ガベージ コレクション後も存続します。メモリを再利用するだけで、他のすべてをきれいにすることができます。GC はときどきメモリを圧縮する必要がありますが、それとは別に、メモリの再利用は、複数のインスタンスを再利用する場合でも単純なポインター操作です。これを C++ のデストラクタへの複数の呼び出しと比較してください。

于 2009-05-15T05:55:43.343 に答える
1

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

あなたの意見は、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.
}

IC# では、 usingコンストラクトも使用できます。

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 に任せます。

于 2013-09-03T01:31:39.917 に答える
1

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

IDisposable の全体的なポイントは、GC が非決定論的な時間に実行されており、貴重なリソースを保持していて決定論的な時間に解放したいため、IDisposable を実装することです。

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

編集:

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

ウィキペディアには、リファレンス カウント GC の欠点が簡単に説明されています。

于 2009-05-15T05:55:31.237 に答える
1

参照カウント

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

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

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

上記に加えて、GC はジェネレーションの概念を使用して、より効率的なガベージ コレクションを実現します。これは、次の概念に基づいています 1. マネージド ヒープ全体よりもマネージド ヒープの一部のメモリを圧縮する方が高速です 2. 新しいオブジェクトは寿命が短く、古いオブジェクトは寿命が長くなります 3. 新しいオブジェクトは、互いに関連付けられ、ほぼ同時にアプリケーションによってアクセスされる

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

ジェネレーション 0 がいっぱいになり、新しいオブジェクトのメモリを割り当てる必要がある場合、ガベージ コレクターはコレクションを実行します。ジェネレーション 0 のコレクションが十分なメモリを再利用できない場合、ガベージ コレクターはジェネレーション 1 のコレクションを実行し、次にジェネレーション 0 を実行できます。 .

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

于 2009-05-15T05:55:55.027 に答える