编译器(Visual Studio 2010和GCC)之间的浮点不匹配

Floating point mismatch between compilers (Visual Studio 2010 and GCC)

本文关键字:之间 不匹配 GCC Visual Studio 2010 编译器      更新时间:2023-10-16

我正试图解决一个突然出现的跨平台问题,但我不太确定如何解决

#include <cmath>
#include <cstdio>
int main()
{
    int xm = 0x3f18492a;
    float x = *(float*)&xm;
    x = (sqrt(x) + 1) / 2.0f;
    printf("%f %xn", x, *(int*)&x);
}

在VS2010中编译时,Windows上的输出为:

0.885638 3f62b92a

使用GCC 4.8.1(ideone.com示例)编译时的输出为:

0.885638 3f62b92b

在需要在多个平台上相同运行的程序过程中,这些小的不匹配最终会演变成一个严重的问题。与其说我关心"准确性",不如说我关心结果彼此匹配。我试着将VS中的/fp模式从precise切换到strict,但这似乎并不能解决问题

我还应该通过什么途径使这一计算在两个平台上都有相同的结果?

UPDATE:有趣的是,如果我这样更改代码,它会在各个平台上匹配:

#include <cmath>
#include <cstdio>
int main()
{
    int xm = 0x3f18492a;
    float x = *(float*)&xm;
    //x = (sqrt(x) + 1) / 2.0f;
    float y = sqrt(x);
    float z = y + 1;
    float w = z / 2.0f;
    printf("%f %x %f %x %f %x %f %xn", x, *(int*)&x, y, *(int*)&y, z, *(int*)&z, w, *(int*)&w);
}

然而,我不确定这样遍历代码并更改所有浮点运算是否现实!

摘要:编译器通常不支持这一点,在更高级别的语言中很难做到,并且需要使用一个所有目标平台通用的数学库。

C和C++语言标准允许在浮点运算中实现相当大(太多)的灵活性。许多C和C++浮点运算不需要以许多程序员可能直观的方式遵守IEEE 754-2008标准。

即使许多C和C++实现也不能为遵守IEEE 754-2008标准提供良好的支持。

数学库的实现是一个特殊的问题。不存在任何为所有标准数学函数提供正确舍入结果的普通库(商业上可用或广泛使用的具有已知有界运行时间的开源)。(正确计算某些函数的数学是一个非常困难的问题。)

然而,sqrt相对简单,应该在质量合理的库中返回正确的四舍五入结果。(我无法保证Microsoft的实现。)您显示的代码中的特定问题更有可能是编译器在计算表达式时选择使用不同精度的浮点值。

您可以将各种开关与各种编译器一起使用,以要求它们遵守有关浮点行为的某些规则。这些可能足以使基本操作按预期执行。否则,汇编语言就是访问定义良好的浮点运算的一种方式。但是,除非您提供一个公共库,否则不同平台之间的库例程的行为会有所不同。这既包括数学库例程(如pow),也包括在例程中找到的转换,如fprintffscanfstrtof。因此,您必须为所依赖的每个例程找到一个设计良好的实现,该实现在所有目标平台上都得到支持。(它必须精心设计,因为它在所有平台上都提供相同的行为。从数学上讲,它可能有些不准确,只要它在应用程序可容忍的范围内。)

Visual Studio编译器倾向于生成使用旧x87 FPU(*)的指令,但它在可执行文件的开头生成代码,以将FPU设置为double格式的精度。

GCC也可以生成使用旧x87 FPU的指令,但在生成x86-64代码时,默认情况下使用SSE2。在Mac OS X上,由于所有Intel Mac都具有SSE2,因此即使在32位中,默认情况下也要使用SSE2。当GCC为387生成指令时,它不会将FPU的精度设置为double格式,以便以80位双扩展格式进行计算,然后在分配时四舍五入为double

因此:

  1. 如果只使用double计算,Visual Studio应该生成一个精确计算类型精度的程序,因为它总是double(**)。如果在GCC方面使用-msse2 -mfpmath=sse,那么GCC也可以生成以doubles的精度计算的代码,这次是使用SSE2指令。计算结果应该匹配。

  2. 或者,如果您让GCC和Visual Studio都发出SSE2指令,那么计算应该匹配。我不熟悉Visual Studio,但交换机可能是/arch:SSE2

这并不能解决数学库的问题,这确实是一个尚未解决的问题。如果您的计算涉及三角函数或其他函数,则必须在两侧使用相同的库作为项目的一部分。我推荐CRlibm。不太精确的库也可以,只要它是同一个库,并且它尊重上述约束(仅使用double或在两侧使用SSE2编译)。

