MOVNTI 商店是否相对于同一线程制作的其他 MOVNTI 商店重新排序?
Are MOVNTI stores reordered relative to other MOVNTI stores made by the same thread?
长话短说:
我知道 MOVNTI 操作相对于程序的其余部分没有排序,因此需要 SFENCE/MFENCE。
但是 MOVNTI 操作是否相对于同一线程的其他 MOVNTI 操作没有顺序?
假设我有一个生产者-消费者队列,我想在生产者端使用MOVNTI来避免缓存污染。
(还没有实际观察到缓存污染的影响,所以现在可能是理论问题)
所以我要更换以下制作人:
std::atomic<std::size_t> producer_index;
QueueElement queue_data[MAX_SIZE];
...
void producer()
{
for (;;)
{
...
queue_data[i].d1 = v1;
queue_data[i].d2 = v2;
...
queue_data[i].dN = vN;
producer_index.store(i, std::memory_order_release);
}
}
具有以下内容:
void producer()
{
for (;;)
{
...
_mm_stream_si64(&queue_data[i].d1, v1);
_mm_stream_si64(&queue_data[i].d2, v2);
...
_mm_stream_si64(&queue_data[i].dN, vN);
_mm_sfence();
producer_index.store(i, std::memory_order_release);
}
}
注意我添加了 _mm_sfence
,它会等到 "non-temporal" 操作结果变得可见。
如果我不添加它,consumer
可能会在 queue_data
更改之前观察到 producer_index
。
但是如果我也用 _mm_stream_si64
写索引呢?
std::size_t producer_index_value;
std::atomic_ref<std::size_t> producer_index { producer_index_value };
void producer()
{
for (;;)
{
...
_mm_stream_si64(&queue_data[i].d1, v1);
_mm_stream_si64(&queue_data[i].d2, v2);
...
_mm_stream_si64(&queue_data[i].dN, vN);
_mm_stream_si64(&producer_index_value, i);
}
}
根据我对 Intel 手册的阅读,这应该行不通,因为非临时存储已经放宽了排序。
但他们不是说 "relaxed" 只是为了使非临时操作不针对程序的其余部分排序吗?
也许它们是在它们自己内部排序的,所以 producer
仍然会按预期工作?
而如果MOVNTI真的放宽了,以至于最新的代码不正确,内存写入重新排序的原因是什么?
movnti
商店之间的排序也较弱。在 asm 中,无论是使用 movnti
还是普通的 mov
存储,在存储数据后你肯定需要 sfence
以获得存储的发布语义 producer_index
。
在使用 NT 存储进行一些全行写入之前,独立存储对其他线程是不可见的,这在大多数情况下可能会发生。实际上可能:完成缓存行会触发将 WC 缓冲区刷新到 DRAM(绕过/逐出缓存),但索引肯定不会是完整的行存储,除非它恰好与写入数据的末尾连续。
在 C++ 中,这意味着在存储到 producer_index
.
之前使用 _mm_sfence()
请注意,对单个标量使用 movnti
是一个非常糟糕的主意:它会强制将缓存行从缓存中逐出,因此 reader必须从 DRAM 中一路获取它。即它会增加该控制变量的内核间延迟,否则可能会在 L3 中命中。
仅当您希望完成整个缓存行并且您不希望另一个线程很快重新加载数据时才使用 NT 存储。
长话短说: 我知道 MOVNTI 操作相对于程序的其余部分没有排序,因此需要 SFENCE/MFENCE。 但是 MOVNTI 操作是否相对于同一线程的其他 MOVNTI 操作没有顺序?
假设我有一个生产者-消费者队列,我想在生产者端使用MOVNTI来避免缓存污染。
(还没有实际观察到缓存污染的影响,所以现在可能是理论问题)
所以我要更换以下制作人:
std::atomic<std::size_t> producer_index;
QueueElement queue_data[MAX_SIZE];
...
void producer()
{
for (;;)
{
...
queue_data[i].d1 = v1;
queue_data[i].d2 = v2;
...
queue_data[i].dN = vN;
producer_index.store(i, std::memory_order_release);
}
}
具有以下内容:
void producer()
{
for (;;)
{
...
_mm_stream_si64(&queue_data[i].d1, v1);
_mm_stream_si64(&queue_data[i].d2, v2);
...
_mm_stream_si64(&queue_data[i].dN, vN);
_mm_sfence();
producer_index.store(i, std::memory_order_release);
}
}
注意我添加了 _mm_sfence
,它会等到 "non-temporal" 操作结果变得可见。
如果我不添加它,consumer
可能会在 queue_data
更改之前观察到 producer_index
。
但是如果我也用 _mm_stream_si64
写索引呢?
std::size_t producer_index_value;
std::atomic_ref<std::size_t> producer_index { producer_index_value };
void producer()
{
for (;;)
{
...
_mm_stream_si64(&queue_data[i].d1, v1);
_mm_stream_si64(&queue_data[i].d2, v2);
...
_mm_stream_si64(&queue_data[i].dN, vN);
_mm_stream_si64(&producer_index_value, i);
}
}
根据我对 Intel 手册的阅读,这应该行不通,因为非临时存储已经放宽了排序。
但他们不是说 "relaxed" 只是为了使非临时操作不针对程序的其余部分排序吗?
也许它们是在它们自己内部排序的,所以 producer
仍然会按预期工作?
而如果MOVNTI真的放宽了,以至于最新的代码不正确,内存写入重新排序的原因是什么?
movnti
商店之间的排序也较弱。在 asm 中,无论是使用 movnti
还是普通的 mov
存储,在存储数据后你肯定需要 sfence
以获得存储的发布语义 producer_index
。
在使用 NT 存储进行一些全行写入之前,独立存储对其他线程是不可见的,这在大多数情况下可能会发生。实际上可能:完成缓存行会触发将 WC 缓冲区刷新到 DRAM(绕过/逐出缓存),但索引肯定不会是完整的行存储,除非它恰好与写入数据的末尾连续。
在 C++ 中,这意味着在存储到 producer_index
.
_mm_sfence()
请注意,对单个标量使用 movnti
是一个非常糟糕的主意:它会强制将缓存行从缓存中逐出,因此 reader必须从 DRAM 中一路获取它。即它会增加该控制变量的内核间延迟,否则可能会在 L3 中命中。
仅当您希望完成整个缓存行并且您不希望另一个线程很快重新加载数据时才使用 NT 存储。