与内核和编译器优化共享内存

时间:2018-07-22 19:03:52

标签: linux-kernel x86 memory-model

frame与内核共享。

用户空间代码:

read frame  // read frame content
_mm_mfence  // prevent before "releasing" a frame before we read everything.
frame.status = 0 // "release" a frame

内核代码:

poll for frame.status // reads a frame's status   
_mm_lfence

内核可以在另一个线程中异步轮询它。因此,用户空间代码和内核空间之间没有syscall


同步是否正确?

由于以下情况,我对此表示怀疑:

编译器的内存模型很弱,我们必须假定它可以进行疯狂的更改,正如您可以想象的那样,如果优化/更改的程序在单线程内是一致的。

因此,在我看来,我们需要第二个障碍,因为编译器有可能优化store frame.status, 0

是的,这将是一个非常疯狂的优化,但是如果编译器能够证明上下文中(线程内)没有人读取该字段,则可以对其进行优化。

我相信这是理论上的可能性,不是吗?

因此,为防止出现这种情况,我们可以设置第二个障碍:

用户空间代码:

read frame  // read frame content
_mm_mfence  // prevent before "releasing" a frame before we read everything.
frame.status = 0 // "release" a frame
_mm_fence

好的,现在编译器会在优化之前束手无策。

你怎么看?


编辑

[__mm_fenceoptimizations-out之前并没有阻止这个问题。

@PeterCordes,请确保自己:__mm_fence不会在优化之前阻止它(这只是x86内存屏障,不是编译器)。但是,atomic_thread_fence(any_order)可以防止在重新排序之前(显然,它取决于any_order),但是可以防止在优化之前进行重新排序?

例如:

   // x is an int pointer
   *x = 5 
   *(x+4) = 6 
   std::atomic_thread_barrier(memory_order_release)

在优化从商店到x之前预防?似乎必须-否则x的每个存储都应为volatile。 但是,我看到了很多无锁代码,没有making fieldsvolatile一样。

1 个答案:

答案 0 :(得分:0)

_mm_mfence也是 的编译器障碍。 (请参见When should I use _mm_sfence _mm_lfence and _mm_mfence,以及BeeOnRope的答案)。

带有release,rel_acq或seq_cst的

atomic_thread_fence阻止较早的存储与以后的存储合并。但是mo_acquire不必这样做。

写入非原子全局变量只能通过与写入相同非原子变量的 other 合并来优化,而不能通过完全优化它们来实现。因此,真正的问题是会发生什么重新排序,从而使两个非原子分配合并在一起。

必须在某处为原子变量赋值,以使任何其他线程可以与之同步的内容。一些编译器可能会给atomic_thread_fence带来更强的行为。非原子变量,但是在C ++ 11中,没有其他线程可以合法地观察有关*xx[4]

中的顺序的任何信息
#include <atomic>
std::atomic<int> shared_flag {0};
int x[8];

void writer() {
    *x = 0;
    x[4] = 0;
    atomic_thread_fence(mo_release);
    x[4] = 1;
    atomic_thread_fence(mo_release);
    shared_flag.store(1, mo_relaxed);
}

要存储到shared_flag的存储必须出现在存储到x[0]x[4]的后面,但这只是一个实现细节,详细说明了存储到x[0]x[4]的顺序,以及x[4]是否有2家商店。

例如,on the Godbolt compiler explorer gcc7和更早的版本将商店合并到x[4],但gcc8不会,而clang或ICC都不会。旧的gcc行为不是违反ISO C ++标准,但我认为它们加强了gcc的thread_fence,因为它的强度不足以防止其他情况下的错误。

例如,

void writer_gcc_bug() {
    *x = 0;
    std::atomic_thread_fence(std::memory_order_release);
    shared_flag.store(1, std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_release);
    *x = 2;  // gcc7 and earlier merge this, which arguably a bug
}

gcc仅按该顺序执行shared_flag = 1; *x = 2;。您可能会争辩说,看到*x之后,另一个线程无法安全地观察shared_flag == 1,因为 this 线程会立即将其再次写入而没有同步。 (即,任何潜在观察者中的数据竞争UB都可以使这种重新排序合法化。)

但是gcc开发人员认为这还不够,(这可能违反了__atomic标头用于实现API的内置<atomic>函数的保证)。在其他情况下,甚至存在一个真正的错误,即使是符合标准的程序也可能会观察到违反标准的激进重新排序。

显然,此问题在2017-09年更改为gcc bug 80640

  

Alexander Monakov wrote

     

我认为错误是x86 __atomic_thread_fence(x)x!=__ATOMIC_SEQ_CST上扩展为空时,应该放置类似于__atomic_signal_fence扩展的编译器障碍。

({__atomic_signal_fence包含与asm("" ::: "memory" )一样强的东西。)

是的,那肯定是一个错误。因此,并不是说gcc确实很聪明,并且允许重新排序,它主要是在thread_fence上失败了,并且发生的任何正确性都是由于其他因素造成的,例如非内联函数边界! (而且它不会优化原子,只会优化非原子。)