6

以下は、 Concurrent Programming on windows、Chapter 10 Page 528~529、c++ テンプレートからの抜粋です実装を再確認してください

T getValue(){
    if (!m_pValue){
        EnterCriticalSection(&m_crst);
        if (! m_pValue){
            T pValue = m_pFactory();
            _WriteBarrier();
            m_pValue = pValue;                  
        }
        LeaveCriticalSection(&m_crst);
    }
      _ReadBarrier();
  return m_pValue;
}

著者の状態として:

_WriteBarrier は、オブジェクトをインスタンス化した後、m_pValue フィールドにそのオブジェクトへのポインターを書き込む前に見つかりました。これは、オブジェクトの初期化での書き込みが m_pValue 自体への書き込みを超えて遅延しないようにするために必要です。

_WriteBarrier はコンパイル バリアであるため、コンパイルが LeaveCriticalSection のセマンティクスを知っていると便利ではないと思います。コンパイルはおそらく pValue への書き込みを省略しますが、関数呼び出しの前に代入を移動するような最適化は行わないでください。そうしないと、プログラムのセマンティクスに違反します。LeaveCriticalSection には暗黙的なハードウェア フェンスがあると思います。したがって、m_pValue への割り当て前の書き込みはすべて同期されます。

一方、コンパイルが LeaveCriticalSection のセマンティクスを認識していない場合、コンパイルがクリティカル セクションから割り当てを移動するのを防ぐために、すべてのプラットフォームで _WriteBarrier が必要になります。

そして_ReadBarrierについて、著者は言った

同様に、m_value を返す直前に _ReadBarrier が必要です。これにより、getValue の呼び出し後のロードが、呼び出しの前に発生するように並べ替えられません。

まず、この関数がライブラリに含まれていて、ソース コードが利用できない場合、コンパイルはどのようにしてコンパイル バリアがあるかどうかを知るのでしょうか?

第二に、必要に応じて間違った場所に配置される可能性があります。取得フェンスを表現するには、EnterCriticalSection の直後に配置する必要があると思います。上で書いたことと同様に、コンパイルが EnterCriticalSection のセマンティクスを理解するかどうかに依存します。

また、著者は次のようにも述べています。

ただし、X86、Intel64、および AMD64 プロセッサではどちらのフェンスも必要ないことも指摘しておきます。IA64 のような弱いプロセッサが水を濁らせてしまったのは残念です

上記で分析したように、特定のプラットフォームでこれらのバリアが必要な場合は、すべてのプラットフォームでそれらが必要になります。これらのバリアはコンパイルバリアであるため、コンパイルが正しい最適化を実行できることを確認するだけです。一部の関数のセマンティクス。

間違っている場合は修正してください。

もう 1 つの質問ですが、msvc と gcc が同期セマンティクスを理解している関数を指摘する参照はありますか?

更新 1 : 回答によると (m_pValue はクリティカル セクションからアクセスされます)、ここからサンプル コードを実行すると、次のようになります。

  1. ここで著者が意味しているのは、コンパイル バリア以外のハードウェア フェンスであると思います。次のMSDNからの引用を参照してください。
  2. ハードウェアフェンスにも暗黙のコンパイルバリア(コンパイルの最適化を無効にする)があると思いますが、その逆はありません(ここを参照してください、cpuフェンスを使用しても並べ替えは表示されませんが、その逆はありません)

バリアはフェンスではありません。バリアはキャッシュ内のすべてに影響することに注意してください。フェンスは単一のキャッシュ ラインに影響します。

どうしても必要な場合を除き、バリアを追加しないでください。フェンスを使用するには、_Interlocked 組み込み関数のいずれかを選択できます。

著者が書いたように、「X86 Intel64 および AMD64 プロセッサではどちらのフェンスも必要ありません」。

まだ質問が残っています。コンパイルは、Enter/Leave クリティカル セクションの呼び出しのセマンティクスを理解していますか? そうでない場合は、次の回答のように最適化を行っている可能性があり、それが悪い動作を引き起こす可能性があります。

ありがとう

4

2 に答える 2

2

_ReadBarrier と _WriteBarrier

Joe Duffy は、_ReadBarrier および _WriteBarrier コンパイラ組み込み関数は、コンパイラ レベルとプロセッサ レベルのフェンスの両方であると考えています。Windowsでの並行プログラミング、ページ 515 で、彼は次のように書いています。

コンパイラ組み込み関数のセットは、VC++ でコンパイラ レベルとプロセッサ レベルの両方のフェンスを強制します。

著者は、_ReadBarrier および _WriteBarrier コンパイラ組み込み関数に依存して、コンパイラとハードウェアの両方の並べ替えを防止しています。

