编译器之间的浮点不匹配(Visual Studio 2010和GCC)
我正在努力解决一个正在出现的跨平台问题,我不确定如何解决这个问题。 这是一个演示程序:
#include #include 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
在需要在多个平台上以相同方式运行的程序过程中,这些小的不匹配最终会成为一个严重的问题。 我并不太关心“准确性”,因为结果相互匹配 。 我尝试将VS中的/fp
模式从precise
切换到strict
,但这似乎并没有解决它。
我应该考虑采用哪些其他途径来进行此计算,在两个平台上都有相同的结果?
更新 :有趣的是,如果我更改这样的代码,它会跨平台匹配:
#include #include 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); }
但是,我不确定是否可以通过代码来改变这样的所有浮点运算!
简介:编译器通常不支持这一点,您将很难用更高级的语言进行操作,并且需要使用所有目标平台通用的一个数学库。
C和C ++语言标准允许在浮点运算中实现相当多(太多)的灵活性。 许多C和C ++浮动操作不需要遵循IEEE 754-2008标准,这对许多程序员来说可能是直观的。
甚至许多C和C ++实现也没有为遵守IEEE 754-2008标准提供良好的支持。
数学库实现是一个特殊问题。 不存在任何普通库(商业上可用或广泛使用的具有已知有界运行时的开源),它为所有标准数学函数提供了正确的舍入结果。 (在一些函数上获得正确的数学是一个非常困难的问题。)
但是, sqrt
相对简单,应该在合理质量的库中返回正确的舍入结果。 (我无法保证Microsoft的实现。)您展示的代码中的特定问题更可能是编译器在评估表达式时使用不同的浮点精度。
可以使用各种开关来与各种编译器一起使用,以使它们符合有关浮点行为的某些规则。 这些可能足以使基本操作按预期执行。 如果不是,汇编语言是一种访问定义良好的浮点运算的方法。 但是,除非您提供公共库,否则库例程的行为在平台之间会有所不同。 这包括数学库例程(例如pow
)和例程中的转换,例如fprintf
, fscanf
, strtof
。 因此,您必须找到所依赖的每个例程的一个精心设计的实现,并在您定位的所有平台上得到支持。 (它必须经过精心设计,因为它在所有平台上提供相同的行为。从数学上讲,它可能有些不准确,只要它在你的应用程序可以容忍的范围内。)
Visual Studio编译器倾向于生成使用旧x87 FPU(*)的指令,但它会在可执行文件的开头生成代码,以将FPU设置为double
格式的精度。
GCC还可以生成使用旧x87 FPU的指令,但在生成x86-64代码时,默认使用SSE2。 在Mac OS X上,默认情况下使用SSE2即使在32位,因为所有Intel Mac都有SSE2。 当它为387生成指令时,GCC不会将FPU的精度设置为double
精度格式,因此计算以80位双扩展格式进行,然后在分配时舍入为double
精度。
作为结果:
-
如果您只使用
double
计算,Visual Studio应生成一个程序,该程序精确计算类型的精度,因为它总是double
(**)。 如果在GCC端你使用-msse2 -mfpmath=sse
,你可以期望GCC也生成以double
s精度计算的代码,这次使用SSE2指令。 计算应该匹配。 -
或者,如果您同时使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)
称为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
使用我的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编译器,所以我不知道它产生了什么。
IEEE 754规定,计算可以以比存储在存储器中的精度更高的精度进行处理,然后在写回存储器时进行舍入。 这会导致许多问题,例如您看到的问题。 简而言之,该标准并不保证在所有硬件上执行的相同计算将返回相同的答案。
如果要计算的值被放置在较大的寄存器上,则进行一次计算,然后将该值从寄存器移回存储器,结果在那里被截断。 然后可以将其移回更大的寄存器以进行另一次计算。
另一方面,如果在将值移回内存之前在较大的寄存器上完成所有计算,则会得到不同的结果。 您可能必须反汇编代码以查看您的案例中发生的情况。
使用浮点工作时,重要的是要了解在最终答案中需要多少精度以及保证您所选择的变量的精度(请注意单词的两次使用)所具有的精度,并且永远不会期望比您更精确保证。
最后,当您比较结果时(对于任何浮点工作都是如此),您无法查找完全匹配,您可以确定所需的精度,并检查两个值之间的差异是否小于所需的精度。
回到实用性,英特尔处理器具有80位浮点计算寄存器,即使您指定了通常为32位(但并非总是如此)的浮点数,也可以使用这些寄存器。
如果你想玩得开心尝试在你的编译器中启用各种优化和处理器选项,比如SSE,看看你得到了什么结果(以及反汇编程序的结果)。
GCC编译器实现了所谓的严格别名语义 ,它依赖于这样的事实:在C和C ++中,通过指针转换执行类型惩罚通常是非法的(少数例外)。 您的代码包含多个违反严格别名语义要求的行为。 因此,期望严格别名语义和优化的组合可能在GCC(或任何其他编译器)中产生完全意外且看似不合逻辑的结果是完全合乎逻辑的。
最重要的是, sqrt
在不同的实现中会产生稍微不同的结果。
如果您可以自由更改语言,请考虑将Java与“strictfp”一起使用。 Java语言规范在strictfp模式下为操作顺序,舍入等提供了非常精确的规则。
跨实现的完全匹配的结果是strictfp模式的Java标准的目标。 它不是C ++标准的目标。
您希望它们都使用IEEE 754标准 。