提问者:小点点

为什么GCC不使用LOAD(无Geofence)和STORE SFENCE来实现顺序一致性?


以下是在x86/x86_64中实现顺序一致性的四种方法:

  1. LOAD(无Geofence)和STORE MFENCE
  2. LOAD(不带Geofence)和LOCK XCHG
  3. MFENCE加载和存储(无Geofence)
  4. LOCK XADD(0)和STORE(不带Geofence)

就像这里写的:http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html

C/C 11操作x86实施

  • 加载Seq_Cst:MOV(从内存)
  • 存储Seq Cst:(LOCK)XCHG//替代:MOV(进入内存),MFENCE

注意:有一个C/C 11到x86的替代映射,它不是锁定(或Geofence)Seq Cst存储锁定/GeofenceSeq Cst负载:

  • 加载Seq_Cst:LOCK XADD(0)//替代:MFENCE,MOV(从内存)
  • 存储Seq Cst: MOV(进入内存)

GCC4.8.2(x86_64GDB)对C 11d::memory_order_seq_cst使用第一(1)种方法,即LOAD(无Geofence)和STORE MFENCE:

std::atomic<int> a;
int temp = 0;
a.store(temp, std::memory_order_seq_cst);
0x4613e8  <+0x0058>         mov    0x38(%rsp),%eax
0x4613ec  <+0x005c>         mov    %eax,0x20(%rsp)
0x4613f0  <+0x0060>         mfence

众所周知,那个MFENCE=LFENCE SFENCE。然后这段代码我们可以重写为:LOAD(不带Geofence)和STORE LFENCE SFENCE

问题:

  1. 为什么我们在LOAD之前不需要在这里使用LFENCE,而在STORE之后需要使用LFENCE(因为LFENCE只有在LOAD之前才有意义!)?
  2. 为什么GCC不对std::memory_order_seq_cst使用LOAD(无Geofence)和STORE SFENCE?

共3个答案

匿名用户

x86唯一能做的重新排序(对于正常的内存访问)是它可能会重新排序跟随存储的负载。

SFENCE保证Geofence前的所有存储在Geofence后的所有存储之前完成。LFENCE保证Geofence前的所有加载在Geofence后的所有加载之前完成。对于正常的内存访问,默认情况下已经提供了单个SFENCE或LFENCE操作的排序保证。基本上,LFENCE和SFENCE本身只对x86较弱的内存访问模式有用。

LFENCE、SFENCE和LFENCE SFENCE都不会阻止后跟加载的存储被重新排序。MFENCE会。

相关参考是Intel x86架构手册。

匿名用户

考虑以下代码:

#include <atomic>
#include <cstring>

std::atomic<int> a;
char b[64];

void seq() {
  /*
    movl    $0, a(%rip)
    mfence
  */
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
}

void rel() {
  /*
    movl    $0, a(%rip)
   */
  int temp = 0;
  a.store(temp, std::memory_order_relaxed);
}

关于原子变量“a”,seq()和rel()在x86架构上都是有序和原子的,因为:

  1. mov是一个原子指令
  2. mov是一个遗留指令,英特尔promise遗留指令的有序内存语义学与总是使用有序内存语义学的旧处理器兼容。

将常量值存储到原子变量中不需要栅栏。栅栏在那里是因为std::memory_order_seq_cst意味着所有内存都是同步的,而不仅仅是保存原子变量的内存。

效果可以通过以下set和get函数来演示:

void set(const char *s) {
  strcpy(b, s);
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
}

const char *get() {
  int temp = 0;
  a.store(temp, std::memory_order_seq_cst);
  return b;
}

strcpy是一个库函数,如果在运行时可用,它可能会使用较新的sse指令。由于sse指令在旧处理器中不可用,因此不需要向后兼容性,并且内存顺序未定义。因此,一个线程中的strcpy结果可能在其他线程中不直接可见。

上面的set和get函数使用原子值来强制内存同步,以便strcpy的结果在其他线程中可见。现在栅栏很重要,但是它们在对atomic::store的调用中的顺序并不重要,因为atomic::store内部不需要栅栏。

匿名用户

SFENCE LFENCE不是StoreLoad屏障(MFENCE),因此问题的前提是不正确的。(另请参阅我对同一用户相同问题的另一个版本的回答为什么(或不是?)SFENCE LFENCE等同于MFENCE?)

  • SFENCE可以通过(出现在)早期加载之前。(这只是一个StoreStore屏障)。
  • LFENCE可以通过早期的存储。(负载不能在任何方向上穿过它:负载屏障)。
  • 负载可以通过SFENCE(但商店不能通过LFENCE,因此它是LoadStore屏障和LoadLoad屏障)。

LFENCE SFENCE不包含任何阻止存储在稍后加载之前被缓冲的内容。MFENCE确实防止了这种情况。

普雷辛的博客文章更详细地解释了StoreLoad屏障的特殊之处,并有一个实际的工作代码示例,演示了在没有MFENCE的情况下重新排序。任何对内存排序感到困惑的人都应该从那个博客开始。

x86有一个强大的内存模型,每个普通存储都有发布语义学,每个普通加载都有获取语义学。这篇文章有细节。

LFENCE和SFENCE仅用于movnt存储,这些存储是弱有序的,并且绕过了缓存。(后来的SSE4.1movntdqa从WC内存加载也是弱有序的,但不是从WB内存加载,因此它不会绕过可缓存内存的缓存。)LFENCE在实践中主要是用于阻止非内存指令的乱序执行,如rdtsc。(在它被引入很久之后,在某些情况下阻止推测性执行以缓解幽灵。)

以防这些链接消失,在我对另一个类似问题的回答中还有更多信息。