首页 > 解决方案 > MOVNTI 存储是否相对于同一线程创建的其他 MOVNTI 存储重新排序?

问题描述

TL;DR:我知道 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,它将等到“非临时”操作结果变得可观察。如果我不添加它,可能会在更改之前consumer观察。producer_indexqueue_data

但是如果我也写索引_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);
    }
}

根据我对英特尔手册的阅读,这应该行不通,因为非临时商店已经放松了订购。

但是他们不是说“放松”只是为了使非临时操作不对程序的其余部分下令吗?也许他们是在自己内部订购的,所以producer仍然会按预期工作?

而如果MOVNTI真的放宽了,以至于最新的代码不正确,那么内存写入要重新排序的原因是什么?

标签: c++x86memory-barriers

解决方案


movnti商店相对于彼此也是弱排序的。在 asm 中,您肯定需要sfence在存储数据后才能获得商店的发布语义producer_index,无论您是使用商店movnti还是普通mov商店。

在大多数情况下,在使用 NT 存储进行一些全行写入之后,单独的存储可能不会对其他线程可见。实际上很可能:完成一个缓存行会触发将 WC 缓冲区刷新到 DRAM(绕过/驱逐缓存),但索引肯定不会是完整的行存储,除非它恰好与写入数据的末尾连续。

在 C++ 中,这意味着_mm_sfence()在存储到producer_index.


请注意,使用movnti单个标量是一个非常糟糕的主意:它会强制将缓存行从缓存中逐出,因此读取器必须一直从 DRAM 中获取它。即,它将增加该控制变量的内核间延迟,否则可能会在 L3 中出现。

仅当您希望完成整个高速缓存行并且不希望另一个线程很快重新加载数据时才使用 NT 存储。


推荐阅读