21

以下代码引发了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();
}
4

4 回答 4

37

使用在 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

在您的主目录中,第一个结果存储在 64 位内存位置中,因此无论哪种方式都会丢失额外的精度:

double seconds = millis2seconds(millis);

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

assert(seconds == millis2seconds(millis));

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

在 x86-64 上,计算是使用只有 64 位的 SSE 寄存器完成的,所以不会出现这个问题。

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

于 2013-06-03T01:04:47.540 回答
8

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

解决此问题的一种方法是即使在 x86 目标上也使用 SSE 数学构建,使用-msse2 -mfpmath=sse.

于 2013-06-03T00:38:25.930 回答
4

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

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

C99 标准ISO/IEC 9899

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

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

仍然需要强制转换和赋值运算符来执行它们指定的转换。

这解释了为什么专门分配给 a 会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 行为更好。

于 2013-06-03T00:38:02.437 回答
2

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

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

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

于 2013-06-03T03:08:43.367 回答