繁体   English   中英

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

[英]Why does returning a floating-point value change its value?

以下代码在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 <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                  : %d\n", millis);
    printf("seconds                 : %f\n", seconds);
    printf("millis2seconds(millis)  : %f\n", millis2seconds(millis));
    printf("seconds <  millis2seconds(millis)  : %d\n", seconds < millis2seconds(millis));
    printf("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

在您的主机中,第一个结果存储在64位内存位置中,因此,两种方式都会失去额外的精度:

double seconds = millis2seconds(millis);

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

assert(seconds == millis2seconds(millis));

当使用第一个版本的millis2seconds ,您最终将已被截断为64位精度的值与具有完整80位精度的值进行比较,因此存在差异。

在x86-64上,使用SSE寄存器(只有64位)完成计算,因此不会出现此问题。

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

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

解决此问题的一种方法是甚至在x86目标上也使用-msse2 -mfpmath=sse来构建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)放入浮点变量或进行复制时,更改的可能性很小。 但是可能会有更复杂的值(在上面的示例中,我们看到了简单算术表达式的结果会发生什么!)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM