标准 C++11 是否保证 memory_order_seq_cst 防止 StoreLoad 围绕原子对非原子重新排序?

Does standard C++11 guarantee that memory_order_seq_cst prevents StoreLoad reordering of non-atomic around an atomic?

标准 C++11 是否保证 memory_order_seq_cst 防止 StoreLoad 围绕非原子内存访问的原子操作重新排序?

众所周知,C++11 中有 6 个 std::memory_order,它指定 如何定期、非原子 内存访问围绕一个原子操作 - 工作草案,C++ 编程语言标准 2016-07-12:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4606.pdf

§ 29.3 Order and consistency

§ 29.3 / 1

The enumeration memory_order specifies the detailed regular (non-atomic) memory synchronization order as defined in 1.10 and may provide for operation ordering. Its enumerated values and their meanings are as follows:

另外,这 6 个 memory_orders 阻止了其中一些重新排序:

但是,memory_order_seq_cst 是否会阻止 StoreLoad 针对 常规非原子 内存访问的原子操作重新排序,或者仅针对具有相同 [=14= 的其他原子操作]?

即为了防止这种 StoreLoad 重新排序,我们应该对 STORE 和 LOAD 都使用 std::memory_order_seq_cst,还是只对其中之一使用?

std::atomic<int> a, b;
b.store(1, std::memory_order_seq_cst); // Sequential Consistency
a.load(std::memory_order_seq_cst); // Sequential Consistency

关于 Acquire-Release 语义很清楚,它指定跨原子操作的非原子内存访问重新排序:http://en.cppreference.com/w/cpp/atomic/memory_order


为了防止 StoreLoad 重新排序,我们应该使用 std::memory_order_seq_cst

两个例子:

  1. std::memory_order_seq_cst 对于 STORE 和 LOAD:MFENCE

StoreLoad 无法重新排序 - GCC 6.1.0 x86_64:https://godbolt.org/g/mVZJs0

std::atomic<int> a, b;
b.store(1, std::memory_order_seq_cst); // can't be executed after LOAD
a.load(std::memory_order_seq_cst); // can't be executed before STORE
  1. std::memory_order_seq_cst 仅用于 LOAD:没有 MFENCE

StoreLoad 可以重新排序 - GCC 6.1.0 x86_64:https://godbolt.org/g/2NLy12

std::atomic<int> a, b;
b.store(1, std::memory_order_release); // can be executed after LOAD
a.load(std::memory_order_seq_cst); // can be executed before STORE

此外,如果 C/C++-编译器使用 C/C++11 到 x86 的替代映射,它会在加载之前刷新存储缓冲区:MFENCE,MOV (from memory),因此我们必须使用std::memory_order_seq_cst 也用于 LOAD:http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html As this example is discussed in another question as approach (3): Does it make any sense instruction LFENCE in processors x86/x86_64?

即我们应该对 STORE 和 LOAD 使用 std::memory_order_seq_cst 以生成 MFENCE 保证,以防止 StoreLoad 重新排序。

对于原子加载或存储,memory_order_seq_cst 是真的吗:

std::memory_order_seq_cst gua运行tee 没有被编译器和 cpu 重新排序。在这种情况下,相同的内存顺序就好像一次只执行一条指令。

但是编译器优化混淆了问题,如果关闭-O3 则围栏是there

编译器可以看到在你的带有 -O3 的测试程序中没有 mfence 的结果,因为程序太简单了。

如果你 运行 它在另一方面像 this 一样的手臂上,你可以看到障碍物 dmb ish.

因此,如果您的程序更复杂,您可能会在这部分代码中看到 mfence,但如果编译器可以分析并推断不需要它,则不会。

不,标准 C++11 保证 memory_order_seq_cst 防止 StoreLoad 重新排序 non-atomic 围绕 atomic(seq_cst).

即使是标准的 C++11 也不能 保证 memory_order_seq_cst 会阻止 StoreLoadatomic(non-seq_cst) 的重新排序围绕 atomic(seq_cst).

