19

スレッドセーフな遅延初期化の場合、関数内の静的変数、std::call_once、または明示的な二重チェック ロックを使用する必要がありますか? 意味のある違いはありますか?

3つすべてがこの質問で見ることができます。

C++11 のダブルチェック ロック シングルトン

C++11 の二重チェック ロックの 2 つのバージョンが Google に表示されます。

Anthony Williamsは、明示的なメモリ順序付けによる二重チェック ロックと std::call_once の両方を示しています。彼は静的については言及していませんが、その記事は C++11 コンパイラが利用可能になる前に書かれた可能性があります。

Jeff Preshing は、詳細な記事で、二重チェック ロックのいくつかのバリエーションについて説明しています。彼は静的変数をオプションとして使用することについて言及しており、静的変数を初期化するためにコンパイラーがダブルチェックロックのコードを生成することさえ示しています。ある方法が他の方法よりも優れていると彼が結論付けているかどうかは、私には明らかではありません.

どちらの記事も教育的なものであり、そうする理由はないと私は感じています。静的変数または std::call_once を使用すると、コンパイラが自動的に実行します。

4

1 に答える 1

31

staticGCC はプラットフォーム固有のトリックを使用して、高速パスでアトミック操作を完全に回避し、call_once やダブルチェックよりも優れた分析を実行できるという事実を活用します。

ダブルチェックは競合ケースを回避する方法としてアトミックを使用するため、毎回取得の代償を払わなければなりません。高くないですが、それなりのお値段です。

比較交換のような難しい操作であっても、アトミックはすべてのケースでアトミックのままでなければならないため、これを支払う必要があります。これにより、最適化が非常に困難になります。一般的に言えば、変数を二重ロック以外の目的で使用する場合に備えて、コンパイラはそれをそのままにしておく必要があります。アトミックでより複雑な操作をまったく使用していないことを証明する簡単な方法はありません。

一方、staticは高度に専門化されており、言語の一部です。最初から、証明可能な初期化が非常に簡単になるように設計されています。したがって、コンパイラは、より一般的なバージョンでは利用できなかったショートカットを使用できます。 コンパイラは、実際には静的に対して次のコードを発行します。

簡単な関数:

void foo() {
    static X x;
}

GCC 内で次のように書き換えられます。

void foo() {
    static X x;
    static guard x_is_initialized;
    if ( __cxa_guard_acquire(x_is_initialized) ) {
        X::X();
        x_is_initialized = true;
        __cxa_guard_release(x_is_initialized);
    }
}

これは、二重チェックのロックによく似ています。ただし、コンパイラはここで少しごまかします。ユーザーが直接使用することは決してできないことを知っていますcxa_guard。コンパイラーがそれを使用することを選択した特別な状況でのみ使用されることを知っています。したがって、その追加情報を使用すると、時間を節約できます。CXA ガード仕様は、配布されているため、すべて共通のルールを共有しています。__cxa_guard_acquireつまり、ガードの最初のバイトを決して変更せず、__cxa_guard__releaseゼロ以外に設定します。

これは、各ガードが単調でなければならないことを意味し、どの操作がそうするかを正確に指定します。したがって、ホスト プラットフォーム内の既存の競合ケース保護を利用できます。たとえば、x86 では、強力に同期された CPU によって保証される LL/SS 保護は、この取得/解放パターンを実行するのに十分であることが判明したため、二重ロックを行うときに、最初のバイトの生の読み取りを行うことができます。取得読み取り。これが可能なのは、GCC が C++ アトミック API を使用して二重のロックを行っていないためです。つまり、プラットフォーム固有のアプローチを使用しています。

一般に、GCC はアトミックを最適化できません。あまり同期しないように設計されたアーキテクチャ (1024+ コア用に設計されたものなど) では、GCC はアーキテクチャに依存して LL/SS を実行することができません。したがって、GCC は実際にアトミックを発行することを余儀なくされます。ただし、x86 や x64 などの一般的なプラットフォームでは、より高速になる可能性があります。

call_once同様once_flagに、アトミックに適用できる関数の一部に実行できる操作の数を制限するため、GCC の静的関数の効率を持つことができます。トレードオフは、静的が適用可能な場合は使用するのがはるかに便利ですがcall_once、静的が不十分な多くの場合 (once_flag動的に生成されたオブジェクトが所有するなど) で機能することです。

call_once静的なプラットフォームとこれらのより高いプラットフォームでは、パフォーマンスにわずかな違いがあります。これらのプラットフォームの多くは、LL/SS を提供していませんが、少なくとも整数の非引き裂き読み取りを提供します。これらのプラットフォームは、これとスレッド固有のポインターを使用して、アトミックを回避するためにスレッドごとのエポック カウントを行うことができます。これは static またはcall_onceの場合は十分ですが、ロールオーバーしないカウンターに依存します。ティアリングのない 64 ビット整数がない場合は、call_onceロールオーバーを心配する必要があります。実装では、これについて心配する場合としない場合があります。この問題を無視すれば、静的と同じくらい高速になる可能性があります。その問題に注意を払うなら、アトミックと同じくらい遅くなければなりません。Static はコンパイル時に静的変数/ブロックの数を認識しているため、コンパイル時にロールオーバーがないことを証明できます (または、少なくとも自信を持ってください!)。

于 2014-11-29T20:16:06.000 に答える