0

編集:この質問は2つのトピックをカバーしています:

  • フロートの代わりにダブルで使用する効率
  • 丸め後の浮動小数点精度

float の代わりに常に Java double を使用すべきではない理由はありますか?

この質問をするのは、float を使用する場合のこのテスト コードが失敗し、唯一の違いは 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)

このテストが失敗する理由と、float の代わりに常に double を使用するべきではない理由について何か考えはありますか? もちろん、ダブル以外の理由は、フロートよりも広いです。

編集: 面白いことに、Assert.assertNotEquals(double,double,delta) はどちらの場合も double を取るため、失敗したテストで返された浮動小数点数はとにかく double として拡張されているので、なぜテストが失敗するのでしょうか?

編集:この他の質問が関連している可能性がありますが、よく わかりません:16進数は同じではありません

編集: この質問hex not the sameへの回答から、float の .99 に対する IEEE 754 の科学的表現は、同じ値の double とは異なると結論付けることができます。これは丸めによるものです。

したがって、次のようになります。

  • 0.99 - 0.97 = 0.01999998 //フロートの場合
  • 0.99 - 0.97 = 0.020000000000000018 //二重の場合

上記の単体テストの最大デルタは 0.02 であり、(失敗したテストでは) 0.01999998 はデルタ値を下回っているため、数値は同じように見えますが、テストはそれらが失敗していないと主張していることを意味します。

みんな、これに同意しますか?

4

2 に答える 2

2

BigDecimalのドキュメントでは、floatValue()丸め方については言及されていません。私は、最も近いものへの丸め、偶数への結び付きを使用していると思います。

leftrightそれぞれ .99 と .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 となり、明らかに .02 を超えています。

.99 と .97 を に変換するfloatと、結果は 0.9900000095367431640625 (0x1.fae148p-1) と 0.9700000286102294921875 (0x1.f0a3d8p-1) になります。これらを差し引くと、結果は 0.019999980926513671875 となり、明らかに .02 より小さくなります。

簡単に言えば、10 進数を浮動小数点数に変換する場合、丸めは上または下になります。最も近い表現可能な浮動小数点値に対して、数値がたまたまどこにあるかによって異なります。制御または分析されていない場合、事実上ランダムです。したがって、予想よりも大きな値になることもあれば、小さな値になることもあります。

doubleの代わりに使用しても、上記のような結果が発生しないことは保証されませんfloat。この場合の値が正確な数学的値を超えていて、値が超えていなかったことは単なる偶然です。他の数値では、逆になる可能性があります。たとえば、 withは.02 未満ですが、 with , .09f - .07f` は .02 より大きくなります。doublefloatdouble.09-.07float

Handbook of Floating-Point Arithmeticなど、浮動小数点演算の処理方法に関する多くの情報があります。これは、Stack Overflow の質問で扱うには大きすぎるテーマです。その上に大学のコースがあります。

多くの場合、今日の典型的なプロセッサでdoubleは、float. 単純なスカラー浮動小数点演算は、 と でほぼ同じ速度で実行されdoubleますfloat。データが多すぎて (ディスクからメモリへ、またはメモリからプロセッサへ) 転送する時間が重要になったり、データがディスク上で占有するスペースが大きくなったり、ソフトウェアがプロセッサの SIMD 機能を使用したりすると、パフォーマンスの違いが生じます。(SIMD により、プロセッサーは複数のデータに対して同じ操作を並行して実行できます。現在のプロセッサーは通常、SIMD 操作に対してfloatSIMD 操作の約 2 倍の帯域幅をdouble提供するか、SIMD 操作をまったく提供しませんdouble。)

于 2013-10-26T22:11:29.267 に答える
1

double は有効桁数の大きい数値を表すことができ、float の場合はその逆になります。二重計算は、CPU に関してよりコストがかかります。したがって、すべてはアプリケーションに依存します。2 進数は、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 で実行されました。タイトなループで 1 つの変数のみを扱っているため、利用可能なメモリ帯域幅を圧倒する方法はないことに注意してください。実際、コアの 1 つは、各実行中に一貫して 100% の CPU を示していました。結論: 差は 9% であり、実際には無視できると考えられます。

2 番目のテストには同じテストが含まれますが、float と double にそれぞれ 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% 時間がかかることが多少確認されます。また、制限されたメモリ帯域幅のパフォーマンスへの影響と比較して、CPU オーバーヘッドが比較的無視できる (9%) というEric Postpischil のメモも確認できます。

于 2013-10-25T13:20:45.320 に答える