工作草案,C++ 编程语言标准 2016-07-12:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4606.pdf

  • 所有 memory_order_seq_cst 操作都应有一个总顺序 S - C++11 标准:

§ 29.3

3

There shall be a single total order S on all memory_order_seq_cst operations, consistent with the “happens before” order and modification orders for all affected locations, such that each memory_order_seq_cst operation B that loads a value from an atomic object M observes one of the following values: ...

  • 但是,任何排序弱于 memory_order_seq_cst 的原子操作都没有顺序一致性,也没有单一的总顺序,即非 memory_order_seq_cst 操作可以用 memory_order_seq_cst 重新排序允许方向上的操作 - C++11 标准:

§ 29.3

8 [ Note: memory_order_seq_cst ensures sequential consistency only for a program that is free of data races and uses exclusively memory_order_seq_cst operations. Any use of weaker ordering will invalidate this guarantee unless extreme care is used. In particular, memory_order_seq_cst fences ensure a total order only for the fences themselves. Fences cannot, in general, be used to restore sequential consistency for atomic operations with weaker ordering specifications. — end note ]


C++ 编译器也允许这样的重新排序:

  1. 开x86_64

通常 - 如果在编译器中 seq_cst 在存储后实现为屏障,则:

STORE-C(relaxed); LOAD-B(seq_cst); 可以重新排序为 LOAD-B(seq_cst); STORE-C(relaxed);

GCC 7.0生成的Asm截图x86_64:https://godbolt.org/g/4yyeby

此外,理论上可行 - 如果在编译器中 seq_cst 在加载前实现为屏障,则:

STORE-A(seq_cst); LOAD-C(acq_rel); 可以重新排序为 LOAD-C(acq_rel); STORE-A(seq_cst);

  1. 在 PowerPC 上

STORE-A(seq_cst); LOAD-C(relaxed); 可以重新排序为 LOAD-C(relaxed); STORE-A(seq_cst);

在 PowerPC 上也可以这样重新排序:

STORE-A(seq_cst); STORE-C(relaxed); 可以重新排序为 STORE-C(relaxed); STORE-A(seq_cst);

如果连原子变量都可以跨原子(seq_cst)重新排序,那么非原子变量也可以跨原子(seq_cst)重新排序。

GCC 4.8 PowerPC 生成的 Asm 截图:https://godbolt.org/g/BTQBr8


更多详情:

  1. 开x86_64

STORE-C(release); LOAD-B(seq_cst); 可以重新排序为 LOAD-B(seq_cst); STORE-C(release);

Intel® 64 and IA-32 Architectures

8.2.3.4 Loads May Be Reordered with Earlier Stores to Different Locations

即x86_64代码:

STORE-A(seq_cst);
STORE-C(release); 
LOAD-B(seq_cst);

可以重新订购为:

STORE-A(seq_cst);
LOAD-B(seq_cst);
STORE-C(release); 

这可能发生,因为 c.storeb.load 之间不是 mfence:

x86_64 - GCC 7.0https://godbolt.org/g/dRGTaO

C++ 和 asm - 代码:

#include <atomic>

// Atomic load-store
void test() {
    std::atomic<int> a, b, c;
    a.store(2, std::memory_order_seq_cst);          // movl 2,[a]; mfence;
    c.store(4, std::memory_order_release);          // movl 4,[c];
    int tmp = b.load(std::memory_order_seq_cst);    // movl [b],[tmp];
}

可以重新排序为:

#include <atomic>

// Atomic load-store
void test() {
    std::atomic<int> a, b, c;
    a.store(2, std::memory_order_seq_cst);          // movl 2,[a]; mfence;
    int tmp = b.load(std::memory_order_seq_cst);    // movl [b],[tmp];
    c.store(4, std::memory_order_release);          // movl 4,[c];
}

