为什么返回浮点值会改变其值?

以下代码在Red Hat 5.4 32位上引发了assert ,但在Red Hat 5.4 64位(或CentOS)上运行。

在32位上,我必须将millis2seconds的返回值放在一个变量中,否则会引发assert ,表明函数返回的double值与传递给它的值不同。

如果你评论“#define BUG”行,它就可以了。

感谢@R,将-msse2 -mfpmath选项传递给编译器,使得millis2seconds函数的两个变量都能正常工作。

 /* * TestDouble.cpp */ #include  #include  #include  static double millis2seconds(int millis) { #define BUG #ifdef BUG // following is not working on 32 bits architectures for any values of millis // on 64 bits architecture, it works return (double)(millis) / 1000.0; #else // on 32 bits architectures, we must do the operation in 2 steps ?!? ... // 1- compute a result in a local variable, and 2- return the local variable // why? somebody can explains? double result = (double)(millis) / 1000.0; return result; #endif } static void testMillis2seconds() { int millis = 10; double seconds = millis2seconds(millis); printf("millis : %d\n", millis); printf("seconds : %f\n", seconds); printf("millis2seconds(millis) : %f\n", millis2seconds(millis)); printf("seconds < millis2seconds(millis) : %d\n", seconds  millis2seconds(millis) : %d\n", seconds > millis2seconds(millis)); printf("seconds == millis2seconds(millis) : %d\n", seconds == millis2seconds(millis)); assert(seconds == millis2seconds(millis)); } extern int main(int argc, char **argv) { testMillis2seconds(); } 

使用在Linux x86系统上使用的cdecl调用约定,使用st0 x87寄存器从函数返回double。 所有x87寄存器都是80位精度。 使用此代码:

 static double millis2seconds(int millis) { return (double)(millis) / 1000.0; }; 

编译器使用80位精度计算除法。 当gcc使用标准的GNU方言(默认情况下它)时,它会将结果保留在st0寄存器中,因此将完整的精度返回给调用者。 汇编代码的结尾如下所示:

 fdivrp %st, %st(1) # Divide st0 by st1 and store the result in st0 leave ret # Return 

有了这段代码,

 static double millis2seconds(int millis) { double result = (double)(millis) / 1000.0; return result; } 

结果存储在64位内存位置,这会失去一些精度。 在返回之前,64位值被加载回80位st0寄存器,但损坏已经完成:

 fdivrp %st, %st(1) # Divide st0 by st1 and store the result in st0 fstpl -8(%ebp) # Store st0 onto the stack fldl -8(%ebp) # Load st0 back from the stack leave ret # Return 

在您的main中,第一个结果存储在64位内存位置,因此无论如何都会丢失额外的精度:

 double seconds = millis2seconds(millis); 

但在第二次调用中,返回值是直接使用的,因此编译器可以将它保存在寄存器中:

 assert(seconds == millis2seconds(millis)); 

当使用millis2seconds的第一个版本时,您最终将截断的值与64位精度进行比较,并将其与完全80位精度的值进行比较,因此存在差异。

在x86-64上,使用SSE寄存器进行计算,这些寄存器仅为64位,因此不会出现此问题。

此外,如果使用-std=c99以便不获取GNU方言,则计算值将存储在内存中并在返回之前重新加载到寄存器中,以便符合标准。

在i386(32位x86)上,所有浮点表达式都被评估为80位IEEE扩展浮点类型。 这反映在FLT_EVAL_METHOD ,来自float.h,定义为2.将结果存储到变量或对结果应用FLT_EVAL_METHOD会通过舍入减少多余的精度,但这仍然不足以保证您在上面看到的相同结果一个没有过多精度的实现(如x86_64),因为舍入两次可以得到与在同一步骤中执行计算和舍入不同的结果。

绕过这个问题的一种方法是使用SSE数学建立甚至在x86目标上,使用-msse2 -mfpmath=sse

首先值得注意的是,由于函数是隐式纯的并且使用常量参数调用两次,因此编译器将完全放弃计算和比较的权利。

clang-3.0-6ubuntu3确实消除了使用-O9的纯函数调用,并在编译时进行所有浮点计算,因此程序成功。

C99标准, ISO / IEC 9899 ,说

浮动操作数的值和浮动表达式的结果可以以比该类型所需的精度和范围更高的精度和范围来表示; 因此不改变类型。

因此编译器可以自由地传回80位值,正如其他人所描述的那样。 但是,标准继续说:

转换和赋值运算符仍然需要执行指定的转换。

这解释了为什么专门分配一个double强制值降低到64位并从函数返回double不会。 这对我来说非常令人惊讶。

但是,看起来C11标准实际上会通过添加此文本来减少这种混乱:

如果以不同于返回类型的浮点格式计算返回表达式,则表达式将被转换,就好像通过赋值[删除任何额外的范围和精度]到函数的返回类型,并将结果值返回给呼叫者。

因此,此代码基本上是在不同的点上执行未指定的行为,以确定值是否被截断。


对我来说,在Ubuntu Precise上,使用-m32

  • clang
  • clang -O9也过去了
  • gcc ,断言失败
  • gcc -O9通过,因为它也消除了常量表达式
  • gcc -std=c99失败
  • gcc -std=c1x也失败了(但它可能适用于以后的gcc)
  • gcc -ffloat-store通过,但似乎有持续消除的副作用

我不认为这是一个gcc bug,因为标准允许这种行为,但clang行为更好。

除了在其他答案中解释的所有细节之外,我会说有一个非常简单的规则,关于在Fortran中几乎任何编程语言中使用浮点类型: 从不检查浮点值是否具有精确的相等性 。 关于80位和64位值的所有知识都是正确的,但对于某个硬件和某个编译器来说确实如此(是的,如果您更改编译器甚至打开或关闭优化,可能会发生变化)。 更通用的规则(适用于任何可移植的代码 )是浮点值通常不像整数或字节序列,并且可以更改,例如在复制时,并且检查它们的相等性通常具有不可预测的结果。

因此,即使它在测试中起作用,通常最好不要这样做。 当事情发生变化时,它可能会失败。

UPD:虽然有些人已经投票,但我坚持认为这个建议一般都是正确的。 似乎只是复制一个值的东西(它们从高级编程语言程序员的角度来看是这样的;在最初的例子中发生的是一个典型的例子,返回值并将其放入变量中 – 瞧 – 它被改变了!),可以改变浮点值。 比较浮点值是否存在相等或不平等通常是一种不好的做法,只有在您知道为什么在某些情况下可以这样做时才允许这种做法。 编写可移植程序通常需要最小化低级知识。 是的,当放入浮点变量或复制时,更改像0或1这样的整数值是不太可能的。 但是更复杂的值(在上面的例子中,我们看到了简单算术表达式的结果会发生什么!)。