java junit 测试选择哪个 delta 以避免浮点错误

java juint testing which delta to chose to avoid floatting point error

我正在使用 jUnit 测试我的 Java 应用程序(我对 Java 完全陌生)。

public class MyClass {
    public static void main(String args[]) {

        double A = 50000.0;
        double B = 1.1;
         System.out.println("Result" + A * B);
    }
}

一个'normal'答案(从数学角度)是:55000

不过javareturns:

Result 55000.00000000001

因此,我的测试失败了,因为未遵守 assertconsiditon

我的 jUnit 测试看起来像:

        Assert.assertEquals("55000", <javaResult>, 0.0);

我认为问题出在最后一个名为 delta 的参数上。因为当我尝试随机更改增量时。以下不给出失败(即测试按预期通过)

        Assert.assertEquals("55000", <javaResult>, 0.01);

所以我的问题是:假设我必须只执行乘法,我应该选择哪个增量? (我觉得随机选择 0.01 只是因为它有效......感觉不舒服......)

在正常范围内(量级在[2−1022、21024)),aJavadouble有一个53位的尾数,所以相邻可表示值之间的步长最多是252的一部分。每当一个数字使用舍入到double格式时,结果如果在正常范围内,最多与原始数字相差半步。

因此,如果 a 是十进制数字的值转换为 doublea,则 a = a•(1+e),其中|e| ≤ 2−53.

考虑将两个数字的数学乘积 xy 与将这两个数字转换为 double xy 的结果进行比较] 并将它们相乘以产生 double 结果。我们可能会将确切的乘积 xy 写成十进制数字,但是,在将其传递给 assertEquals 时,它会被转换为 double,因此我们实际上传递了 xy•(1+e0) 对于某些 e 0 如上所述。

同理,x转换为一些x等于x•(1+e 1),而y转换为一些y等于y•(1+e2)。然后我们将它们相乘形成 (x•(1+e1))•(y•(1+e2))•(1+e3).

最后,我们将 xy•(1+e0) 与 (x•(1+e1))•(y•(1+e2))•(1+e3 )。它们有多大不同?

后者为xy•(1+e1)•(1+ e2)•(1+e3) , 所以差值是 xy•((1+e1)•(1+e2)•(1+e3)−(1+e0)) = xy•(e1+e2+e3+e1e2+e 2e3+e 1e3+e 1e2e3e0)。我们可以很容易地看到当 e0 为 −2−53 并且其他错误是+2−53。那么就是4•2−53 + 3•2−106 + 2−159。无论 xy 是正的还是负的,这个误差界限的最大幅度保持不变,所以我们可以将这个界限的差异描述为|xy|•(4•2−53 + 3•2−106 + 2−159).

我们无法使用 double 精确计算,原因有以下三个:

  • xy 可能无法准确表示。
  • 4•2−53 + 3•2−106 + 2−159不可表示。
  • 这两个值用double相乘时,可能会出现另一个舍入误差。

为了处理第一个问题,假设我们有所需产品的绝对值 xy,作为十进制数字 N。然后我们可以替换|xy|在带有 Math.nextAfter(N, Double.POSITIVE_INFINITY) 的表达式中。这增加了少量(尽可能少的)以补偿 N 在转换为 double 时可能向下舍入的事实。 (我们也可以通过将其转换为 double 并向 ∞ 舍入而不是舍入到最近的舍入来准备 N。)

为了处理第二个问题,我们可以将4•2−53 + 3•2−106 + 2−159double,向 ∞ 舍入。结果为 4•2−53 + 3•2−106,或 2−51 + 2−103。从 JDK 5 开始,我们可以将其写为 0x1.0000000000001p-51.

第三题相对于结果最多可以引入2−53的舍入误差。然而,在将误差项转换为 double 时,我们四舍五入了不止于此(2−103 超过 3•2−106 的量 + 2−159 小于误差项的 2−53 倍),所以,即使计算结果四舍五入下降了 2−53(相对),仍然高于期望的数学结果。

因此,Assert.assertEquals("<exactResult>", <javaResult>, Math.nextAfter(N, Double.POSITIVE_INFINITY)*0x1.0000000000001p-51); 只有在满足以下至少一项条件时才会报告错误:

  • <javaResult>不是在double中计算两个十进制数的乘积的结果,其精确乘积是<exactResult>.
  • <exactResult>不在double的正常范围内。
  • 计算出的公差不在 double 的正常范围内(导致它被向下舍入超过上述预期)。 (注意第二个条件隐含了这个第三个,所以第二个可以省略。)

如果 <exactResult> 处于低于正常范围内,则可以使用较小的绝对值作为公差,而不是相对值。我省略了讨论。

请注意,如果产品符合预期,此绑定将不会报告错误。但是,如果产品有误,不保证一定会报错。容差是单个操作错误界限的四倍多(因为涉及四个操作)。因此,断言将接受除计算乘积的正确结果之外的几个值。换句话说,对于非常接近但不同于正确结果的结果,可能会出现假阴性。

这是错误的上限。可以通过更多分析进一步收紧它,并且在某些情况下可以收紧更多,例如如果已知 xy 是可精确表示的,或者如果xy 是已知的。

我希望 nextAfter 可以替换为增加误差因子,但我还没有进行分析以能够断言,比如说,增加一个 ULP 就足够了。