0

编辑:这个问题涵盖两个主题:

  • 使用 in double 代替 float 的效率
  • 舍入后的浮点精度

有什么理由不应该总是使用 Java double 而不是 float?

我问这个问题是因为这个测试代码在使用浮点数时失败并且不清楚原因,因为唯一的区别是使用浮点数而不是双精度数。

public class BigDecimalTest {
@Test public void testDeltaUsingDouble() { //test passes
    BigDecimal left = new BigDecimal("0.99").setScale(2,BigDecimal.ROUND_DOWN);
    BigDecimal right = new BigDecimal("0.979").setScale(2,BigDecimal.ROUND_DOWN);

    Assert.assertEquals(left.doubleValue(), right.doubleValue(), 0.09);
    Assert.assertEquals(left.doubleValue(), right.doubleValue(), 0.03);

    Assert.assertNotEquals(left.doubleValue(), right.doubleValue(), 0.02);
    Assert.assertNotEquals(left.doubleValue(), right.doubleValue(), 0.01);
    Assert.assertNotEquals(left.doubleValue(), right.doubleValue(), 0.0);
}
@Test public void testDeltaUsingFloat() {  //test fails on 'failing assert'

    BigDecimal left = new BigDecimal("0.99").setScale(2,BigDecimal.ROUND_DOWN);
    BigDecimal right = new BigDecimal("0.979").setScale(2,BigDecimal.ROUND_DOWN);

    Assert.assertEquals(left.floatValue(), right.floatValue(), 0.09);
    Assert.assertEquals(left.floatValue(), right.floatValue(), 0.03);

    /* failing assert */ Assert.assertNotEquals(left.floatValue() + " - " + right.floatValue() + " = " + (left.floatValue() - right.floatValue()),left.floatValue(), right.floatValue(), 0.02);
    Assert.assertNotEquals(left.floatValue(), right.floatValue(), 0.01);
    Assert.assertNotEquals(left.floatValue(), right.floatValue(), 0.0);
}}

失败消息:

java.lang.AssertionError: 0.99 - 0.97 = 0.01999998. Actual: 0.9900000095367432
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failEquals(Assert.java:185)
at org.junit.Assert.assertNotEquals(Assert.java:230)
at com.icode.common.BigDecimalTest.testDeltaUsingFloat(BigDecimalTest.java:34)

知道为什么这个测试会失败,为什么我不应该总是使用 double 而不是 float 吗?当然,除了双精度外,还有一个比浮点数更宽的原因。

编辑:有趣的是 Assert.assertNotEquals(double,double,delta) 在这两种情况下都需要双精度,因此失败测试中返回的浮点数无论如何都会扩大为双精度,那么为什么测试失败呢?

编辑:可能这个其他问题是相关的,但不确定: 十六进制不一样

编辑:从这个问题的答案十六进制不一样可以得出结论,对于浮点数的 .99 的科学表示 IEEE 754 与相同值的双精度表示不同。这是由于四舍五入。

因此我们得到这个:

  • 0.99 - 0.97 = 0.01999998 //在浮点情况下
  • 0.99 - 0.97 = 0.020000000000000018 //在双重情况下

由于上述单元测试中的最大增量为 0.02 并且 0.01999998(在失败的测试中)低于 delta 值,这意味着这些数字看起来是相同的,但测试断言它们没有因此失败。

伙计们,你同意这一切吗?

4

2 回答 2

2

BigDecimal的文档对如何floatValue()轮次保持沉默。我认为它使用四舍五入到最近的,平到的。

leftright分别设置为 0.99 和 0.97 。When these are converted to doublein round-to-nearest mode, the results are 0.9899999999999999911182158029987476766109466552734375 (in hexadecimal floating-point, 0x1.fae147ae147aep-1) and 0.9699999999999999733546474089962430298328399658203125 (0x1.f0a3d70a3d70ap-1). 减去这些后,结果为 0.020000000000000017763568394002504646778106689453125,显然超过了 0.02。

当 .99 和 .97 转换为float时,结果为 0.9900000095367431640625 (0x1.fae148p-1) 和 0.9700000286102294921875 (0x1.f0a3d8p-1)。减去这些后,结果为 0.019999980926513671875,显然小于 0.02。

简单地说,当十进制数字转换为浮点数时,四舍五入可能会向上或向下。这取决于数字相对于最近的可表示浮点值的位置。如果它不受控制或分析,它实际上是随机的。因此,有时您最终获得的价值比您预期的要大,而有时您最终获得的价值较小。

