为 x86 编译 C++ 时会发生 StoreStore 重新排序
StoreStore reordering happens when compiling C++ for x86
while(true) {
int x(0), y(0);
std::thread t0([&x, &y]() {
x=1;
y=3;
});
std::thread t1([&x, &y]() {
std::cout << "(" << y << ", " <<x <<")" << std::endl;
});
t0.join();
t1.join();
}
首先,由于数据竞争,我知道它是UB。
但是,我只希望得到以下输出:
(3,1), (0,1), (0,0)
我确信不可能得到 (3,0)
,但我做到了。所以我很困惑-毕竟 x86 doesn't allow StoreStore reordering.
所以 x = 1
应该在 y = 3
之前全局可见
我想从理论上看输出 (3,0)
是不可能的,因为 x86 内存模型。我想它的出现是因为 UB。但我不确定。请解释。
除了 StoreStore 重新排序之外还有什么可以解释得到 (3,0)
?
您正在使用内存模型较弱的 C++ 编写。您没有对 prevent reordering at compile-time.
做任何事情
如果您查看 asm,您可能会发现存储以与源相反的顺序发生,and/or加载以与您期望的相反的顺序发生。
加载在源代码中没有任何顺序:如果编译器愿意,它可以在加载 y 之前加载 x,即使它们是 std::atomic
类型:
t2 <- x(0)
t1 -> x(1)
t1 -> y(3)
t2 <- y(3)
这甚至不是 "re"排序,因为一开始就没有定义的顺序:
std::cout << "(" << y << ", " <<x <<")" << std::endl;
不一定在 x
之前计算 y
。 <<
运算符具有从左到右的结合性,运算符重载只是
的语法糖
op<<( op<<(op<<(y),x), endl); // omitting the string constants.
因为 the order of evaluation of function arguments is undefined (even if we're talking about nested function calls),编译器可以在计算 op<<(y)
之前自由计算 x
。 IIRC,gcc 通常只是从右到左求值,必要时匹配将 args 推入堆栈的顺序。链接问题的答案表明情况经常如此。但当然,这种行为绝不能得到任何保证。
它们的加载顺序未定义,即使它们是 std::atomic
。我不确定 x
和 y
的计算之间是否存在 sequence point。如果不是,那么它将与计算 x+y
相同:编译器可以自由地以任何顺序计算操作数,因为它们是无序的。如果有一个序列点,那么就有一个顺序,但未定义哪个顺序(即它们的顺序不确定)。
略有相关:gcc doesn't reorder non-inline function calls in expression evaluation, to take advantage of the fact that C leaves the order of evaluation unspecified。我假设在内联后它确实优化得更好,但在这种情况下你没有给它任何理由支持在 x
.
之前加载 y
如何正确操作
关键是编译器决定重新排序的确切原因并不重要,只是允许。如果您不强加所有必要的排序要求,您的代码就会有错误,句号。它是否碰巧与某些具有特定周围代码的编译器一起工作并不重要;那只是意味着它是一个潜在的错误。
有关 how/why 的文档,请参阅 http://en.cppreference.com/w/cpp/atomic/atomic 这有效:
// Safe version, which should compile to the asm you expected.
while(true) {
int x(0); // should be atomic, too, because it can be read+written at the same time. You can use memory_order_relaxed, though.
std::atomic<int> y(0);
std::thread t0([&x, &y]() {
x=1;
// std::atomic_thread_fence(std::memory_order_release); // A StoreStore fence is an alternative to using a release-store
y.store(3, std::memory_order_release);
});
std::thread t1([&x, &y]() {
int tx, ty;
ty = y.load(std::memory_order_acquire);
// std::atomic_thread_fence(std::memory_order_acquire); // A LoadLoad fence is an alternative to using an acquire-load
tx = x;
std::cout << ty + tx << "\n"; // Don't use endl, we don't need to force a buffer flush here.
});
t0.join();
t1.join();
}
为了 Acquire/Release semantics 为您提供所需的顺序,最后一个存储必须是发布存储,获取加载必须是第一个加载。这就是为什么我将 y
设为 std::atomic,即使您将 x 设置为 0 或 1 更像是一个标志。
如果您不想使用 release/acquire,您可以在商店之间放置一个 StoreStore 栅栏,在负载之间放置一个 LoadLoad 栅栏。在 x86 上,这只会阻止编译时重新排序,但在 ARM 上你会得到一个内存屏障指令。 (请注意,y
在技术上仍然需要是原子的才能遵守 C 的数据竞争规则,但您可以在其上使用 std::memory_order_relaxed
。)
实际上,即使 Release/Acquire 为 y
排序,x
也应该是原子的 。即使我们看到 y==0
,x 的负载仍然会发生。所以线程2读x和线程1写y不同步,所以是UB。实际上,。但请记住 std::atomic
暗示其他语义,例如该值可以由其他线程异步更改的事实。
如果您在存储 i
和 -i
或其他内容的线程中循环,并在另一个线程中循环检查 abs( y) 总是 >= abs(x)。每次测试创建和销毁两个线程的开销很大。
当然,要做到这一点,你必须知道如何使用C生成你想要的asm(或者直接写成asm)。
while(true) {
int x(0), y(0);
std::thread t0([&x, &y]() {
x=1;
y=3;
});
std::thread t1([&x, &y]() {
std::cout << "(" << y << ", " <<x <<")" << std::endl;
});
t0.join();
t1.join();
}
首先,由于数据竞争,我知道它是UB。 但是,我只希望得到以下输出:
(3,1), (0,1), (0,0)
我确信不可能得到 (3,0)
,但我做到了。所以我很困惑-毕竟 x86 doesn't allow StoreStore reordering.
所以 x = 1
应该在 y = 3
我想从理论上看输出 (3,0)
是不可能的,因为 x86 内存模型。我想它的出现是因为 UB。但我不确定。请解释。
除了 StoreStore 重新排序之外还有什么可以解释得到 (3,0)
?
您正在使用内存模型较弱的 C++ 编写。您没有对 prevent reordering at compile-time.
做任何事情如果您查看 asm,您可能会发现存储以与源相反的顺序发生,and/or加载以与您期望的相反的顺序发生。
加载在源代码中没有任何顺序:如果编译器愿意,它可以在加载 y 之前加载 x,即使它们是 std::atomic
类型:
t2 <- x(0)
t1 -> x(1)
t1 -> y(3)
t2 <- y(3)
这甚至不是 "re"排序,因为一开始就没有定义的顺序:
std::cout << "(" << y << ", " <<x <<")" << std::endl;
不一定在 x
之前计算 y
。 <<
运算符具有从左到右的结合性,运算符重载只是
op<<( op<<(op<<(y),x), endl); // omitting the string constants.
因为 the order of evaluation of function arguments is undefined (even if we're talking about nested function calls),编译器可以在计算 op<<(y)
之前自由计算 x
。 IIRC,gcc 通常只是从右到左求值,必要时匹配将 args 推入堆栈的顺序。链接问题的答案表明情况经常如此。但当然,这种行为绝不能得到任何保证。
它们的加载顺序未定义,即使它们是 std::atomic
。我不确定 x
和 y
的计算之间是否存在 sequence point。如果不是,那么它将与计算 x+y
相同:编译器可以自由地以任何顺序计算操作数,因为它们是无序的。如果有一个序列点,那么就有一个顺序,但未定义哪个顺序(即它们的顺序不确定)。
略有相关:gcc doesn't reorder non-inline function calls in expression evaluation, to take advantage of the fact that C leaves the order of evaluation unspecified。我假设在内联后它确实优化得更好,但在这种情况下你没有给它任何理由支持在 x
.
y
如何正确操作
关键是编译器决定重新排序的确切原因并不重要,只是允许。如果您不强加所有必要的排序要求,您的代码就会有错误,句号。它是否碰巧与某些具有特定周围代码的编译器一起工作并不重要;那只是意味着它是一个潜在的错误。
有关 how/why 的文档,请参阅 http://en.cppreference.com/w/cpp/atomic/atomic 这有效:
// Safe version, which should compile to the asm you expected.
while(true) {
int x(0); // should be atomic, too, because it can be read+written at the same time. You can use memory_order_relaxed, though.
std::atomic<int> y(0);
std::thread t0([&x, &y]() {
x=1;
// std::atomic_thread_fence(std::memory_order_release); // A StoreStore fence is an alternative to using a release-store
y.store(3, std::memory_order_release);
});
std::thread t1([&x, &y]() {
int tx, ty;
ty = y.load(std::memory_order_acquire);
// std::atomic_thread_fence(std::memory_order_acquire); // A LoadLoad fence is an alternative to using an acquire-load
tx = x;
std::cout << ty + tx << "\n"; // Don't use endl, we don't need to force a buffer flush here.
});
t0.join();
t1.join();
}
为了 Acquire/Release semantics 为您提供所需的顺序,最后一个存储必须是发布存储,获取加载必须是第一个加载。这就是为什么我将 y
设为 std::atomic,即使您将 x 设置为 0 或 1 更像是一个标志。
如果您不想使用 release/acquire,您可以在商店之间放置一个 StoreStore 栅栏,在负载之间放置一个 LoadLoad 栅栏。在 x86 上,这只会阻止编译时重新排序,但在 ARM 上你会得到一个内存屏障指令。 (请注意,y
在技术上仍然需要是原子的才能遵守 C 的数据竞争规则,但您可以在其上使用 std::memory_order_relaxed
。)
实际上,即使 Release/Acquire 为 y
排序,x
也应该是原子的 。即使我们看到 y==0
,x 的负载仍然会发生。所以线程2读x和线程1写y不同步,所以是UB。实际上,std::atomic
暗示其他语义,例如该值可以由其他线程异步更改的事实。
如果您在存储 i
和 -i
或其他内容的线程中循环,并在另一个线程中循环检查 abs( y) 总是 >= abs(x)。每次测试创建和销毁两个线程的开销很大。
当然,要做到这一点,你必须知道如何使用C生成你想要的asm(或者直接写成asm)。