4

以下のコードは、atomic フラグを介して共有状態を取得する 2 つの方法を示しています。リーダー スレッドは or を呼び出しpoll1()poll2()、ライターがフラグを通知したかどうかを確認します。

投票方法 #1:

bool poll1() {
    return (flag.load(std::memory_order_acquire) == 1);
}

投票オプション #2:

bool poll2() {
    int snapshot = flag.load(std::memory_order_relaxed);
    if (snapshot == 1) {
        std::atomic_thread_fence(std::memory_order_acquire);
        return true;
    }
    return false;
}

オプション #1 は以前の質問で提示され、オプション #2 はcppreference.com のサンプル コードに似ていることに注意してください。

poll関数が を返す場合にのみ共有状態を調べることに読者が同意すると仮定するとtrue、2 つのpoll関数はどちらも正しく同等でしょうか?

オプション #2 には標準名がありますか?

各オプションの利点と欠点は何ですか?

オプション #2 は、実際にはより効率的である可能性がありますか? 効率が悪い可能性はありますか?

完全な動作例を次に示します。

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

int x; // regular variable, could be a complex data structure

std::atomic<int> flag { 0 };

void writer_thread() {
    x = 42;
    // release value x to reader thread
    flag.store(1, std::memory_order_release);
}

bool poll1() {
    return (flag.load(std::memory_order_acquire) == 1);
}

bool poll2() {
    int snapshot = flag.load(std::memory_order_relaxed);
    if (snapshot == 1) {
        std::atomic_thread_fence(std::memory_order_acquire);
        return true;
    }
    return false;
}

int main() {
    x = 0;

    std::thread t(writer_thread);

    // "reader thread" ...  
    // sleep-wait is just for the test.
    // production code calls poll() at specific points

    while (!poll2()) // poll1() or poll2() here
      std::this_thread::sleep_for(std::chrono::milliseconds(50));

    std::cout << x << std::endl;

    t.join();
}
4

1 に答える 1

2

ほとんどの質問にお答えできると思います。

両方のオプションは確かに正しいですが、スタンドアロン フェンスの適用範囲がわずかに広いため、まったく同等ではありません (達成したいことに関しては同等ですが、スタンドアロン フェンスは技術的には他のものにも適用できます。まあ -- このコードがインライン化されていると想像してください)。スタンドアロン フェンスがストア/フェッチ フェンスとどのように異なるかの例は、Jeff Preshing によるこの投稿で説明されています。

オプション #2 のチェック後フェンス パターンには、私の知る限り名前がありません。しかし、それは珍しいことではありません。

パフォーマンスに関しては、私の g++ 4.8.1 on x64 (Linux) では、両方のオプションによって生成されたアセンブリは、1 つのロード命令に要約されます。とにかく、x86(-64) のロードとストアのすべてがハードウェア レベルで取得と解放のセマンティクスを持っていることを考えると、これは驚くことではありません (x86 は非常に強力なメモリ モデルで知られています)。

ただし、メモリ バリアが実際の個々の命令にコンパイルされる ARM の場合、次の出力が生成されます ( gcc.godbolt.comを とともに使用-O3 -DNDEBUG)。

の場合while (!poll1());:

.L25:
    ldr     r0, [r2]
    movw    r3, #:lower16:.LANCHOR0
    dmb     sy
    movt    r3, #:upper16:.LANCHOR0
    cmp     r0, #1
    bne     .L25

の場合while (!poll2());:

.L29:
    ldr     r0, [r2]
    movw    r3, #:lower16:.LANCHOR0
    movt    r3, #:upper16:.LANCHOR0
    cmp     r0, #1
    bne     .L29
    dmb     sy

唯一の違いは、同期命令 ( dmb) が配置されている場所 (for ループ内と for ループのpoll1後) だけであることがわかりpoll2ます。したがってpoll2、この実際のケースでは実際により効率的です:-) (ただし、フラグが変更されるまでブロックするためにループで呼び出された場合、これが問題にならない理由については、さらに読んでください。)

ARM64 の場合、バリアが組み込まれている特別なロード/ストア命令があるため ( ldar-> load-acquire)、出力は異なります。

の場合while (!poll1());:

.L16:
    ldar    w0, [x1]
    cmp     w0, 1
    bne     .L16

の場合while (!poll2());:

.L24:
    ldr     w0, [x1]
    cmp     w0, 1
    bne     .L24
    dmb     ishld

繰り返しpoll2ますが、内側にバリアがなく、外側に 1 つのループがありますpoll1が、通過するたびにバリアがあります。

さて、実際にどちらがよりパフォーマンスが高いかは、ベンチマークを実行する必要がありますが、残念ながら私はそのためのセットアップを持っていません. poll1poll2は、直観に反して、この場合は同じように効率的である可能性があります。これは、フラグ変数がとにかく伝播する必要がある効果の 1 つである場合、ループ内でメモリ効果が伝播するのを待つために余分な時間を費やしても、実際には時間を無駄にしない可能性があるためです (つまり、個々の (インライン化された) への呼び出しが への呼び出しpoll1よりも長くかかる場合でも、ループが終了するまでの合計時間は同じになる場合がありますpoll2)。もちろん、これはフラグが変更されるのを待っているループを想定しています。個々のpoll1 doの呼び出しは、 の個々の呼び出しよりも多くの作業を必要としpoll2ます。

したがって、コンパイラがインライン化されたときにブランチを削除できる限り、よりpoll2効率が大幅に低下することはなく、多くの場合より高速になる可能性があると言っても過言ではありません (少なくともこれら 3 つの一般的なアーキテクチャではそうであるようです)。 poll1)。

参照用の私の(わずかに異なる)テストコード:

#include <atomic>
#include <thread>
#include <cstdio>

int sharedState;
std::atomic<int> flag(0);

bool poll1() {
    return (flag.load(std::memory_order_acquire) == 1);
}

bool poll2() {
    int snapshot = flag.load(std::memory_order_relaxed);
    if (snapshot == 1) {
        std::atomic_thread_fence(std::memory_order_acquire);
        return true;
    }
    return false;
}

void __attribute__((noinline)) threadFunc()
{
    while (!poll2());
    std::printf("%d\n", sharedState);
}

int main(int argc, char** argv)
{
    std::thread t(threadFunc);
    sharedState = argc;
    flag.store(1, std::memory_order_release);
    t.join();
    return 0;
}
于 2016-02-10T13:25:25.247 に答える