「弱いメモリ モデルの正確で効率的なワークスティーリング」で説明されているように、ワーカー両端キューを使用しています。キュー アイテムのサイズを 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バイト構造体アイテムの配列への揮発性ポインターに変更し、上記の関数でそれらのアイテムのロードとストアを変更できますか?単純な非アトミック代入式に?