使用double而不是float不能保证不会出现与上述类似的结果。double在这种情况下,该值超过了精确的数学值,而该float值没有,这只是偶然的。对于其他数字,情况可能相反。例如,如果double,.09-.07小于 0.02,但float如果 , .09f - .07f` 大于 0.02。

有很多关于如何处理浮点运算的信息,例如Handbook of Floating-Point Arithmetic。堆栈溢出问题中涵盖的主题太大。上面有大学课程。

double通常在当今的典型处理器上,使用而不是float;几乎没有额外费用。double和以几乎相同的速度执行简单的标量浮点运算float。当您拥有大量数据以至于传输它们(从磁盘到内存或内存到处理器)的时间变得很重要,或者它们在磁盘上占用的空间变大,或者您的软件使用处理器的 SIMD 功能时,性能差异就会出现。(SIMD 允许处理器对多条数据并行执行相同的操作。当前的处理器通常为floatSIMD 操作提供大约两倍于 SIMD 操作的带宽,double或者根本不提供doubleSIMD 操作。)

于 2013-10-26T22:11:29.267 回答
1

Double 可以表示具有更多有效数字的数字,具有更大的范围,反之亦然。双重计算在 CPU 方面的成本更高。所以这一切都取决于你的应用程序。二进制数不能精确表示 1/5 这样的数字。这些数字最终会被四舍五入,从而引入错误,这些错误肯定是您的失败断言的起源。有关详细信息,请参阅http://en.m.wikipedia.org/wiki/Floating_point 。

[编辑] 如果一切都失败了运行基准测试:

package doublefloat;

/**
 *
 * @author tarik
 */
public class DoubleFloat {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        // TODO code application logic here
        long t1 = System.nanoTime();
        double d = 0.0;
        for (long i=0; i<1000000000;i++) {
            d = d * 1.01;
        }
        long diff1 = System.nanoTime()-t1;
        System.out.println("Double ticks: " + diff1);

        t1 = System.nanoTime();
        float f = 0.0f;
        for (long i=0; i<1000000000;i++) {
            f = f * 1.01f;
        }
        long diff2 = System.nanoTime()-t1;
        System.out.println("Float  ticks: " + diff2);
        System.out.println("Difference %: " + (diff1 - diff2) * 100.0 / diff1);    
    }
}

输出:

Double ticks: 3694029247
Float  ticks: 3355071337
Difference %: 9.175831790592209

此测试在配备 Intel Core 2 Duo 的 PC 上运行。请注意,由于我们仅在紧密循环中处理单个变量,因此无法压倒可用的内存带宽。事实上,其中一个核心在每次运行期间始终显示 100% 的 CPU。结论:差异为 9%,这实际上可以忽略不计。

第二个测试涉及相同的测试,但使用相对较大的内存量 140MB 和 280MB 分别用于 float 和 double:

package doublefloat;

/**
 *
 * @author tarik
 */
public class DoubleFloat {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        final int LOOPS = 70000000;
        long t1 = System.nanoTime();
        double d[] = new double[LOOPS];
        d[0] = 1.0;
        for (int i=1; i<LOOPS;i++) {
            d[i] = d[i-1] * 1.01;
        }
        long diff1 = System.nanoTime()-t1;
        System.out.println("Double ticks: " + diff1);

        t1 = System.nanoTime();
        float f[] = new float[LOOPS];
        f[0] = 1.0f;
        for (int i=1; i<LOOPS;i++) {
            f[i] = f[i-1] * 1.01f;
        }
        long diff2 = System.nanoTime()-t1;
        System.out.println("Float  ticks: " + diff2);
        System.out.println("Difference %: " + (diff1 - diff2) * 100.0 / diff1);    
    }
}

输出:

Double ticks: 667919011
Float  ticks: 349700405
Difference %: 47.64329218950769

内存带宽不堪重负,但我仍然可以看到 CPU 在短时间内达到 100% 的峰值。

结论:这个基准测试在一定程度上证实了在 CPU 密集型应用程序上使用 double 会多花 9% 的时间,而在数据密集型应用程序上会多花 50% 的时间。它还证实了Eric Postpischil 的说明,与有限的内存带宽对性能的影响相比,CPU 开销相对可以忽略不计(9%)。

于 2013-10-25T13:20:45.320 回答