web-dev-qa-db-ja.com

合計の順序を変更すると異なる結果が返されるのはなぜですか?

合計の順序を変更すると異なる結果が返されるのはなぜですか?

23.53 + 5.88 + 17.64=47.05

23.53 + 17.64 + 5.88=47.050000000000004

JavaJavaScript の両方が同じ結果を返します。

私は、浮動小数点数が2進数で表現される方法のために、いくつかの有理数(like 1/3-0.333333 ...)を正確に表現できないことを理解しています。

単純に要素の順序を変更すると、結果に影響するのはなぜですか?

292

この質問はばかげているのかもしれませんが、なぜ要素の順序を変更するだけで結果に影響するのでしょうか?

大きさに基づいて、値が丸められるポイントを変更します。私たちが見ているもののkindの例として、2進浮動小数点の代わりに、10進浮動小数点型を使用しているふりをしましょう有効数字4桁。各加算は「無限」精度で実行され、最も近い表現可能な数値に丸められます。以下に2つの合計を示します。

1/3 + 2/3 + 2/3 = (0.3333 + 0.6667) + 0.6667
                = 1.000 + 0.6667 (no rounding needed!)
                = 1.667 (where 1.6667 is rounded to 1.667)

2/3 + 2/3 + 1/3 = (0.6667 + 0.6667) + 0.3333
                = 1.333 + 0.3333 (where 1.3334 is rounded to 1.333)
                = 1.666 (where 1.6663 is rounded to 1.666)

これが問題になるために、非整数も必要ありません。

10000 + 1 - 10000 = (10000 + 1) - 10000
                  = 10000 - 10000 (where 10001 is rounded to 10000)
                  = 0

10000 - 10000 + 1 = (10000 - 10000) + 1
                  = 0 + 1
                  = 1

これは、重要な部分が、限られた数の有効桁数-限られた数ではなく小数点以下の桁数。常に同じ数の小数点以下の桁数を維持できれば、少なくとも加算と減算で問題ありません(値がオーバーフローしない限り)。問題は、より大きな数値に到達すると、より小さな情報が失われることです。この場合、10001は10000に丸められます。 (これは Eric Lippertが答えで指摘した という問題の例です。)

右側の最初の行の値はすべての場合で同じであることに注意することが重要です-したがって、10進数(23.53、5.88、17.64)がdouble値。これは、上記の問題のために問題になります。

275
Jon Skeet

これがバイナリで行われていることです。知っているように、一部の浮動小数点値は、10進数で正確に表現できる場合でも、2進数で正確に表現できません。これらの3つの数字は、その事実の単なる例です。

このプログラムでは、各数値の16進表現と各加算の結果を出力しました。

public class Main{
   public static void main(String args[]) {
      double x = 23.53;   // Inexact representation
      double y = 5.88;    // Inexact representation
      double z = 17.64;   // Inexact representation
      double s = 47.05;   // What math tells us the sum should be; still inexact

      printValueAndInHex(x);
      printValueAndInHex(y);
      printValueAndInHex(z);
      printValueAndInHex(s);

      System.out.println("--------");

      double t1 = x + y;
      printValueAndInHex(t1);
      t1 = t1 + z;
      printValueAndInHex(t1);

      System.out.println("--------");

      double t2 = x + z;
      printValueAndInHex(t2);
      t2 = t2 + y;
      printValueAndInHex(t2);
   }

   private static void printValueAndInHex(double d)
   {
      System.out.println(Long.toHexString(Double.doubleToLongBits(d)) + ": " + d);
   }
}

printValueAndInHexメソッドは、16進数のプリンターヘルパーです。

出力は次のとおりです。

403787ae147ae148: 23.53
4017851eb851eb85: 5.88
4031a3d70a3d70a4: 17.64
4047866666666666: 47.05
--------
403d68f5c28f5c29: 29.41
4047866666666666: 47.05
--------
404495c28f5c28f6: 41.17
4047866666666667: 47.050000000000004

最初の4つの数字は、xyz、およびsの16進表現です。 IEEE浮動小数点表現では、ビット2〜12はバイナリexponent、つまり数値のスケールを表します。 (最初のビットは符号ビットで、残りのビットは仮数です。)実際に表現される指数は、2進数から1023を引いたものです。

最初の4つの数値の指数が抽出されます。

    sign|exponent