_ReadWriteBarrier コンパイラ組み込み関数に関する MSDN ドキュメントは、コンパイラ組み込み関数がハードウェア レベルに影響を与えるという仮定をサポートしていません。Visual Studio 2010 および Visual Studio 2008 の MSDN ドキュメントは、コンパイラの組み込み関数がハードウェア レベルに適用されることを明確に否定しています。

_ReadBarrier、_WriteBarrier、および _ReadWriteBarrier コンパイラ組み込み関数は、コンパイラの並べ替えのみを防止します。CPU が読み取り操作と書き込み操作の順序を変更しないようにするには、MemoryBarrier マクロを使用します。

Visual Studio 2005 および Visual Studio .NET 2003 の MSDN ドキュメントには、そのような注意事項はありません。組み込み関数がハードウェア レベルに適用されるかどうかについては何も述べていません。

_ReadBarrier と _WriteBarrier が実際にハードウェア フェンスを強制しない場合、コードは正しくありません。

「柵」という言葉について

Joe Duffy は著書の中で、ハードウェア フェンスとメモリ フェンスの両方にフェンスという用語を使用しています。511ページで、彼は次のように書いています。

フェンスが障壁とも呼ばれることは一般的です。Intel は「フェンス」という用語を好むようですが、AMD は「バリア」という用語を好みます。私も「フェンス」の方が好きなので、この本ではそれを使っています。

ハードウェア フェンス

ハードウェアフェンスにも暗黙のコンパイルバリアがあると思います(コンパイルの最適化を無効にします)

Synchronization and Multiprocessor Issuesの記事では、ハードウェア バリアがコンパイラにも影響することが確認されています。

これらの命令 (メモリバリア) は、バリアを越えてメモリ操作を並べ替える可能性のある最適化をコンパイラが無効にすることも保証します。

ただし、MemoryBarrier マクロの MSDN ドキュメントでは、コンパイラの並べ替えが常に防止されるわけではないことが示唆されています。

CPU が読み取り操作と書き込み操作の順序を変更できないようにするハードウェア メモリ バリア (フェンス) を作成します。また、コンパイラが読み取り操作と書き込み操作の順序を変更できなくなる場合もあります。

実際、コンパイラがメモリ操作の順序を変更できる場合、ハードウェア フェンスを使用する方法がわかりません。フェンスが適切な場所にあるかどうかはわかりません。

于 2014-03-09T13:00:20.550 に答える
2

tl;dr:
ファクトリ コールは、への割り当て後に移動される可能性のあるいくつかの手順を実行する可能性がありますm_pValue。式!m_pValueは、ファクトリ コールが完了する前に false を返し、2 番目のスレッドで不完全な戻り値を返します。

説明:

コンパイルはおそらく pValue への書き込みを省略しますが、関数呼び出しの前に代入を移動するような最適化は行わないでください。そうしないと、プログラムのセマンティクスに違反します。

必ずしも。T を と見なしint*、ファクトリ メソッドは新しい int を作成し、42 で初期化します。

int* pValue = new int(42);
m_pValue = pValue;         
//m_pValue now points to anewly allocated int with value 42.

コンパイラの場合、new式は別の前に移動できるいくつかのステップになります。そのセマンティクスは、割り当て、初期化、そして へのアドレスの割り当てですpValue

int* pTmp = new int;
*pTmp = 42;
int* pValue = *pTmp;

順次プログラムでは、一部のコマンドが他のコマンドの後に移動されても、セマンティクスは変わりません。特に、割り当ては、メモリ割り当てと最初のアクセスの間、つまり、新しい式の後のポインター値の割り当て後を含む、ポインターの 1 つの最初の逆参照の間で自由に移動できます。

int* pTmp = new int;
int* pValue = *pTmp;
m_pValue = pValue;  
*pTmp = 42;
//m_pValue now points to a newly allocated int with value 42.

コンパイラはおそらく、ほとんどの一時ポインタを最適化するためにこれを行います。

m_pValue = new int;  
*m_pValue = 42;
//m_pValue now points to a newly allocated int with value 42.

これは、順次プログラムの正しいセマンティクスです。

LeaveCriticalSection には暗黙的なハードウェア フェンスがあると思います。したがって、m_pValue への割り当て前の書き込みはすべて同期されます。

いいえ。フェンスは m_pValue への割り当ての後にありますが、コンパイラはそれとフェンスの間で整数の割り当てを移動できます。

m_pValue = new int;  
*m_pValue = 42;
LeaveCriticalSection();

Thread2 は CriticalSection に入る必要がないため、それでは遅すぎます。

Thread 1:                | Thread 2:
                         |
m_pValue = new int;      | 
                         | if (!m_pValue){     //already false
                         | }
                         | return m_pValue;
                         | /*use *m_pValue */
*m_pValue = 42;          |
LeaveCriticalSection();  |
于 2013-06-07T07:01:53.820 に答える