3

「弱いメモリ モデルの正確で効率的なワークスティーリング」で説明されているように、ワーカー両端キューを使用しています。キュー アイテムのサイズを 16 バイトにしたいのですが、Intel/AMD Windows x64 と VS 2019 だけに関心があります。

16 バイト (たとえば __m128) で整列されたロード/ストアは、最新のプロセッサでは通常アトミックであることを理解していますが、仕様では保証されていません。

両端キューのタイプは次のとおりです。

typedef struct {
    atomic_size_t size;
    atomic_int buffer[];
} Array;


typedef struct {
    atomic_size_t top, bottom;
    Atomic(Array *) array;
} Deque;

重要なことに、配列バッファー項目は特にアトミック型を持っています。これをVS2019でコンパイルすると、スピンロックでバッファアイテムのサイズが肥大化することがわかります-これは望ましくありません。それを防ぐことは可能ですか?具体的には、特定の保証が付いている x64 のみを気にします。

両端キューに対するアクションは、次の関数によって与えられます。

int take(Deque* q) {
    size_t b = load_explicit(&q->bottom, relaxed) - 1;
    Array* a = load_explicit(&q->array, relaxed);
    store_explicit(&q->bottom, b, relaxed);
    thread_fence(seq_cst);
    size_t t = load_explicit(&q->top, relaxed);
    int x;
    if( t <= b ) {
        /* Non-empty queue. */
        x = load_explicit(&a->buffer[b % a->size], relaxed);
        if( t == b ) {
            /* Single last element in queue. */
            if( !compare_exchange_strong_explicit(&q->top, &t, t + 1, seq_cst, relaxed) )
                /* Failed race. */
                x = EMPTY;
            store_explicit(&q->bottom, b + 1, relaxed);
        }
    } else { /* Empty queue. */
        x = EMPTY;
        store_explicit(&q->bottom, b + 1, relaxed);
    }
    return x;
}


void push(Deque* q, int x) {
    size_t b = load_explicit(&q->bottom, relaxed);
    size_t t = load_explicit(&q->top, acquire);
    Array* a = load_explicit(&q->array, relaxed);
    if( b - t > a->size - 1 ) { /* Full queue. */
        resize(q);
        a = load_explicit(&q->array, relaxed);
    }
    store_explicit(&a->buffer[b % a->size], x, relaxed);
    thread_fence(release);
    store_explicit(&q->bottom, b + 1, relaxed);
}


int steal(Deque* q) {
    size_t t = load_explicit(&q->top, acquire);
    thread_fence(seq_cst);
    size_t b = load_explicit(&q->bottom, acquire);
    int x = EMPTY;
    if( t < b ) {
        /* Non-empty queue. */
        Array* a = load_explicit(&q->array, consume);
        x = load_explicit(&a->buffer[t % a->size], relaxed);
        if( !compare_exchange_strong_explicit(&q->top, &t, t + 1, seq_cst, relaxed) )
            /* Failed race. */
            return ABORT;
    }
    return x;
}

その多くは冗長であり、x64 で最適化する必要があります。実際、論文では、thread_fence(seq_cst) 行の take 関数でメモリ フェンスのみが必要であると指定されています。キュー アイテム タイプのサイズが 16 バイトの場合、これが正しいかどうかはわかりませんが?

take()/push() は同じスレッドで発生する必要があるように見えるため、それらの間に問題はありません。したがって、steal() を呼び出して部分的に書き込まれた 16 バイト項目を読み取るスレッドは危険です。しかし、push() は 16 バイトすべてが書き込まれた後にのみメモリ フェンスを実行し、その後のみ下部を更新するため、x64 ではこれは問題ではないようです。

バッファー項目のアトミック修飾子を削除し、揮発性ポインターを介してバッファーとの間でプレーンな割り当てを使用する実験を行いました。そして、それはうまくいくように見えましたが、明らかにそれは確実ではありません!

これが不可能な場合、おそらく cmpxchg16b を使用することは、私の特定のケースで 16 バイトをロード/保存するためのより良いオプションでしょうか? または、キュー アイテムをインデックスとして使用し、インデックス付きの 16 バイト スロットをロックなしで割り当てることによって、すべてが複雑になります。

したがって、私の質問の簡略版は次のとおりです。x64では、配列バッファータイプの定義を、アトミック修飾されていない16バイト構造体アイテムの配列への揮発性ポインターに変更し、上記の関数でそれらのアイテムのロードとストアを変更できますか?単純な非アトミック代入式に?

4

1 に答える 1

2

私は約16バイトのアトミックに答えようとしているので、これは部分的な答えにすぎないかもしれませんが、カウンターだけでなく、キューの要素をアトミックにする必要がある理由がわかりません。


MSVC 2019 でこれを実現する最も簡単な方法は、移植性のあるコードを書きながら、次のいずれかです。

  • 使用するstd::atomic_ref<16 bytes type>
  • #define _STD_ATOMIC_ALWAYS_USE_CMPXCHG16B
  • static_assertオンatomic_ref<...>::is_always_lock_freeにして、実際のロックフリーのアトミックを確実に取得します

また:

  • boost::atomic<16 bytes type>または_boost::atomic_ref<16 bytes type>
  • 繰り返しますが、本当のロックフリーのアトミックであることを確認するためstatic_assertis_always_lock_free

MSVC は に対して同じことを実装している可能性がありますが、std::atomic<16 bytes type>ABI の互換性の問題が原因でまだ実装されていません。(現在の ABI を壊す将来のバージョンでこれを実装することが期待されています)

これによりcmpxchg16b、リラックスしたロードやリラックスしたストアであっても、すべての操作が可能になります。そして、これはどの CPU でも保証できる最高のものです。

128 ビット タイプの単一命令ロード/ストアがありますが、アトミックであるとは限りません。SSE 命令を参照してください: どの CPU がアトミック 16B メモリ操作を実行できますか?


それにかんするvolatile

MSVC ではvolatile、読み取りと割り当ては/volatile:ms/によって制御され/volatile:isoます。/volatile (volatile キーワードの解釈)を参照してください。つまり/volatile:ms、x86-64 のデフォルトである を使用すると、ロードが取得され、ストアが解放されることになります。

でも:

  • 単純なロード/ストアは、どの CPU の 128 ビット タイプでもアトミックであることが保証されていません (上記のリンクされた回答を参照してください)。
  • CPU にとってアトミックであっても、128 ビット型はコンパイラにとってアトミックではないため、必ずしも単一の命令アクセスを生成するとは限りません。

したがって、ターゲット CPU でアトミックに動作する 128 ビットのロード/ストアに引き続き依存する場合は、組み込み関数_mm_load_si128/_mm_store_si128を使用alignas(16)し、適切なアライメントを確保するために を使用し、コンパイラの並べ替えを防ぐために_ReadWriteBarrierを使用することをお勧めします。これ機能する可能性があります (コンパイラは要求どおりに 128 ビットのロード/ストアを生成する可能性が高く、これらのロード/ストアは特定の CPU で実際にアトミックである可能性があります)。

于 2021-09-03T13:04:35.203 に答える