403 => 0|100 0000 0011| => 1027 - 1023 = 4
401 => 0|100 0000 0001| => 1025 - 1023 = 2
403 => 0|100 0000 0011| => 1027 - 1023 = 4
404 => 0|100 0000 0100| => 1028 - 1023 = 5

追加の最初のセット

2番目の数値(y)は、より小さい値です。これらの2つの数値を追加してx + yを取得すると、2番目の数値の最後の2ビット(01)は範囲外にシフトされ、計算に反映されません。

2番目の追加は、x + yzを追加し、同じスケールの2つの数値を追加します。

追加の2番目のセット

ここでは、x + zが最初に発生します。それらは同じ規模ですが、規模が大きくなるほど数値が大きくなります。

404 => 0|100 0000 0100| => 1028 - 1023 = 5

2番目の追加はx + zyを追加し、今では3ビットがyから削除されて追加されます番号(101)。ここでは、結果が次の浮動小数点数になるため、上方に丸める必要があります。最初の追加セットの4047866666666666と2番目の追加セットの4047866666666667です。このエラーは、合計の印刷に表示するのに十分なほど重要です。

結論として、IEEE番号に対して数学演算を実行するときは注意してください。一部の表現は不正確であり、縮尺が異なるとさらに不正確になります。可能であれば、同様のスケールの数値を加算および減算します。

52
rgettman

ジョンの答えはもちろん正しいです。あなたの場合、エラーは、単純な浮動小数点演算を実行して蓄積するエラーよりも大きくありません。あるケースではエラーがゼロになり、別のケースでは小さなエラーになるシナリオがあります。それは実際にはそれほど興味深いシナリオではありません。良い質問は次のとおりです。計算の順序を小さなエラーから(比較的)巨大なエラーに変更するシナリオはありますか?答えは明白にイエスです。

例について考えてみましょう:

x1 = (a - b) + (c - d) + (e - f) + (g - h);

x2 = (a + c + e + g) - (b + d + f + h);

x3 = a - b + c - d + e - f + g - h;

厳密な算術では明らかに同じです。 x1とx2とx3の値が大きく異なるように、a、b、c、d、e、f、g、hの値を見つけようとするのは楽しいことです。できるかどうかを確認してください!

44
Eric Lippert

これは、実際にはJavaおよびJavascriptだけではなく、floatまたはdoubleを使用するプログラミング言語に影響を与える可能性があります。

メモリでは、浮動小数点はIEEE 754のラインに沿って特別な形式を使用します(コンバーターは、私よりもはるかに良い説明を提供します)。

とにかく、ここにフロートコンバーターがあります。

http://www.h-schmidt.net/FloatConverter/

操作の順序に関することは、操作の「細かさ」です。

最初の行では、最初の2つの値から29.41が得られ、2 ^ 4が指数として得られます。

2行目では41.17が得られ、2 ^ 5が指数として得られます。

指数を大きくすることで有意な数字を失い、結果を変える可能性があります。

41.17の右端の最後のビットをオンおよびオフにしてみてください。指数の1/2 ^ 23のような「重要でない」何かがこの浮動小数点の差を引き起こすのに十分であることがわかります。

編集:重要な数字を覚えている人にとって、これはそのカテゴリーに該当します。有効数字が1の10 ^ 4 + 4999は10 ^ 4になります。この場合、有効数字ははるかに小さくなりますが、.00000000004が添付された結果を見ることができます。

10
Compass

浮動小数点数は、IEEE 754形式を使用して表されます。これは、仮数(仮数)に特定のサイズのビットを提供します。残念ながら、これは特定の数の「分数のビルディングブロック」を使用して再生するため、特定の分数の値を正確に表すことができません。

あなたのケースで起こっているのは、2番目のケースでは、追加が評価される順序のために、おそらく追加が何らかの精度の問題に直面しているということです。値は計算していませんが、たとえば23.53 + 5.64は正確に表すことができますが、23.53 + 17.64は正確に表すことができません。

残念ながら、これは既知の問題であり、対処する必要があります。

9
jbx

評価の順序に関係していると思います。合計は数学の世界では自然に同じですが、バイナリの世界ではA + B + C = Dではなく、

A + B = E
E + C = D(1)

そのため、浮動小数点数が取得できる2次ステップがあります。

順序を変更すると、

A + C = F
F + B = D(2)
6
hotforfeature