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

arm*_*nan 20 c c++ floating-point x86

下面的代码提出了assertRed Hat 5.4 32位,但适用于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();
}
Run Code Online (Sandbox Code Playgroud)

Vau*_*ato 36

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

static double millis2seconds(int millis) {
    return (double)(millis) / 1000.0;
};
Run Code Online (Sandbox Code Playgroud)

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

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

有了这段代码,

static double millis2seconds(int millis) {
    double result = (double)(millis) / 1000.0;
    return result;
}
Run Code Online (Sandbox Code Playgroud)

结果存储在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
Run Code Online (Sandbox Code Playgroud)

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

double seconds = millis2seconds(millis);
Run Code Online (Sandbox Code Playgroud)

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

assert(seconds == millis2seconds(millis));
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • @poolie:这不再仅仅是一个选秀.这是C11. (3认同)

R..*_*R.. 8

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

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