C++ 標準ライブラリ自体にガベージ コレクタを導入する C++14 の話を聞いています。この機能の背後にある理論的根拠は何ですか? これが C++ に RAII が存在する理由ではないでしょうか?
- 標準ライブラリのガベージ コレクタの存在は RAII セマンティックにどのように影響しますか?
- 私 (プログラマー) にとって、または C++ プログラムの作成方法にとって、それはどのように重要なのでしょうか?
C++ 標準ライブラリ自体にガベージ コレクタを導入する C++14 の話を聞いています。この機能の背後にある理論的根拠は何ですか? これが C++ に RAII が存在する理由ではないでしょうか?
ガベージ コレクションと RAII は、さまざまな状況で役立ちます。GC の存在が RAII の使用に影響することはありません。RAII はよく知られているので、GC が便利な例を 2 つ挙げます。
ガベージ コレクションは、ロックのないデータ構造を実装する上で非常に役立ちます。
[...] ロックフリーのデータ構造では、決定論的なメモリの解放が非常に根本的な問題であることが判明しました。( Andrei Alexandrescu による Lock-Free Data Structuresから)
基本的に問題は、スレッドがメモリを読み取っている間にメモリの割り当てを解除しないようにする必要があることです。ここで GC が便利になります。GC はスレッドを確認し、安全な場合にのみ割り当て解除を行うことができます。詳細は記事をお読みください。
ここで明確にしておきたいのは、Java のようにWHOLE WORLDをガベージ コレクションする必要があるという意味ではありません。関連するデータのみを正確にガベージ コレクションする必要があります。
彼のプレゼンテーションの 1 つで、Bjarne Stroustrup も、GC が便利になる有効な例を示しました。C/C++ で記述された、サイズが 10M SLOC のアプリケーションを想像してみてください。アプリケーションはかなりうまく機能しますが (かなりバグはありません)、リークします。これを修正するためのリソース (工数) も機能的な知識もありません。ソース コードはやや乱雑なレガシー コードです。職業はなんですか?GC を使用して問題を解決するには、おそらくこれが最も簡単で安価な方法であることに同意します。
sasha.sochkaで指摘されているように、ガベージ コレクタはオプションになります。
私の個人的な懸念は、人々が Java で使用されているように GC を使い始め、ずさんなコードを書き、すべてをガベージ コレクションするようになることです。(スタック割り当てがそれを行うshared_ptr
場合でも、すでにデフォルトの「go to」になっている印象があります。)unique_ptr
現在の C++ 標準には GC がないという @DeadMG に同意しますが、B. Stroustrup からの次の引用を追加したいと思います。
自動ガベージ コレクションが C++ の一部になる場合 (ない場合)、オプションになります。
したがって、Bjarne は将来的に追加されると確信しています。少なくとも EWG (Evolution Working Group) の議長であり、最も重要な委員会メンバーの 1 人 (さらに重要な言語作成者) は、それを追加したいと考えています。
彼の意見が変わらない限り、将来追加されて実装されることが期待できます。
GC なしでは複雑/非効率的/不可能なアルゴリズムがいくつかあります。これが C++ での GC の主要なセールス ポイントであると思われますが、GC が汎用アロケーターとして使用されることはありません。
汎用アロケータではないのはなぜですか?
まず、RAII があり、ほとんど (私を含む) は、これがリソース管理の優れた方法であると信じているようです。堅牢でリークのないコードの記述がはるかに簡単になり、パフォーマンスが予測可能になるため、決定論が好きです。
第 2 に、メモリの使用方法について、C++ とはまったく異なる制限を設ける必要があります。たとえば、到達可能で難読化されていないポインターが少なくとも 1 つ必要です。難読化されたポインターは、一般的なツリー コンテナー ライブラリ (カラー フラグにアライメントが保証された下位ビットを使用) でよく使用されるように、GC によって認識されません。
それに関連して、難読化されたポインターをいくつでもサポートする場合、最新の GC を非常に使いやすくするものを C++ に適用することは非常に難しくなります。世代別最適化 GC は非常に優れています。なぜなら、割り当ては非常に安価であり (基本的にはポインターをインクリメントするだけです)、最終的には割り当てが圧縮されて局所性が改善されるからです。これを行うには、オブジェクトが移動可能である必要があります。
オブジェクトを安全に移動できるようにするには、GC がそのオブジェクトへのすべてのポインターを更新できる必要があります。難読化されたものを見つけることはできません。これは適応する可能性がありますが、きれいではありません(おそらく、生のポインターが必要なときに使用されるgc_pin
current のように使用されるタイプまたは類似のものです)。std::lock_guard
使いやすさはすぐにわかります。
物事を移動可能にしないと、GC は他の場所で慣れ親しんでいるものよりも大幅に遅くなり、スケーラビリティも低下します。
ユーザビリティの理由 (リソース管理) と効率の理由 (高速で移動可能な割り当て) は別として、GC は他に何に役立つのでしょうか? もちろん汎用ではありません。ロックフリー アルゴリズムに入ります。
なぜロックフリーなのですか?
ロックフリー アルゴリズムは、競合している操作をデータ構造と一時的に「非同期」にし、後のステップでこれを検出/修正することで機能します。この影響の 1 つは、メモリが削除された後に競合状態でアクセスされる可能性があることです。たとえば、複数のスレッドが競合して LIFO からノードをポップする場合、別のスレッドがノードが既に使用されていることに気付く前に、あるスレッドがノードをポップして削除する可能性があります。
スレッド A:
スレッド B:
スレッド A:
スレッド B:
GC を使用すると、スレッド B がノードを参照している間はノードが削除されないため、コミットされていないメモリから読み取る可能性を回避できます。ハザード ポインターや Windows での SEH 例外のキャッチなど、これを回避する方法はありますが、パフォーマンスが大幅に低下する可能性があります。ここでは、GC が最適なソリューションになる傾向があります。
ない、ないから。C++ が GC 用に持っていた唯一の機能は C++11 で導入されたものであり、メモリをマークしているだけであり、コレクターは必要ありません。C++14 にもありません。
私の意見では、収集家が委員会を通過できる方法はまったくありません。
GC には次の利点があります。
GC が最良の選択であると言っているのではありません。特徴が違うとしか言いようがない。一部のシナリオでは、それが利点になる場合があります。
定義:
RCB GC: 参照カウント ベースの GC。
MSB GC: マーク スイープ ベースの GC。
素早い回答:
MSB GC は、場合によっては RCB GC よりも便利であるため、C++ 標準に追加する必要があります。
2 つの説明的な例:
初期サイズが小さいグローバル バッファを考えてみましょう。どのスレッドでも、そのサイズを動的に拡大し、他のスレッドが古い内容にアクセスできるように保持できます。
実装 1 (MSB GC バージョン):
int* g_buf = 0;
size_t g_current_buf_size = 1024;
void InitializeGlobalBuffer()
{
g_buf = gcnew int[g_current_buf_size];
}
int GetValueFromGlobalBuffer(size_t index)
{
return g_buf[index];
}
void EnlargeGlobalBufferSize(size_t new_size)
{
if (new_size > g_current_buf_size)
{
auto tmp_buf = gcnew int[new_size];
memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));
std::swap(tmp_buf, g_buf);
}
}
実装 2 (RCB GC バージョン):
std::shared_ptr<int> g_buf;
size_t g_current_buf_size = 1024;
std::shared_ptr<int> NewBuffer(size_t size)
{
return std::shared_ptr<int>(new int[size], []( int *p ) { delete[] p; });
}
void InitializeGlobalBuffer()
{
g_buf = NewBuffer(g_current_buf_size);
}
int GetValueFromGlobalBuffer(size_t index)
{
return g_buf[index];
}
void EnlargeGlobalBufferSize(size_t new_size)
{
if (new_size > g_current_buf_size)
{
auto tmp_buf = NewBuffer(new_size);
memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));
std::swap(tmp_buf, g_buf);
//
// Now tmp_buf owns the old g_buf, when tmp_buf is destructed,
// the old g_buf will also be deleted.
//
}
}
ご注意ください:
を呼び出した後、古いstd::swap(tmp_buf, g_buf);
をtmp_buf
所有していg_buf
ます。がtmp_buf
破壊されると、古いg_buf
ものも削除されます。
GetValueFromGlobalBuffer(index);
別のスレッドが古い から値を取得するために呼び出している場合g_buf
、競合ハザードが発生します!!!
したがって、実装 2 は実装 1 と同じくらいエレガントに見えますが、機能しません!
実装 2 を正しく機能させたい場合は、ある種のロック機構を追加する必要があります。その場合、遅くなるだけでなく、実装 1 よりもエレガントではなくなります。
結論:
MSB GC をオプション機能として C++ 標準に取り入れることは良いことです。