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

时间:2013-08-28 17:15:57

标签: c++ c visual-studio-2010 gcc floating-point

我正在努力解决一个正在出现的跨平台问题,我不确定如何解决这个问题。这是一个演示程序:

#include <cmath>
#include <cstdio>

int main()
{
    int xm = 0x3f18492a;
    float x = *(float*)&xm;
    x = (sqrt(x) + 1) / 2.0f;
    printf("%f %x\n", x, *(int*)&x);
}

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

0.885638 3f62b92a

使用GCC 4.8.1 (ideone.com sample) 编译时的输出是:

0.885638 3f62b92b

在需要在多个平台上运行相同的程序过程中,这些小的不匹配最终会成为一个严重的问题。我并不太关心“准确性”,因为结果相互匹配。我尝试将VS中的/fp模式从strict切换为precise,但这似乎无法解决问题。

我应该考虑采用哪些其他方法来进行此计算,在两个平台上都有相同的结果?

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

#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 %x\n", x, *(int*)&x, y, *(int*)&y, z, *(int*)&z, w, *(int*)&w);
}

但是,我不确定是否可以通过代码并更改所有浮点运算来实现这一点!

8 个答案:

答案 0 :(得分:6)

总结:编译器通常不支持这种情况,您将很难用更高级别的语言进行操作,并且需要使用一个通用于所有目标平台的数学库。

C和C ++语言标准允许在浮点运算中实现相当大(太多)的灵活性。许多C和C ++浮动操作不需要遵循IEEE 754-2008标准,对许多程序员来说可能是直观的。

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

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

然而,

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

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

答案 1 :(得分:4)

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

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

结果:

  1. 如果仅使用double次计算,Visual Studio应生成一个程序,该程序可以精确计算类型的精度,因为它总是double(**)。如果在GCC端使用-msse2 -mfpmath=sse,您可以期望GCC还生成以double s的精度计算的代码,这次使用SSE2指令。计算应匹配。

  2. 或者,如果您同时使GCC和Visual Studio发出SSE2指令,则计算应该匹配。我不熟悉Visual Studio,但切换可能是/arch:SSE2

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

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

    (**)无穷大和次正规的模数异常。

答案 2 :(得分:3)

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

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

当所有计算编码float时,GCC计算得到不同(且更准确)的结果,但允许对中间结果进行加倍或加倍。

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

[编辑]我认为上述内容并不能解决OP所述问题,但是对于一般的FP问题是一个问题。如下所示,我认为最能说明差异。

MS dev network sqrt

我怀疑不同之处在于,在 windows 编译中,它处于C ++模式,因此sqrt(x)称为 float sqrt(float)。在 gcc 中,它处于C模式,sqrt(x1)称为 double 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 %08X\n", f1, f1, *(int*)&f1);
    f2 = (sqrt(f2) + 1) / 2.0;
    printf("f2  %0.17e %a %08X\n", f2, f2, *(int*)&f2);
    d1 = (sqrt(d1) + 1) / 2.0;
    printf("d1  %0.17e %a\n", 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

答案 3 :(得分:2)

使用我的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编译器,所以我不知道它产生了什么。

答案 4 :(得分:2)

IEEE 754规定,计算的处理精度可以高于存储在存储器中的精度,然后在写回存储器时进行舍入。这会导致许多问题,例如您看到的问题。简而言之,该标准并不保证在所有硬件上执行的相同计算将返回相同的答案。

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

另一方面,如果在将值移回内存之前在较大的寄存器上完成所有计算,则会得到不同的结果。您可能必须反汇编代码以查看您的案例中发生了什么。

使用浮点工作,了解您在最终答案中需要多少精度以及保证您所选择的变量的精度(请注意单词的两次使用)可以保证多少精度非常重要并且永远不会期望精确度超过您的保证。

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

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

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

答案 5 :(得分:1)

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

最重要的是,sqrt在不同的实现中会产生稍微不同的结果,没有什么不寻常之处。

答案 6 :(得分:1)

如果您可以自由更改语言,请考虑将Java与“strictfp”一起使用。 Java语言规范在strictfp模式下为操作顺序,舍入等提供了非常精确的规则。

跨实现的完全匹配结果是strictfp模式的Java标准的目标。它不是C ++标准的目标。

答案 7 :(得分:0)

您希望他们都使用IEEE 754 standard