8

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

#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 示例)编译时的输出为:

0.885638 3f62b92b

在需要在多个平台上以相同方式运行的程序的过程中,这些小的不匹配最终会演变成一个严重的问题。我不太关心“准确性”,因为结果彼此匹配。我尝试将/fpVS 中的模式切换为strictfrom 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);
}

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

4

8 回答 8

6

Summary: This is generally not supported by compilers, you will have a tough time doing it in a higher-level language, and you will need to use one math library common to all your target platforms.

The C and C++ language standards allow implementations a considerable amount (too much) of flexibility in floating-point operations. Many C and C++ floating-operations are not required to adhere to the IEEE 754-2008 standard in the way that might be intuitive to many programmers.

Even many C and C++ implementations do not provide good support for adhering to the IEEE 754-2008 standard.

Math library implementations are a particular problem. There does not exist any normal library (commercially available or widely-used open source with a known-bounded run-time) that provides correctly rounded results for all standard math functions. (Getting the mathematics right on some of the functions is a very difficult problem.)

sqrt, however, is relatively simple and should return correctly rounded results in an library of reasonable quality. (I am unable to vouch for the Microsoft implementation.) It is more likely the particular problem in the code you show is the compiler’s choice to use varying precisions of floating-point while evaluating expressions.

There may be various switches you can use with various compilers to ask them to conform to certain rules about floating-point behavior. Those may be sufficient for getting elementary operations to perform as expected. If not, assembly language is a way to access well-defined floating-point operations. However, the behavior of library routines will be different between platforms unless you supply a common library. This includes both math library routines (such as pow) and conversions found in routines such as fprintf, fscanf, strtof. You must therefore find one well-designed implementation of each routine you rely on that is supported on all of the platforms you target. (It must be well-designed in the sense that it provides identical behavior on all platforms. Mathematically, it could be somewhat inaccurate, as long as it is within bounds tolerable for your application.)

于 2013-08-28T17:34:29.933 回答
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 也生成以 s 的精度计算的代码double,这一次是使用 SSE2 指令。计算应该匹配。

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

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

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

(**) 无穷大和次正规的模例外。

于 2013-08-28T18:30:11.493 回答
3

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

如果所有计算仅使用 using 发生,则 windows 结果与 GCC 匹配float

当所有计算都编码为时, GCC 计算获得不同(且更准确)的结果float,但对于中间结果,允许使用 double 或 long double。

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

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

MS 开发网络 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
于 2013-08-28T18:14:52.843 回答
2

With my 4.6.3 compiler it generates this code:

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

Note that there is ZERO calculations performed in this code, just storing various constants in registers.

I haven't got a Visual stdudio compiler, so I don't know what that produces.

于 2013-08-28T17:35:16.997 回答
2

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

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

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

对于浮点工作,重要的是要了解最终答案中需要多少精度以及您选择的变量的精度(注​​意该词的两种用法)保证您具有多少精度,并且永远不要期望比您更高的精度得到保证。

最后,当您比较结果时(任何浮点工作都是如此),您无法寻找精确匹配,您确定您需要的精度并检查两个值之间的差异是否小于您需要的精度。

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

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

于 2014-05-20T12:42:22.503 回答
1

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

跨实现精确匹配结果是 strictfp 模式的 Java 标准的目标。这不是 C++ 标准的目标。

于 2013-08-28T18:58:19.070 回答
1

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

sqrt最重要的是,在不同的实现中产生略有不同的结果并没有什么不寻常的。

于 2013-08-28T18:05:59.350 回答
0

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

于 2013-08-28T17:17:32.343 回答