我们知道逻辑AND运算符(&&
)可以保证从左到右的评估。
但我想知道编译器优化器是否可以在以下代码中重新排序*a
和b->foo
的内存访问指令,即优化器会先写入尝试访问*b
的指令访问*a
。
(同时考虑a
和b
指向堆中的内存区域。)
if (*a && b->foo) {
/* do something */
}
有人可能认为&&
会导致序列点,因此编译器必须在访问*a
之前发出访问*b
的说明,但在https://stackoverflow.com/a/14983432/1175080读取接受的答案后,我不太确定。如果你看一下这个答案,语句之间有分号,它们也会建立序列点,因此它们也应该阻止重新排序,但是答案似乎表明它们需要编译器级别的内存屏障,尽管存在分号。
我的意思是,如果您声称&&
建立了一个序列点,那么https://stackoverflow.com/a/14983432/1175080代码中的分号就是如此。那么为什么代码中需要编译器级别的内存屏障?
答案 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)
如果生成相同的外部行为,优化编译器可以生成不符合抽象语义的代码。
特别是,在您的示例代码中,如果编译器可以证明(或者愿意假设)*a
和b->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 ++编译器在以下条件下引入重新排序:
a
是一个原始指针(即它的类型不是重载operator*
的类)。 b
是一个原始指针(即它的类型不是重载operator->
的类)b
的价值如何,*a
都可以取消引用
醇>
如果1.不成立,则用户定义的operator*
可能会产生更改b->foo
值的副作用。
如果2.不成立,则用户定义的operator->
可能会更改*a
的值,或者抛出或产生另一个可观察的副作用(例如打印某些内容)不应该*a
评估为false
。
如果无法通过静态分析证明3.那么重新排序会引入原始程序中不存在的未定义行为。
当然,C编译器只需执行第3次检查。事实上,即使*a
和b->foo
涉及运算符重载,C ++编译器仍然可以重新排序某些指令,因为这些运算符可以内联并且编译器没有检测到任何危险的内容。