为什么返回一个浮点值会改变它的值?

Why does returning a floating-point value change its value?

本文关键字:改变 返回 一个 为什么      更新时间:2023-10-16

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

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

如果你注释了"#define BUG"行,它会工作。

多亏了@R,将-msse2 -mfpmath选项传递给编译器,使得毫秒函数的两个变体都可以工作。

/*
 * TestDouble.cpp
 */
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
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                  : %dn", millis);
    printf("seconds                 : %fn", seconds);
    printf("millis2seconds(millis)  : %fn", millis2seconds(millis));
    printf("seconds <  millis2seconds(millis)  : %dn", seconds < millis2seconds(millis));
    printf("seconds >  millis2seconds(millis)  : %dn", seconds > millis2seconds(millis));
    printf("seconds == millis2seconds(millis)  : %dn", seconds == millis2seconds(millis));
    assert(seconds == millis2seconds(millis));
}
extern int main(int argc, char **argv) {
    testMillis2seconds();
}

对于Linux x86系统上使用的cdecl调用约定,使用st0 x87寄存器从函数返回双精度类型。所有的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寄存器完成的,SSE寄存器只有64位,所以这个问题不会出现。

同样,如果您使用-std=c99,这样您就不会得到GNU方言,计算的值将存储在内存中,并在返回之前重新加载到寄存器中,以便符合标准。

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

解决这个问题的一种方法是在x86目标上使用SSE数学构建,使用-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的错误,因为标准允许这种行为,但clang行为更好。

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

所以,即使它在测试中有效,通常最好不要这样做。当事情发生变化时,它可能会失败。

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