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

时间:2016-06-27 18:57:06

标签: c logical-operators

我们知道逻辑AND运算符(&&)可以保证从左到右的评估。

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

(同时考虑ab指向堆中的内存区域。)

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

有人可能认为&&会导致序列点,因此编译器必须在访问*a之前发出访问*b的说明,但在https://stackoverflow.com/a/14983432/1175080读取接受的答案后,我不太确定。如果你看一下这个答案,语句之间有分号,它们也会建立序列点,因此它们也应该阻止重新排序,但是答案似乎表明它们需要编译器级别的内存屏障,尽管存在分号。

我的意思是,如果您声称&&建立了一个序列点,那么https://stackoverflow.com/a/14983432/1175080代码中的分号就是如此。那么为什么代码中需要编译器级别的内存屏障?

4 个答案:

答案 0 :(得分:6)

系统可以评估b->foo,直到它达到超出其推测性执行能力的时间。大多数现代系统可以处理推测性故障,如果事实证明从未使用过操作结果,则忽略该错误。

因此,它完全取决于编译器,CPU和其他系统组件的功能。只要它能确保对符合规范的代码没有明显的后果,它就可以(几乎)随时(几乎)执行它想要的任何东西。

答案 1 :(得分:4)

  

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

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

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

  

本国际标准中的语义描述描述了抽象机器的行为,其中优化问题无关紧要。

(C2013,5.1.2.3 / 1)

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

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

正如@DavidSchwartz所观察到的,即使b的值可能为空或无效,编译器仍然可以发出推测进行的代码,就好像它是有效的,如果事实证明并非如此,则会回溯。这里的关键点是外部可见行为不受有效优化的影响。

答案 2 :(得分:3)

根据C11 ISO标准,§C,附录C,表明

  

以下是中描述的序列点   ...在以下运算符的第一个和第二个操作数的评估之间:逻辑AND&& (6.5.13);逻辑OR || (6.5.14);逗号,(6.5.17)。

并且,如§5.1.2.3所述:

  

之前排序的是由单个线程执行的评估之间的不对称,传递,成对关系,这导致这些评估之间的部分顺序。给定任何两个评估A和B,如果A在B之前被排序,那么A的执行应该在B的执行之前。

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

答案 3 :(得分:3)

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

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

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

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

    如果1.不成立,则用户定义的operator*可能会产生更改b->foo值的副作用。

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

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

    当然,C编译器只需执行第3次检查。

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