首先,我必须说这里提出的问题已经为我解决了,我想知道:
- 我误解了什么
- 如果编译器有错误(我知道这很罕见)(它是gcc 4.8.4)
我想计算二维向量的范数,该向量的坐标仅在该时刻计算。比方说,我想通过公式sqrt((x0-x1)**2+(y0-y1)**2)来计算||(x0,y0)-(x1,y1)||。只需要保存结果。
出于性能原因,平方是通过自乘完成的,我希望子运算和对变量的访问只进行一次。我希望总量在运行时是高效的,并以某种方式优雅地编码。我想了三种可能性:
- 重复两次
x0 - x1
和y0 - y1
,并希望编译器的优化步骤能够检测到重复 - 使用内联函数
- 将缓冲区变量和顺序运算符一起使用
我决定试试最后一个选项。
现在考虑以下代码:
#include <cmath>
#include <stdio.h>
int main (void)
{
float x0 (-1), x1 (2), y0 (13), y1 (9), result, tmp;
result = std::sqrt ((tmp = x0 - x1, tmp * tmp) + (tmp = y0 - y1, tmp * tmp));
printf ("%fn", result);
}
我知道我必须获得5.000000
,但我获得了5.656854
,它是sqrt((y0-y1)**2+((y0-y1)***2))。
我可以用得到想要的结果
#include <cmath>
#include <stdio.h>
int main (void)
{
float x0 (-1), x1 (2), y0 (13), y1 (9), result, tmp, tmp2;
result = std::sqrt ((tmp = x0 - x1, tmp * tmp) + (tmp2 = y0 - y1, tmp2 * tmp2));
printf ("%fn", result);
}
这就像顺序运算符的前几部分首先被求值一样,忽略了括号和第一个顺序运算符的返回值。看起来有点古怪;C++的定义中有什么东西我在这里遗漏了吗?
注意:在测试过程中,打开或关闭优化不会改变任何事情。
operator +
的两侧可以交错评估。特别是,括号中的两个赋值中的每一个都必须发生在紧接右侧的乘法之前(在正确的代码中,第一个变体不是),但不必发生在另一个赋值之前。此外,允许在一个赋值和匹配乘法之间发生另一个赋值。
因此,您的第一个变体调用未定义的行为,因为它包含tmp
的两个未排序的修改。因此,从字面上讲,每一个结果都是合法的,包括崩溃或NaN。
一般来说,请记住"您的代码很聪明"并不是一种恭维。保持简单,如果你真的必须优化减去(你很可能没有):
auto dx = x0 - x1;
auto dy = y0 - y1;
auto result = std::sqrt(dx * dx + dy * dy);
此行:
result = std::sqrt ((tmp = x0 - x1, tmp * tmp) + (tmp = y0 - y1, tmp * tmp));
您应该避免修改在同一表达式的其他地方使用的值。大多数运算符不能保证以任何特定的顺序计算其操作数(例外是&&、||、?:和逗号运算符)。在这种情况下,此表达式中+运算符的操作数可以按任何顺序求值,而不一定一次求值全部,因此您的代码具有未定义的行为。
此外,除非你已经分析了你的代码,并且知道你需要一个特定的语句来进行严格优化,否则你应该更喜欢清晰而不是巧妙的技巧。