是否存在算术运算受到编译器优化影响的情况

Is there any case that an arithmetic operation is affected by compiler optimization?

本文关键字:优化 影响 情况 编译器 存在 算术运算 是否      更新时间:2023-10-16

这是一个常见的问题,但由于我主要处理gcc/g++/VStudio,所以我将其标记为c/c++。这个问题是我在考虑优化选项时想到的。在最简单的形式中,考虑一个算术运算,例如i / 6 * 8。如果一个人面对这个表情,他很可能会把它简化为类似i / 3 * 4的东西。如果他更喜欢乘4,他会首先这样做,即(i * 4) / 3。我必须再次强调,这只是一个简单的例子。

那么编译器呢?他们是否有可能对这些行动采取同样的做法?既然我们知道在上面的例子中,如果i是一个整数,那么简化和改变运算顺序可能会导致完全不同的结果,那么问题可以改为:编译器是否完全避免了这种操作?

如果我们希望程序完全按照我们所说的进行一些算术运算,并且不改变运算顺序,我们应该担心编译器的行为吗?

编译器很可能会对常量表达式应用"常量折叠"answers"常量传播"优化。

在上面的情况下,编译器不能应用这样的优化。

想象一下

i = i * (4/2)

编译器将生成

i= i * 2

这是因为不断的折叠。

编译器在优化代码方面非常保守。它们可能会改变运算的执行顺序,甚至会预先计算在编译时已知操作数的算术运算(这称为常数折叠),但它们永远不会改变计算结果。浮点运算有点麻烦。在不更改计算结果的情况下,通常无法更改计算顺序或预计算。因此,大多数编译器默认情况下保持原样。然而,可以要求编译器积极地进行优化;在这种情况下,计算结果可能会更改,但用户要求更改。例如,gcc选项-Ofast的情况(因为它在内部设置了选项-ffast-math)。请注意,它可能会导致奇怪的副作用,如意外的"随机"除以零。

**编辑:关于非算术运算的注释**

当代码包含指针和函数调用时,优化会变得更加困难。一般来说,预测副作用是不可能的(比如指针别名和全局变量)。因此,编译器总是以非常保守的方式放弃:一个好的编译程序至少应该是正确的,速度快是一种奢侈。

**编辑:一些例子**

这个SO问题给出了一个非常详细的例子,说明浮点会发生什么:启用优化的浮点结果不同——编译器错误?

方程简化和编译器优化之间几乎没有共同点。前者旨在使表达式更易于人类阅读,后者旨在使程序尽可能高效。像你所做的那样简化方程不会产生更快的程序,所以编译器不会为此而烦恼。

编译器无法将表达式重新排序为i * 8 / 6,因为这可能会更改代码的含义。基本上,编译器比人类数学家聪明得多,因为编译器完全了解类型,而人类可能缺乏这种意识。编程时,i / 6 * 8而不是等价于i * 8 / 6!因为存在潜在的整数溢出问题。如果编译器不知道i的值,那么如果i * 8不能容纳在整数中,则重新排序可能会导致溢出。

出于同样的原因,编译器也不能将代码更改为i / 3 * 4。如果程序员想要溢出怎么办?该程序可以尝试演示未定义的行为,也可以针对溢出情况实现编译器行为。如果编译器更改值,则可能不再存在溢出,并且程序行为将发生更改,这是不允许的。

更有可能的是,编译器会在编译时通过预先计算来寻找删除其中一个操作的方法。它可能也会寻找一种方法来用比特移位取代除法,因为除法传统上是一种缓慢的操作。实际要做的优化取决于周围的整个代码。

正如其他答案所解释的,有效的编译器必须是保守的,并且不能使用任何会改变定义良好的程序行为的优化。但重要的是要记住,这种保守主义只适用于有效的、正确编写的、定义良好的程序。如果正在编译的代码依赖于未定义的行为,那么现代编译器在使用优化时可能会非常激进,而在现实世界中,这意味着所述问题的答案实际上是,",在某些情况下,算术运算可能会受到编译器优化的影响。"

以下是两个很棒的网页,描述了编译器在遇到未定义行为时有时会应用的一些意义改变优化:

  • "每个C程序员应该知道的关于未定义行为的知识"

  • "C和C++中未定义行为指南"

编程语言定义通常被描述为程序员和程序作为一方,编译器及其实现者作为另一方之间的"合同"。只要你的代码遵循所有规则,编译器就有义务生成一个行为与语言定义和"抽象机器"完全匹配的可执行文件。但是,如果你违反了任何规则,特别是如果你的代码陷入了任何未定义的行为,那么所有的赌注都会落空,合同无效,编译器基本上可以随心所欲。

例如,如果您编写

int i = 1;
printf("%dn", i++ + i++);    /* WRONG */

您很可能会发现,表达式的值会随着优化级别的更改而更改。

(不用说,这个故事的寓意是而不是,"如果你写了未定义的代码,你必须小心使用哪些优化设置。"正确的教训是,"不要写依赖于未定义行为的代码。")