此外,x86/x86_64中的顺序一致性可以通过四种方式实现:http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html

  1. LOAD (without fence) and STORE + MFENCE
  2. LOAD (without fence) and LOCK XCHG
  3. MFENCE + LOAD and STORE (without fence)
  4. LOCK XADD ( 0 ) and STORE (without fence)
  • 1 和 2 种方式:LOAD 和 (STORE+MFENCE)/(LOCK XCHG) - 我们在上面回顾过
  • 3 和 4 种方式:(MFENCE+LOAD)/LOCK XADDSTORE - 允许下一次重新排序:

STORE-A(seq_cst); LOAD-C(acq_rel); 可以重新排序为 LOAD-C(acq_rel); STORE-A(seq_cst);


  1. 在 PowerPC 上

STORE-A(seq_cst); LOAD-C(relaxed); 可以重新排序为 LOAD-C(relaxed); STORE-A(seq_cst);

允许存储加载重新排序(Table 5 - PowerPC):http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf

Stores Reordered After Loads

即PowerPC 代码:

STORE-A(seq_cst);
STORE-C(relaxed); 
LOAD-C(relaxed); 
LOAD-B(seq_cst);

可以重新订购为:

LOAD-C(relaxed);
STORE-A(seq_cst);
STORE-C(relaxed); 
LOAD-B(seq_cst);

PowerPC - GCC 4.8https://godbolt.org/g/xowFD3

C++ 和 asm - 代码:

#include <atomic>

// Atomic load-store
void test() {
    std::atomic<int> a, b, c;       // addr: 20, 24, 28
    a.store(2, std::memory_order_seq_cst);          // li r9<-2; sync; stw r9->[a];
    c.store(4, std::memory_order_relaxed);          // li r9<-4; stw r9->[c];
    c.load(std::memory_order_relaxed);              // lwz r9<-[c];
    int tmp = b.load(std::memory_order_seq_cst);    // sync; lwz r9<-[b]; ... isync;
}

通过将 a.store 分成两部分 - 它可以重新排序为:

#include <atomic>

// Atomic load-store
void test() {
    std::atomic<int> a, b, c;       // addr: 20, 24, 28
    //a.store(2, std::memory_order_seq_cst);            // part-1: li r9<-2; sync;
    c.load(std::memory_order_relaxed);              // lwz r9<-[c];
    a.store(2, std::memory_order_seq_cst);          // part-2: stw r9->[a];
    c.store(4, std::memory_order_relaxed);          // li r9<-4; stw r9->[c];
    int tmp = b.load(std::memory_order_seq_cst);    // sync; lwz r9<-[b]; ... isync;
}

从内存加载 lwz r9<-[c]; 比存储到内存 stw r9->[a]; 更早执行。


在 PowerPC 上也可以这样重新排序:

STORE-A(seq_cst); STORE-C(relaxed); 可以重新排序为 STORE-C(relaxed); STORE-A(seq_cst);

因为 PowerPC 的内存排序模型较弱 - 允许 Store-Store 重新排序 (Table 5 - PowerPC): http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf

Stores Reordered After Stores

即在 PowerPC 操作上,Store 可以与其他 Store 一起重新排序,那么前面的示例可以重新排序,例如:

#include <atomic>

// Atomic load-store
void test() {
    std::atomic<int> a, b, c;       // addr: 20, 24, 28
    //a.store(2, std::memory_order_seq_cst);            // part-1: li r9<-2; sync;
    c.load(std::memory_order_relaxed);              // lwz r9<-[c];
    c.store(4, std::memory_order_relaxed);          // li r9<-4; stw r9->[c];
    a.store(2, std::memory_order_seq_cst);          // part-2: stw r9->[a];
    int tmp = b.load(std::memory_order_seq_cst);    // sync; lwz r9<-[b]; ... isync;
}

其中存储到内存 stw r9->[c]; 比存储到内存 stw r9->[a]; 执行得早。