C 编译器优化器能否违反逻辑与表达式中操作数的短路和重新排序内存访问?

Can the C compiler optimizer violate short-circuiting and reorder memory accesses for operands in a logical-AND expression?

我们知道逻辑与运算符 (&&) 保证从左到右的计算。

但我想知道编译器优化器是否可以在以下代码中重新排序 *ab->foo 的内存访问指令,即优化器编写尝试访问 *b 的指令] 在访问 *a.

之前

(考虑 ab 都是指向堆中内存区域的指针。)

if (*a && b->foo) {
  /* do something */
}

有人可能会认为 && 会导致序列点,因此编译器必须在访问 *b 之前发出访问 *a 的指令,但在阅读 [=22= 处接受的答案之后],我不太确定。如果你看这个答案,语句之间有分号,它们也建立序列点,因此它们也应该防止重新排序,但那里的答案似乎表明尽管存在分号,它们仍需要编译器级别的内存屏障。

我的意思是,如果您声称 && 建立了一个序列点,那么 处的代码中的分号也是如此。那么为什么在该代码中需要编译器级别的内存屏障?

根据 C11 ISO 标准,在 §C 的附录 C 中指出

The following are the sequence points described in ... Between the evaluations of the first and second operands of the following operators: logical AND && (6.5.13); logical OR || (6.5.14); comma , (6.5.17).

并且,如 §5.1.2.3 中所述:

Sequenced before is an asymmetric, transitive, pair-wise relation between evaluations executed by a single thread, which induces a partial order among those evaluations. Given any two evaluations A and B, if A is sequenced before B, then the execution of A shall precede the execution of B.

因此可以保证第一个操作数在第二个操作数之前求值。在这种情况下应该不可能进行安全优化。

But I am wondering if the compiler optimizer can ever reorder the memory access instructions for *a and b->foo in the following code, i.e. the optimizer writes instructions that try to access *b before accessing *a.

if (*a && b->foo) {
  /* do something */
}

表达式的 C 语义要求首先计算 *a,并且仅当 *a 计算为非零时才计算 b->foo。 @Jack 的回答为标准提供了基础。但是你的问题是关于编译器执行的优化,标准规定

The semantic descriptions in this International Standard describe the behavior of an abstract machine in which issues of optimization are irrelevant.

(C2013, 5.1.2.3/1)

如果优化编译器产生相同的外部行为,它可能会产生不符合抽象语义的代码。

特别是,在您的示例代码中,如果编译器可以证明(或愿意假设)*ab->foo 的计算没有外部可见的行为并且是独立的 -两者都没有影响另一个的评估或副作用的副作用——那么它可能会在评估 *a 之前或之后无条件地发出评估 b->foo 的代码。请注意,如果 b 为 NULL 或包含无效指针值,则评估 b->foo 具有未定义的行为。在这种情况下,b->foo 的评估不独立于程序中的任何其他评估。

然而,正如@DavidSchwartz 所观察到的,即使 b 的值可能为空或无效,编译器仍可能能够发出 推测性地 继续进行的代码就好像它是有效的,并在事实并非如此的情况下回溯。这里的关键点是外部可见的行为不受有效优化的影响。

首先我想当然地认为&&代表逻辑AND运算符的内置版本。

我认为编译器可以在 && 运算符的右侧子表达式完成左侧的评估之前合法地执行评估,但其方式不会'不改变完整表达式的语义。

对于您的示例,允许 C++ 编译器在以下条件下引入重新排序:

  1. a 是原始指针(即它的类型不是重载 operator* 的 class)。
  2. b 是原始指针(即它的类型不是重载 operator-> 的 class)
  3. b 已知是可取消引用的,无论 *a
  4. 的值如何

如果 1. 不成立,那么用户定义的 operator* 可能会产生改变 b->foo.

值的副作用

如果 2. 不成立,那么用户定义的 operator-> 可能会更改 *a 的值,或者抛出,或者产生另一个可观察到的副作用(例如打印一些东西) *a 评估为 false.

不应该出现

如果 3. 不能通过静态分析证明,那么重新排序会引入原始程序中没有的未定义行为。

C编译器自然只需要执行第3次检查。

事实上,即使 *ab->foo 涉及运算符重载,当这些运算符可以内联并且编译器没有检测到任何危险时,C++ 编译器仍然可以重新排序一些指令。

系统可以评估 b->foo,直到它遇到超出其推测执行能力的东西。大多数现代系统都可以处理推测性故障并在结果证明操作结果从未被使用时忽略该故障。

所以这完全取决于编译器、CPU 和其他系统组件的能力。只要它能确保符合代码没有明显的后果,它就可以(几乎)在任何时候执行它想要的任何东西。