Java / C# 中浮点计算的显着精度差异

Significant precision difference in floating point calculations in Java / C#

我知道以前有人问过类似的问题,但是 none 的答案解决了我的问题。

我有两个功能:

Java

public static void main(String[] args) {
    double h, z, lat0, n0, eSq;
    z    = 4488055.516;
    lat0 = 0.7853981634671384;
    n0   = 6388838.290122733;
    eSq  = 0.0066943799901975545;
    h    = z / Math.sin(lat0) - n0 * (1 - eSq);
    System.out.println(h);
}

C#

public static void Main (string[] args)
{
    double h, z, lat0, n0, eSq;
    z    = 4488055.516;
    lat0 = 0.7853981634671384;
    n0   = 6388838.290122733;
    eSq  = 0.0066943799901975545;
    h    = z / Math.Sin(lat0) - n0 * (1 - eSq);
    Console.WriteLine(h);
}

4488055,516/sin(0,7853981634671384)-6388838,290122733*(1-0,0066943799901975545)

对于 SpeedCrunch、Maxima 和 LibreOffice Calc。

结果是:

Java:             1000.0000555226579 (same with and without strictfp)
C#:               1000,00005552359
SpeedCrunch (15): 1000,000055524055155
SpeedCrunch (50): 1000,00005552405515548724762598846216107366705932894830
LibreOffice Calc: 1000,000055523590000
Maxima:           1000.000055523589
Maxima:           1.00000005552391142b3 (bfloat - fpprec:20)

如你所见,Java和C#在小数点后9位不同。其他人也不是那么统一。这是在相同的 OS 和相同的 CPU 上测试的。测试也在 32 位和 64 位系统上完成。

如何解决此类问题?我认为精度应该等于小数点后 15 位。

没有得到15位的原因是两个相似数相减。 z / Math.sin(lat0) 大约是 6347068.978968251。 n0 * (1 - eSq) 大约是 6346068.978912728。小数点前有 7 位,减法结果中小数点后 9 位的变化对应于其中一个输入的 10^15 中小于一个部分的变化。

此类问题最简单的解决方案通常是仅显示输入中可靠数字支持的数字。对于 10^12 中的一个部分可以进行的测量很少,因此在这种情况下,几乎可以肯定由于浮点舍入误差而不同的数字将被丢弃。

例如,您的数据看起来好像与位置有关。这些数据中最仔细测量的部分之一是珠穆朗玛峰的高度。目前基于高精度 GPS 测量的最佳估计是“29,035 英尺,误差范围为正负 6.5 英尺”Encyclopedia Britannica。第13位有效数字的误差对应于测量地球周长时误差小于千分之一英寸。

如果舍入误差相对于结果要求以及在给定输入精度的情况下实际可实现的结果而言确实很重要,那么您可能需要考虑更巧妙的计算安排方式或更高精度的算法.

您观察到的精度差异不在于浮点支持本身。 Java 和 C# 将使用相同的 (IEE 768) 浮点表示法,并且很可能使用相同的指令。

您观察到的可能是计算超越函数的算法的差异;例如正弦函数。理论计算涉及对 无限 级数求和。为了尽可能获得最准确的答案,您不断地对序列求和。为了得到一定精度的答案,你不断求和,直到 "deltas" 小于所需的精度。

在实践中,这种方法非常慢。实用算法在可能的情况下使用表格和插值来实现。这要快得多,尽管您没有获得最大精度。


Java Math.sin 的精度由用于计算它的算法决定。这些算法是特定于平台的。以下是 Math 的 javadocs 关于精度主题的内容。

Unlike some of the numeric methods of class StrictMath, all implementations of the equivalent functions of class Math are not defined to return the bit-for-bit same results. This relaxation permits better-performing implementations where strict reproducibility is not required.

Math.sin 的精度保证是:

The computed result must be within 1 ulp of the exact result. Results must be semi-monotonic.

其中 "ulp" 和 "semi-monotonic" 在 javadoc 中指定。

相比之下,javadocs for StrictMath 声明使用特定开源库的特定版本进行计算。目标是可重复性;即所有 Java 平台上的相同答案。


问:那他们为什么不让 Math.sin 更精确呢?有可能达到 0.5 ulp 以内...

这是速度和精度之间的工程权衡。阅读 this wikipedia article 以了解问题所在。

我的建议是,如果您想要最大的精度,请寻找实现先验函数的第 3 方开源 Java 库。并准备好你的代码会慢很多。