语句重新排序是否适用于 conditional/control 语句?

Does statement re-ordering apply to conditional/control statements?

如其他帖子所述,没有任何类型的 volatilestd::atomic 限定,编译器 and/or 处理器可以自由地重新排序语句序列(例如赋值):

// this code
int a = 2;
int b = 3;
int c = a;
b = c;

// could be re-ordered/re-written as the following by the compiler/processor
int c = 2;
int a = c;
int b = a;

然而,条件语句和控制语句(例如 ifwhileforswitchgoto)也允许重新使用有序,或者它们本质上被认为是 "memory fence"?

int* a = &previously_defined_int;
int b = *a;

if (a == some_predefined_ptr)
{
   a = some_other_predefined_ptr; // is this guaranteed to occur after "int b = *a"?
}

如果可以重新排序上述语句(例如,将 a 存储在临时寄存器中,更新 a,然后通过取消引用 [=48= 来填充 b ] a 在临时寄存器中),我猜他们可能在单线程环境中仍然满足相同的 "abstract machine" 行为,那么为什么在使用 locks/mutexes 时没有问题?

bool volatile locked = false; // assume on given implementation, "locked" reads/writes occur in 1 CPU instruction
                              // volatile so that compiler doesn't optimize out

void thread1(void)
{
    while (locked) {}
    locked = true;
    // do thread1 things // couldn't these be re-ordered in front of "locked = true"?
    locked = false;
}

void thread2(void)
{
    while (locked) {}
    locked = true;
    // do thread2 things // couldn't these be re-ordered in front of "locked = true"?
    locked = false;
}

即使使用 std::atomic,非原子语句仍然可以围绕原子语句重新排序,因此这无助于确保 "critical section" 语句(即 "do threadX things") 包含在它们预期的临界区内(即 locking/unlocking 之间)。


编辑:实际上,我意识到锁示例实际上与我提出的 conditional/control 语句问题没有任何关系。不过,如果能对提出的两个问题进行澄清,那就太好了:

这里的"as-if" rule ([intro.abstract])很重要:

The semantic descriptions in this document define a parameterized nondeterministic abstract machine. This document places no requirement on the structure of conforming implementations. In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below

任何东西*都可以重新排序,只要实现可以保证结果程序的可观察行为不变。

如果没有栅栏和防止重新排序,线程同步构造通常无法正确实现。例如,该标准保证 lock/unlock 对互斥体的操作应表现为原子操作。 Atomics 还明确引入了栅栏,特别是关于 memory_order 指定的。这意味着依赖于(未放松的)原子操作的语句不能重新排序,否则程序的可观察行为可能会改变。

[intro.races] 大量谈论数据竞争和排序。

分支与汇编中的内存栅栏完全相反。推测执行 + 乱序执行意味着控制依赖性不是数据依赖性,因此例如 if(x) tmp = y; 可以加载 y 而无需等待 x 上的缓存未命中。参见

当然在 C++ 中,这只是意味着不,if() 没有帮助。 (基本上已弃用)memory_order_consume 的定义甚至可能指定 if 不是数据依赖项。真正的编译器将其提升为 acquire,因为它很难按照最初指定的方式实现。

所以TL:DR:如果你想在两个线程之间建立happens-before,你仍然需要mo_acquiremo_release(或更强)的原子。在 if() 中使用松散变量根本没有帮助,实际上在实际 CPU 上进行重新排序 更容易

当然,如果没有同步,非原子变量是不安全的。不过,if(data_ready.load(acquire)) 足以保护对非原子变量的访问。互斥量也是如此;根据 C++ 定义,mutex lock/unlock 算作对互斥对象的获取和释放操作。 (很多实际的实现都涉及全屏障,但形式上 C++ 只保证互斥量的 acq 和 rel)

编译器可以随意'Optimize'源代码而不影响程序的结果。

只要不影响程序的最终结果,优化可以是重新排序或删除不必要的语句。

例如,下面的赋值和'if'条件可以优化成一条语句:

优化前:

int a = 0;
int b = 20;
// ...
if (a == 0) {
    b = 10;
}

优化代码

int b = 10;

条件也可以重新排序,只要遵循规则的程序的行为不会受到重新排序的影响。 locks/mutexes 没有问题,因为优化只有在不破坏遵循规则的程序时才是合法的。正确使用锁和互斥量的程序遵循规则,因此实现时必须小心不要破坏它们。

您使用 while (locked) {} 的示例代码,其中 lockedvolatile 要么遵循平台规则,要么不遵循。在某些平台上,volatile 保证了使此代码正常工作的语义,并且该代码在这些平台上是安全的。但是在 volatile 没有指定多线程语义的平台上,所有的赌注都没有了。允许优化破坏代码,因为代码依赖于平台不保证的行为。

I am also wondering about the placement of thread function code outside of the "critical section"

查看您平台的文档。它要么保证操作不会围绕对 volatile 对象的访问进行重新排序,要么不会。如果没有,那么它可以自由地重新排序操作,而你依赖它不这样做是愚蠢的。

小心。曾经有一段时间,你常常别无选择,只能做这样的事情。但那是 long 以前的事了,现代平台提供了合理的原子操作、具有明确定义的内存可见性的操作等等。有新的优化出现的历史,这些优化显着提高了实际代码的性能,但打破了依赖于假定但不能保证的语义的代码。不要无缘无故地给问题添油加醋

在 Herb Sutter 的 talk 的 41:27,他指出 "opaque function calls"(我假设编译只能看到声明,但看不到定义的函数)需要一个完整的记忆障碍。因此,虽然 conditional/control 语句是完全透明的并且可以由 compiler/processor 重新排序,但解决方法可能是使用不透明的函数(例如#include 一个包含具有 "NOP" 实现的函数的库在源文件中)。