13

C++11 メモリ モデルでは、メモリ負荷を取得負荷より上に上げることができないという印象を受けました。ただし、gcc 4.8 が生成するコードを見ると、すべてのメモリではなく、他のアトミック ロードにのみ当てはまるようです。それが真実で、ロードの取得がすべてのメモリを同期しない場合 (単にstd::atomics)、std::atomic に関して汎用ミューテックスを実装する方法がわかりません。

次のコード:

extern std::atomic<unsigned> seq;
extern std::atomic<int> data;

int reader() {
    int data_copy;
    unsigned seq0;
    unsigned seq1;
    do {
        seq0 = seq.load(std::memory_order_acquire);
        data_copy = data.load(std::memory_order_relaxed);
        std::atomic_thread_fence(std::memory_order_acquire);
        seq1 = seq.load(std::memory_order_relaxed);
    } while (seq0 != seq1);
    return data_copy;
}

プロデュース:

_Z6readerv:
.L3:
    mov ecx, DWORD PTR seq[rip]
    mov eax, DWORD PTR data[rip]
    mov edx, DWORD PTR seq[rip]
    cmp ecx, edx
    jne .L3
    rep ret

これは私には正しいように見えます。

ただし、データをintではなくに変更するstd::atomic:

extern std::atomic<unsigned> seq;
extern int data;

int reader() {
    int data_copy;
    unsigned seq0;
    unsigned seq1;
    do {
        seq0 = seq.load(std::memory_order_acquire);
        data_copy = data;
        std::atomic_thread_fence(std::memory_order_acquire);
        seq1 = seq.load(std::memory_order_relaxed);
    } while (seq0 != seq1);
    return data_copy;
}

これを生成します:

_Z6readerv:
    mov eax, DWORD PTR data[rip]
.L3:
    mov ecx, DWORD PTR seq[rip]
    mov edx, DWORD PTR seq[rip]
    cmp ecx, edx
    jne .L3
    rep ret

どうしたの?

4

3 に答える 3

4

負荷が取得の上に持ち上げられた理由

これをgcc bugzillaに投稿したところ、バグとして確認されました。

-1 の MEM エイリアス セット (ALIAS_SET_MEMORY_BARRIER) はこれを防ぐはずですが、PRE はこの特別なプロパティを認識しません (それを横切るすべての参照を "kill" する必要があります)。

gcc wikiには、これに関する素晴らしいページがあるようです。

一般に、リリースはコードを沈めるためのバリアであり、取得はコードをホイストするためのバリアです。

このコードがまだ壊れている理由

この論文によると、データ競合が発生するため、私のコードはまだ正しくありません。パッチを当てた gcc は正しいコードを生成しますがdatastd::atomic. その理由は、データ競合は、結果として生じる計算が破棄されたとしても、未定義の動作だからです。

AdamH.Peterson の好意による例:

int foo(unsigned x) {
    if (x < 10) {
        /* some calculations that spill all the 
           registers so x has to be reloaded below */
        switch (x) {
        case 0:
            return 5;
        case 1:
            return 10;
        // ...
        case 9:
            return 43;
        }
    }
    return 0;
}

ここで、コンパイラはスイッチをジャンプ テーブルに最適化する可能性があり、上記の if ステートメントのおかげで、範囲チェックを回避できます。ただし、データ競合が未定義の動作でない場合は、2 回目の範囲チェックが必要になります。

于 2013-05-29T20:25:59.110 に答える
1

あなたのatomic_thread_fenceが正しいとは思いません。コードで動作する唯一の C++11 メモリ モデルは seq_cst のものです。しかし、これは必要なものに対して非常に高価です (完全なメモリ フェンスを取得することになります)。

元のコードは機能し、これがパフォーマンスの最良のトレードオフだと思います。

あなたの更新に基づいて編集:

通常の int を使用したコードが思いどおりに機能しない正式な理由を探している場合は、引用したまさにその論文を信じます ( http://www.hpl.hp.com/techreports/2012/ HPL-2012-68.pdf ) に答えがあります。セクション 2 の最後を見てください。あなたのコードには、図 1 のコードと同じ問題があります。データ競合があります。複数のスレッドが、通常の int の同じメモリに対して同時に操作を実行できます。C++11 メモリ モデルで禁止されている場合、このコードは正式には有効な C++ コードではありません。

gcc は、コードにデータ競合がないこと、つまり有効な C++ コードであることを期待しています。競合がなく、コードが int を無条件にロードするため、ロードは関数本体のどこにでも発行できます。したがって、gcc はスマートであり、揮発性ではないため、1 回発行するだけです。通常、取得バリアと連携する条件文は、コンパイラの動作において重要な役割を果たします。

標準の正式なスラングでは、atomic ロードと通常の int ロードは順序付けされていません。たとえば、条件を導入すると、シーケンス ポイントが作成され、コンパイラはシーケンス ポイントの後に通常の int を評価するようになります ( http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx )。次に、C++ メモリ モデルが残りを行います (つまり、命令を実行する CPU による可視性を確保します)。

したがって、あなたの発言はどちらも真実ではありません。あなたは間違いなくc++ 11でロックを構築できますが、データ競合のあるものではありません:-)通常、ロックには読み取り前の待機が含まれます(これは明らかにここで避けようとしていることです)ので、この種の問題。

seq0 != seq1 だけをチェックしたくないため、元の seqlock にはバグがあることに注意してください (更新の途中である可能性があります)。seqlock ペーパーは正しい状態です。

于 2013-05-23T21:04:29.333 に答える