算术运算是否会受到编译器优化的影响?
Is there any case that an arithmetic operation is affected by compiler optimization?
这是一个一般性问题,但由于我主要处理 gcc/g++/VStudio,因此我将其标记为 c/c++。当我摆弄优化选项时,我想到了这个问题。在最简单的形式中,考虑一个算术运算,例如 i / 6 * 8
。如果一个人面对这个表达式,他很可能会将其简化为 i / 3 * 4
之类的东西。如果他更愿意乘以 4,他将首先这样做,即 (i * 4) / 3
。必须再次强调,这只是一个简单的例子。
现在编译器呢?他们是否有可能对此类操作做同样的事情?而既然我们知道在上面的例子中,如果i
是一个整数,那么简化和改变运算顺序可能会导致完全不同的结果,那么问题就可以改为:编译器是否完全避免了这样的行为?
如果我们希望程序完全按照我们所说的进行一些算术运算而不改变运算顺序,我们是否应该担心编译器的行为?
编译器很可能会对常量表达式应用 'constant folding' 和 'constant propagation' 优化。
在上述情况下,编译器无法应用此类优化。
想象一下
i = i * (4/2)
编译器会生成
i= i * 2
这是因为不断折叠。
编译器在优化代码方面非常保守。它们可能会改变运算的执行顺序,甚至会预先计算出编译时已知操作数的算术运算(这称为常量折叠),但它们永远不会改变计算结果。
浮点运算有点问题。通常,您不能在不更改计算结果的情况下更改计算顺序或预先计算。因此大多数编译器默认保留它们。然而,可以要求编译器积极优化;在这种情况下,计算结果可能会改变,但用户要求它。这是 gcc 选项 -Ofast
的例子(因为它在内部设置了选项 -ffast-math
)。请注意,它可能会导致奇怪的副作用,例如意外 "random" 除以零。
** 编辑:关于非算术运算的注释 **
当代码包含指针和函数调用时,优化会变得更加困难。一般来说,预测副作用是不可能的(想想指针别名和全局变量)。因此编译器总是以非常保守的方式放弃:一个好的编译程序至少应该是正确的,快速是一种奢侈。
** 编辑:一些示例 **
这个 SO 问题给出了一个非常详细的例子,说明了浮点会发生什么:Different floating point result with optimization enabled - compiler bug?
等式简化和编译器优化之间几乎没有共同之处。前者旨在使表达式对人类更具可读性,后者旨在使程序尽可能高效。像您所做的那样简化方程式不会产生更快的程序,因此编译器不会为此烦恼。
编译器无法将表达式重新排序为 i * 8 / 6
,因为这可能会改变代码的含义。基本上,编译器比人类数学家聪明得多,因为编译器完全了解类型,而人类可能缺乏这种意识。编程时,i / 6 * 8
是不等同于i * 8 / 6
!因为存在潜在的整数溢出问题。如果编译器不知道 i
将具有什么值,那么如果 i * 8
不能放入整数,则重新排序可能会导致溢出。
出于同样的原因,编译器也无法将代码更改为i / 3 * 4
。如果程序员 想要 溢出怎么办?该程序可能试图证明未定义的行为,或者可能存在针对溢出情况实施的编译器行为。如果编译器更改这些值,可能不会再发生溢出,并且程序行为会发生变化,这是不允许的。
更有可能的是,编译器会寻找一种方法,通过在编译时预先计算来删除其中一个操作。而且它可能还会寻找一种方法来用移位代替除法,因为传统上除法是一种缓慢的操作。实际将进行哪些优化取决于整个周围的代码。
正如其他答案所解释的那样,有效的编译器必须是保守的,并且不得采用任何会改变定义明确的程序的行为的优化。但重要的是要记住,这种保守主义只适用于有效的、正确编写的、定义明确的程序。如果正在编译的代码依赖于未定义的行为,现代编译器在他们采用的优化中可能是彻头彻尾的激进,在现实世界中,这意味着问题的答案实际上是,“是, 在某些情况下算术运算可能会受到编译器优化的影响。"
这里有两个很棒的网页,描述了编译器在遇到未定义行为时有时会应用的一些意义改变优化:
编程语言定义通常被描述为程序员和程序之间的 "contract" 作为一方,编译器及其实现者作为另一方。只要您的代码遵循所有规则,编译器就有义务生成行为与语言定义和 "abstract machine" 完全匹配的可执行文件。但是,如果您违反任何规则,特别是如果您的代码陷入任何未定义的行为,则所有赌注都将取消,合同将无效,编译器基本上可以为所欲为。
例如,如果你写
int i = 1;
printf("%d\n", i++ + i++); /* WRONG */
您很可能会发现表达式的值随着优化级别的改变而改变。
(不用说,这个故事的寓意是不是,"If you write undefined code, you'll have to be careful which optimization settings you use."正确的教训是,"Don't write code that depends on undefined behavior.")
这是一个一般性问题,但由于我主要处理 gcc/g++/VStudio,因此我将其标记为 c/c++。当我摆弄优化选项时,我想到了这个问题。在最简单的形式中,考虑一个算术运算,例如 i / 6 * 8
。如果一个人面对这个表达式,他很可能会将其简化为 i / 3 * 4
之类的东西。如果他更愿意乘以 4,他将首先这样做,即 (i * 4) / 3
。必须再次强调,这只是一个简单的例子。
现在编译器呢?他们是否有可能对此类操作做同样的事情?而既然我们知道在上面的例子中,如果i
是一个整数,那么简化和改变运算顺序可能会导致完全不同的结果,那么问题就可以改为:编译器是否完全避免了这样的行为?
如果我们希望程序完全按照我们所说的进行一些算术运算而不改变运算顺序,我们是否应该担心编译器的行为?
编译器很可能会对常量表达式应用 'constant folding' 和 'constant propagation' 优化。
在上述情况下,编译器无法应用此类优化。
想象一下
i = i * (4/2)
编译器会生成
i= i * 2
这是因为不断折叠。
编译器在优化代码方面非常保守。它们可能会改变运算的执行顺序,甚至会预先计算出编译时已知操作数的算术运算(这称为常量折叠),但它们永远不会改变计算结果。
浮点运算有点问题。通常,您不能在不更改计算结果的情况下更改计算顺序或预先计算。因此大多数编译器默认保留它们。然而,可以要求编译器积极优化;在这种情况下,计算结果可能会改变,但用户要求它。这是 gcc 选项 -Ofast
的例子(因为它在内部设置了选项 -ffast-math
)。请注意,它可能会导致奇怪的副作用,例如意外 "random" 除以零。
** 编辑:关于非算术运算的注释 **
当代码包含指针和函数调用时,优化会变得更加困难。一般来说,预测副作用是不可能的(想想指针别名和全局变量)。因此编译器总是以非常保守的方式放弃:一个好的编译程序至少应该是正确的,快速是一种奢侈。
** 编辑:一些示例 **
这个 SO 问题给出了一个非常详细的例子,说明了浮点会发生什么:Different floating point result with optimization enabled - compiler bug?
等式简化和编译器优化之间几乎没有共同之处。前者旨在使表达式对人类更具可读性,后者旨在使程序尽可能高效。像您所做的那样简化方程式不会产生更快的程序,因此编译器不会为此烦恼。
编译器无法将表达式重新排序为 i * 8 / 6
,因为这可能会改变代码的含义。基本上,编译器比人类数学家聪明得多,因为编译器完全了解类型,而人类可能缺乏这种意识。编程时,i / 6 * 8
是不等同于i * 8 / 6
!因为存在潜在的整数溢出问题。如果编译器不知道 i
将具有什么值,那么如果 i * 8
不能放入整数,则重新排序可能会导致溢出。
出于同样的原因,编译器也无法将代码更改为i / 3 * 4
。如果程序员 想要 溢出怎么办?该程序可能试图证明未定义的行为,或者可能存在针对溢出情况实施的编译器行为。如果编译器更改这些值,可能不会再发生溢出,并且程序行为会发生变化,这是不允许的。
更有可能的是,编译器会寻找一种方法,通过在编译时预先计算来删除其中一个操作。而且它可能还会寻找一种方法来用移位代替除法,因为传统上除法是一种缓慢的操作。实际将进行哪些优化取决于整个周围的代码。
正如其他答案所解释的那样,有效的编译器必须是保守的,并且不得采用任何会改变定义明确的程序的行为的优化。但重要的是要记住,这种保守主义只适用于有效的、正确编写的、定义明确的程序。如果正在编译的代码依赖于未定义的行为,现代编译器在他们采用的优化中可能是彻头彻尾的激进,在现实世界中,这意味着问题的答案实际上是,“是, 在某些情况下算术运算可能会受到编译器优化的影响。"
这里有两个很棒的网页,描述了编译器在遇到未定义行为时有时会应用的一些意义改变优化:
编程语言定义通常被描述为程序员和程序之间的 "contract" 作为一方,编译器及其实现者作为另一方。只要您的代码遵循所有规则,编译器就有义务生成行为与语言定义和 "abstract machine" 完全匹配的可执行文件。但是,如果您违反任何规则,特别是如果您的代码陷入任何未定义的行为,则所有赌注都将取消,合同将无效,编译器基本上可以为所欲为。
例如,如果你写
int i = 1;
printf("%d\n", i++ + i++); /* WRONG */
您很可能会发现表达式的值随着优化级别的改变而改变。
(不用说,这个故事的寓意是不是,"If you write undefined code, you'll have to be careful which optimization settings you use."正确的教训是,"Don't write code that depends on undefined behavior.")