(*)可能有一种方法可以指示它生成SSE2指令。如果你找到了,就用它:它会解决你的特定问题。

(**)无穷大和子范数的模例外。

C允许以浮点精度或更高的精度进行中间计算。

如果仅使用float进行所有计算,则窗口结果与GCC匹配。

当所有计算都编码float时,GCC计算会获得不同的(更准确的)结果,但对于中间结果,允许进行双倍或长双倍。

因此,即使一切都符合IEEE 754,控制允许的中间计算也会产生影响。

[编辑]我不认为以上内容真的回答了OP所说的问题,但它是对一般FP问题的关注。我认为,正是以下内容最能解释这种差异。

MS dev网络sqrt

我怀疑不同之处在于,在windows编译中,它处于C++模式,因此sqrt(x)称为float-sqrt(float)。在gcc中,它处于C模式,sqrt(x1)被称为双sqrt(double)
如果是这种情况,请确保windows中的C代码是在C模式下编译的,而不是C++。

int main() {
  {
    volatile float f1;
    float f2;
    double d1;
    int xm = 0x3f18492a;
    f1 = *(float*) &xm;
    f2 = *(float*) &xm;
    d1 = *(float*) &xm;
    f1 = sqrtf(f1);
    f1 = f1 + 1.0f;
    f1 = f1 / 2.0f;
    printf("f1  %0.17e %a %08Xn", f1, f1, *(int*)&f1);
    f2 = (sqrt(f2) + 1) / 2.0;
    printf("f2  %0.17e %a %08Xn", f2, f2, *(int*)&f2);
    d1 = (sqrt(d1) + 1) / 2.0;
    printf("d1  %0.17e %an", d1, d1);
    return 0;
  }
f1  8.85637879371643066e-01 0x1.c57254p-1 3F62B92A
f2  8.85637938976287842e-01 0x1.c57256p-1 3F62B92B
d1  8.85637911452129889e-01 0x1.c572551391bc9p-1

IEEE 754规定,计算可以以比存储在内存中更高的精度进行处理,然后在写回内存时四舍五入。这会导致许多问题,例如您看到的问题。简言之,该标准并没有承诺在所有硬件上进行相同的计算会返回相同的答案。

如果要计算的值放在一个较大的寄存器上,则完成一次计算,然后将该值从寄存器移回内存,结果在那里被截断。然后可以将它移回较大的寄存器进行另一次计算。

另一方面,如果在将值移回内存之前,所有计算都在较大的寄存器上完成,则会得到不同的结果。您可能需要对代码进行反汇编,以了解您的情况。

对于浮点运算,重要的是要了解你在最终答案中需要多大的精度,以及你选择的变量的精度(注意这个词的两个用法)保证了你有多大的精确度,并且永远不要期望比你保证的精度更高的精度。

最后,当您比较结果时(任何浮点运算都是如此),您无法找到完全匹配的结果,您可以确定所需的精度,并检查两个值之间的差值是否小于所需的精确度。

回到实用性上来,英特尔处理器有80位寄存器用于浮点计算,即使您指定了通常为32位(但并不总是)的浮点,也可以使用这些寄存器。

如果你想玩得开心,试着在编译器中打开各种优化和处理器选项,比如SSE,看看你会得到什么结果(以及反汇编程序会产生什么结果)。

使用我的4.6.3编译器,它生成以下代码:

main:
.LFB104:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $1063434539, %esi
    movl    $.LC1, %edi
    movsd   .LC0(%rip), %xmm0
    movl    $1, %eax
    call    printf
    xorl    %eax, %eax
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LC0:
    .long   1610612736
    .long   1072453413

请注意,该代码中执行了ZERO计算,只是将各种常量存储在寄存器中。

我没有Visual stdudio编译器,所以我不知道它会产生什么。

GCC编译器实现了所谓的严格别名语义,这依赖于这样一个事实,即在C和C++中,通过指针转换执行类型双关通常是非法的(只有少数例外)。您的代码包含多个违反严格别名语义要求的内容。因此,期望严格的别名语义和优化的组合可能会在GCC(或任何其他编译器)中产生完全出乎意料且看似不合逻辑的结果,这是完全合乎逻辑的。

除此之外,sqrt在不同的实现中产生稍微不同的结果并没有什么异常。

如果您可以自由更改语言,请考虑使用带有"strictfp"的Java。Java语言规范为strictfp模式下的操作顺序、舍入等提供了非常精确的规则。

严格fp模式的Java标准的目标是在实现之间精确匹配结果。它不是C++标准的目标。

您希望它们都使用IEEE 754标准。