浮点运算是否稳定?

时间:2018-01-22 14:53:45

标签: c# floating-point

我知道浮点数具有精度,精度后的数字不可靠。

但是如果用于计算数字的等式是相同的呢?我可以假设结果也一样吗?

例如,我们有两个浮点数xy。我们可以假设机器1的结果x/y与机器2的结果完全相同吗? I.E. ==比较将返回true

6 个答案:

答案 0 :(得分:45)

  

但是如果用于计算数字的等式是相同的呢?我可以假设结果也一样吗?

不,不一定。

特别是,在某些情况下,允许JIT使用更准确的中间表示 - 例如原始数据为64位时为80位 - 而在其他情况下则不是。当满足以下任何条件时,这可能导致看到不同的结果:

  • 您的代码略有不同,例如使用局部变量而不是字段,这可以改变值是否存储在寄存器中。 (这是一个相对明显的例子;还有其他更微妙的例子可以影响事物,例如方法中存在try块...)
  • 您正在不同的处理器上执行(我曾经观察过AMD和Intel之间的差异;同一制造商的不同CPU之间也可能存在差异)
  • 您正在执行不同的优化级别(例如,在调试器下或不在调试器下)

来自C#5规范部分4.1.6:

  

可以以比操作的结果类型更高的精度执行浮点运算。例如,某些硬件体系结构支持“扩展”或“长双”浮点类型,其范围和精度比double类型更大,并使用此更高精度类型隐式执行所有浮点运算。只有在性能成本过高的情况下,才能使这种硬件架构以较低的精度执行浮点运算,而不是要求实现失去性能和精度,C#允许更高精度的类型用于所有浮点运算。除了提供更精确的结果外,这几乎没有任何可衡量的影响。但是,在x * y / z形式的表达式中,乘法产生的结果超出双范围,但随后的除法将临时结果带回双范围,表达式的计算结果更高范围格式可能会导致生成有限结果而不是无穷大。

答案 1 :(得分:20)

Jon的回答当然是正确的。然而,没有一个答案说如何你可以确保浮点运算以规范保证的精度和不再完成。

在以下情况下,C#会自动将任何浮点数截断回其规范的32或64位表示:

  • 您输入了一个冗余的显式转换:x + y可能有x和y作为更高精度的数字,然后添加。但是(double)((double)x+(double)y)确保在数学发生之前和之后所有内容都被截断为64位精度。
  • 对类,静态字段,数组元素或取消引用指针的实例字段的任何存储始终会截断。 (本地商店,参数和临时商店的商店不能保证截断;它们可以被注册。结构的字段可能位于短期池中,也可以注册。)

这些保证由语言规范制定,但实现应遵守这些规则。 C#和CLR的Microsoft实现。

编写代码以确保浮点运算在C#中是可预测的但是可以完成是一件痛苦的事。请注意,这样做可能会减慢算术速度。

关于这种可怕情况的投诉应该针对英特尔,而不是微软;他们是设计芯片的人,这使得可预测的算术变得更慢。

另请注意,这是一个经常被问到的问题。您可以考虑将其关闭为:

Why differs floating-point precision in C# when separated by parantheses and when separated by statements?

Why does this floating-point calculation give different results on different machines?

Casting a result to float in method returning float changes result

(.1f+.2f==.3f) != (.1f+.2f).Equals(.3f) Why?

Coercing floating-point to be deterministic in .NET?

C# XNA Visual Studio: Difference between "release" and "debug" modes?

C# - Inconsistent math operation result on 32-bit and 64-bit

Rounding Error in C#: Different results on different PCs

Strange compiler behavior with float literals vs float variables

答案 2 :(得分:8)

不,它没有。每个CPU的计算结果可能不同,因为浮点运算的实现可能因CPU制造商或CPU而异 设计。我甚至还记得一些英特尔处理器中的浮点运算中的一个错误,这搞砸了我们的计算。

然后,在JIT编译器中如何评估代码存在差异。

答案 3 :(得分:8)

坦率地说,我不希望同一代码库中的两个位置为同一x/yx的{​​{1}}返回相同的内容 - 在同一台机器上的同一进程中;它可以取决于编译器/ JIT如何优化yx - 如果它们以不同的方式注册,它们可以具有不同的中间精度。使用比预期更多的位(寄存器大小)完成了许多操作;并且完全在何时强制降低到64位会影响结果。 “确切时间”的选择取决于周围代码中发生的其他事情。

答案 4 :(得分:3)

理论上,在符合IEEE 754标准的系统中,具有相同输入值的相同操作应该产生相同的结果。

正如维基百科所总结的那样:

  

IEEE 754-1985允许实现中的许多变化(例如某些值的编码和某些异常的检测)。 IEEE 754-2008已经加强了其中的许多,但仍然存在一些变化(特别是对于二进制格式)。可重复性条款建议语言标准应提供编写可重现程序的方法(即,在语言的所有实现中产生相同结果的程序),并描述需要采取哪些措施才能获得可重现的结果。

然而,与往常一样,理论与实践不同。 大多数常用的编程语言,包括C#,都不严格符合IEEE 754,,并不一定提供编写可重现程序的方法。

此外,现代CPU / FPU使得确保严格的IEEE 754合规性有些尴尬。默认情况下,它们将以"扩展精度"进行操作,在内部存储比位数更多的位。如果需要严格的语义,则需要将FPU中的值拉入CPU寄存器,检查并处理各种FPU异常,然后在每个FPU操作之间将值推回。由于这种尴尬,严格的一致性会降低性能,即使在硬件级别也是如此。 C#标准选择了更多"马虎"要求避免对较小的变化不成问题的更常见情况施加性能损失。

这在实践中通常不是一个问题,因为大多数程序员已经内化了(错误的,或者至少是误导性的)浮点数学不精确的想法。另外,我们在这里讨论的错误都非常小,足以使它们因从十进制转换而导致的更常见的精度损失相形见绌。

答案 5 :(得分:2)

除了其他答案之外,即使在同一台计算机上也可能x/y != x/y

x86中的浮点计算使用80位寄存器完成,但在存储时截断为64位。因此,可以计算,截断第一个除法,然后将其加载回内存并与非截断的除法进行比较。

有关详细信息,请参阅here (这是一个C ++链接,但推理相同)