双重代替Float和Float四舍五入

时间:2013-10-25 13:13:49

标签: java comparison double precision

编辑: 这个问题涉及两个主题:

  • 使用双倍代替浮动的效率
  • 四舍五入后的浮动精度

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

我问这个问题,因为这个测试代码在使用浮动时失败并且不清楚为什么因为唯一的区别是使用float而不是double。

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?当然除了double以外的原因比浮点宽。

编辑: 有趣的是Assert.assertNotEquals(double,double,delta)在两种情况下都会加倍,所以失败测试中返回的浮点数会随着双倍而变宽,那么为什么测试失败呢?

编辑: 也许这个问题是相关的,但不确定: hex not the same

编辑: 从这个问题的答案hex not the same可以得出结论,浮动的科学表示IEEE 754 for .99对于相同的值不同于double。这是由于四舍五入。

因此我们得到了这个:

  • 0.99 - 0.97 = 0.01999998 //在浮动案例中
  • 0.99 - 0.97 = 0.020000000000000018 //双案例

由于上述单位测试中的最大增量为0.02且0.01999998(在失败测试中)低于增量值,这意味着数字看起来相同,但测试声明它们因此不会失败。

伙计们你同意这一切吗?

2 个答案:

答案 0 :(得分:2)

documentation for BigDecimal没有提及floatValue()轮如何进行。我认为它使用了舍入到最近的,甚至是连接的。

leftright分别设置为.99和.97。当这些在舍入到最近模式下转换为double时,结果为0.9899999999999999911182158029987476766109466552734375(十六进制浮点数,0x1.fae147ae147aep-1)和0.9699999999999999733546474089962430298328399658203125(0x1.f0a3d70a3d70ap-1)。当减去这些时,结果是0.020000000000000017763568394002504646778106689453125,显然超过.02。

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

简单地说,当十进制数字转换为浮点数时,舍入可能是向上或向下。它取决于数字相对于最近的可表示浮点值的位置。如果不对其进行控制或分析,则实际上是随机的。因此,有时候你最终会得到比你预期的更大的价值,有时你的价值会更低。

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

有很多关于如何处理浮点运算的信息,例如Handbook of Floating-Point Arithmetic。 Stack Overflow问题中涉及的主题太大了。有大学课程。

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

答案 1 :(得分: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用于浮点和双倍:

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%的峰值。

结论: 这个基准测试有点证实,使用double会占用CPU密集型应用程序上9%的时间,而在数据密集型应用程序中占用大约50%的时间。它还确认Eric Postpischil注意,与有限内存带宽的性能影响相比,CPU开销相对可忽略不计(9%)。