"double"类型的操作和C语言中的优化

4

最近我分析了一段旧代码,使用VS2005编译。由于“debug”(未进行优化)和“release”(/O2 /Oi /Ot选项)编译中存在不同的数值行为,因此我需要进行分析。这段(简化过的)代码如下:

void f(double x1, double y1, double x2, double y2)
{
double a1, a2, d;

a1 = atan2(y1,x1);
a2 = atan2(y2,x2);
d = a1 - a2;
if (d == 0.0) { // NOTE: I know that == on reals is "evil"!
   printf("EQUAL!\n");
}

如果使用相同的值对函数f(例如f(1,2,1,2))进行调用,则预期函数f应打印“EQUAL”,但是在“release”中并不总是发生这种情况。实际上,编译器已将代码优化为类似于d = a1-atan2(y2,x2)的形式,并完全删除了对中间变量a2的赋值。此外,它利用了第二个atan2()结果已经在FPU堆栈上的事实,因此重新加载FPU上的a1并减去了值。问题在于FPU以扩展精度(80位)工作,而a1仅为双精度(64位),因此将第一个atan2()结果保存在内存中实际上失去了精度。最终,d包含扩展和双精度之间的“转换错误”。
我知道应该避免使用浮点/双精度的等号(==运算符)。我的问题不在于如何检查双精度之间的接近程度。我的问题是关于局部变量赋值的“契约性”。按照我的“天真”观点,赋值应强制编译器将值转换为变量类型所表示的精度(在我的情况下为double)。如果变量是“float”,会怎样?如果它们是“int”(奇怪,但合法)呢?
简而言之,C标准对这些情况有什么规定?

在调试模式和发布模式下报告 FLT_EVAL_METHOD 的值。 - chux - Reinstate Monica
1
@chux,VS 2005甚至没有声称实现C99,因此它不必定义FLT_EVAL_METHOD。而且,并不是所有编译器都会按照它们所定义的方式执行(https://dev59.com/pmMm5IYBdhLWcg3wX-HT)。 - Pascal Cuoq
@GiuseppeGuerrini 赋值具有影响(转换为目标左值的类型,以及进一步删除在FLT_EVAL_METHOD> 2中未自动隐含转换的多余精度)。 这些效果是源代码的一部分,必须在汇编代码中发生。 除此之外,“赋值”是什么意思? 将值分配给寄存器是否算数? 如果块作用域变量x仅用作表达式x + 1的一部分,则永远不能将x分配,而应直接计算x + 1吗? - Pascal Cuoq
@GiuseppeGuerrini 我的意思是汇编代码中没有“赋值”的概念。唯一可以做出的要求是源代码中赋值的功能效果在二进制代码计算的结果中得到尊重。非功能性效果(例如,一个内存位置被值覆盖,花费时间进行写操作)不必发生(除了volatile变量,那是另一回事)。 - Pascal Cuoq
@Pascal:“什么是赋值?”你已经回答过了!由于编译器的目标应该是保留一组操作的最终结果,将中间/内部/临时(无用?)变量赋值是将中间值转换为特定类型的过程(在我的情况下并没有发生,呃……)。 - Giuseppe Guerrini
显示剩余7条评论
1个回答

5
根据我的“天真”观点,赋值应该强制编译器将一个值转换为变量类型所表示的精度(在我的情况下是double)。是的,这就是C99标准所说的。请参见以下内容。C99标准允许在某些情况下计算浮点运算的精度高于类型所暗示的精度:在standard中查找FLT_EVAL_METHODFP_CONTRACT,这两个结构与超额精度有关。但我不知道是否有任何可以被解释为编译器允许任意从计算精度减少浮点值精度到类型精度的单词。在严格解释标准的情况下,这只能在特定的位置,如赋值和转换中以确定方式发生。
最好阅读Joseph S. Myers's analysis的相关部分,与FLT_EVAL_METHOD有关:

C99允许按照一定规则进行超出范围和精度的计算。这些规则在5.2.4.2.2第8段中概述:

除了赋值和强制类型转换(它们会删除所有额外的范围和精度),带有浮点操作数和经过通常算术转换的值以及浮点常量的操作的值将被计算为格式,其范围和精度可能大于类型所需的。使用评估格式的特征是由FLT_EVAL_METHOD的实现定义值来描述的:

Joseph S. Myers接着描述了GCC在他的帖子附带的补丁之前的情况。这种情况与您的编译器(以及无数其他编译器)一样糟糕:

GCC在使用x87浮点数时,将FLT_EVAL_METHOD定义为2。然而,它的实现不符合C99对FLT_EVAL_METHOD == 2的要求,因为它是通过后端假装处理器支持SFmode和DFmode操作来实现的:
1. 有时,根据优化的情况,一个值可能会以SFmode或DFmode的形式溢出到内存中,因此会在不可预测的地方丢失多余精度。
2. 赋值通常不会丢失多余精度,尽管-ffloat-store可能会增加它丢失的可能性。
C++标准继承了从C99继承的math.h的定义,而math.h是定义FLT_EVAL_METHOD的头文件。因此,您可能希望C++编译器也遵循这个规定,但他们似乎没有像认真对待这个问题。即使G++仍然不支持-fexcess-precision=standard,尽管它使用与GCC相同的后端(自Joseph S. Myers的文章和相关补丁以来,GCC已经支持这个选项)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接