2017-10-23 9 views
1

彼の驚くべき本C#を一言で言えば(無料の章はオンラインで入手できます)、メモリバリアが私たちに「リフレッシュ」価値を与える方法について話しています。彼の例は次のとおりです。MemoryBarrierは本当にリフレッシュ値を保証しますか?

static void Main() 
    { 
     bool complete = false; 
     var t = new Thread(() => 
     { 
      bool toggle = false; 
      while (!complete) 
      { 
       toggle = !toggle; 
      } 
     }); 
     t.Start(); 
     Thread.Sleep(1000); 
     complete = true; 
     t.Join();  // Blocks indefinitely 
    } 

リリースモードでビルドした場合は、このブロックが無期限にブロックされます。彼はこのブロックを解決するいくつかのソリューションを提供しています。 whileループでThread.MemoryBarrierを使用し、ロックを使用するか、または「完全な」揮発性静的フィールドを作成します。

私は揮発性フィールドの解決策に同意します。これは、volatileがJITのために読み込まれるレジスタではなく直接メモリの読み取りを強制するからです。しかし、私はこの最適化がフェンスと記憶の障壁とは無関係だと考えています。 JIT最適化の問題は、JITがメモリやレジスタから読み込むのが好きな場合だけです。代わりに、メモリバリアを使用するのでは、実際に、任意のメソッド呼び出しは、のようにすべてのレジスタを使用しないようにJITを「説得」:

class Program 
    { 
     [MethodImpl(MethodImplOptions.NoInlining)] 
     public static bool Toggle(bool toggle) 
     { 
      return !toggle; 
     } 
     static void Main() 
     { 
      bool complete = false; 
      var t = new Thread(() => 
      { 
       bool toggle = false; 
       while (!complete) 
       { 
        toggle = Toggle(toggle); 
       } 
      }); 
      t.Start(); 
      Thread.Sleep(1000); 
      complete = true; 
      t.Join();  // Blocks indefinitely 
     } 
    } 
ここ

私はダミーのトグルコールを作っています。生成されたアセンブリコードから、JITが "完全な"ローカル変数を読み取るためにダイレクトメモリアクセスを使用していることがはっきり分かります。したがって、私の前提は、少なくともIntel CPU上で、コンパイラの最適化を考慮すると、MemoryBarrierは「リフレッシュ」の観点から何の役割も持たない。 MemoryBarrierは完全なフェンスを注文します。それはそれです。私はそのように考えるのは正しいですか?私は揮発性などの揮発性のフィールド溶液で同意するだろう

+0

* 2つの要件があります。まず最初に、変数をレジスタに保存することができないことをジッタに納得させることです。それはかなり簡単にやめて、メソッド呼び出しで十分です。それを宣言する* volatile *はx86上で有効なスレッジハンマーです。または、ロックや連動のようなコードの同期に敏感な部分からの自然な方法。 Interlocked.Equals(toggle、false)を挿入するだけで十分です。もう1つは、バリアから取得するメモリアクセス順序です.Volalite.Read/Writeが最も効果的です。ジッタが変数volatileを扱うように強制しますので、1つで十分です –

+1

私はあなたに同意します:これはメモリバリアとは関係ありません。 –

+0

@HansPassant、 'volatile'は' Volatile.Read'/'Volatile.Write'と同じくらい有効なアプローチですが、あなたはそれらを使うのを忘れることはできません。また、 'Interlocked.Equals'は静的な' Object.Equals'だけで、同期に関連するものでは十分ではありません。 – acelent

答えて

1

は、JITのためのレジスタリードのではなく、読んで直接メモリを強制します。しかし、私はこの最適化がフェンスと記憶の障壁とは無関係だと考えています。

揮発性読み出し及び書き込みがECMA-335、I.12.6.7に記載されています。

揮発性読み出しは、CIL命令シーケンスの読み出し命令の後に発生するメモリへの参照より先に発生することが保証されていることを意味する "取得セマンティクス"を持ちます。揮発性書込みは、「解放セマンティクス」を有する。これは、書込みが、CIL命令シーケンスにおける書込み命令の前にメモリ参照の後に起こることが保証されることを意味する。

CLIの適合実装は、揮発性の操作のこのセマンティクスを保証するものとします。

ネイティブコードにCILを変換する最適化コンパイラは、任意の揮発性動作を削除しないものとし、またそれは、単一の操作に複数の揮発性動作を合体しなければなりません。

取得し、x86およびx86-64のアーキテクチャ用のセマンティクスを解放する(ハードウェア・メモリ・モデルは、揮発性のセマンティクスによって必要とされるよりも弱くないため)任意のメモリバリアを必要としません。しかし、ARMアーキテクチャでは、JITはハーフフェンス(一方向のメモリバリア)を放出する必要があります。

したがって、volatileを使用したこの例では、すべてが最適化の制限のために機能します。また、MemoryBarrierを使用すると、コンパイラはその変数の読み込みをループ外の単一の読み込みに最適化できないため、この読み込みはMemoryBarrierを越えることができないため機能します。

しかし、コード

while (!complete) 
{ 
    toggle = Toggle(toggle); 
} 

はこのようなものに最適化することが許可されている:

var tmp = complete; 
while (!tmp) 
{ 
    toggle = Toggle(toggle); 
} 

それはメソッド呼び出しの場合には起こらない理由は、いくつかの理由からです最適化は適用されませんでした(ただし、適用可能です)。したがって、標準に依存するのではなく、変更される可能性のある実装の詳細に依存するため、このコードは脆弱で実装固有です。

+0

すばらしい答え! MemoryBarrierがJITの動作をどのように変更するかについては、ある種の文書があることを願っていますが、それが言われたことは、あなたが言うことは論理的です。値をレジスタに格納すると、その値がメモリの境界を越えて保持され、最初にバリアの目的を無効にします。 –

+1

@OnurGumus jitの動作の詳細は実装の詳細ですが、RyuJITのソースコードまたはgithubのドキュメントを読むことができます。とにかく、あなたが頼りにすることができ、起こりうることを記述するECMA-335仕様があります。 –

+0

また、C#コンパイラが 'while(!complete)'ステートメントを最適化しない場合でも、JITコンパイラはそのような最適化を実行する可能性があります。そうでない場合、弱いメモリモデルのハードウェアアーキテクチャは、同期が欠如しているとき、何度も「完了」します。 – acelent

関連する問題