我正在使用“用于弱内存模型的正确和高效的工作窃取”中描述的工作人员双端队列。我希望队列项目的大小为 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 上这不是问题?
我做了一个实验,我删除了缓冲区项目上的原子限定符,并通过 volatile 指针对缓冲区进行了简单的分配。它似乎工作正常,但显然这并不确定!
如果这是不可能的,那么也许使用 cmpxchg16b 是加载/存储 16 字节我的具体情况的更好选择?或者通过将队列项作为索引并无锁分配索引的 16 字节插槽来使这一切变得复杂。
所以我的问题的简化版本是:在 x64 上,我可以简单地将 Array 缓冲区类型的定义更改为指向非原子限定 16 字节结构项目数组的 volatile 指针,并在上述函数中更改这些项目的加载和存储到简单的非原子赋值表达式?