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


294

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

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

JavaJavaScriptはどちらも同じ結果を返します。

浮動小数点数が2進数で表現される方法が原因で、一部の有理数(1/3-0.333333 ...など)を正確に表現できないことを理解しています。

要素の順序を変更するだけで結果が影響を受けるのはなぜですか?


28
実数の合計は連想的かつ可換的です。浮動小数点は実数ではありません。実際、あなたは彼らの活動が可換ではないことを証明しました。それらが連想的でないことを示すのも簡単(2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1)です(例:)。したがって、はい。合計の順序やその他の演算を選択するときは注意してください。一部の言語には「高精度」の合計を実行する組み込みmath.fsum関数が用意されているため(たとえばpythonなど)、単純な合計アルゴリズムの代わりにこれらの関数の使用を検討する場合があります。
バクリウ2013年

1
@RBerteigこれは、算術式に対する言語の演算順序を調べることで決定できます。また、メモリ内の浮動小数点数の表現が異なる場合を除き、演算子の優先順位規則が同じであれば、結果は同じになります。もう1つの注意点:銀行アプリケーションを開発する開発者がこれを理解するのにどれくらいの時間がかかったのでしょうか。余分な0000000000004セントは本当に加算されます!
Chris Cirefice 2013年

3
@ChrisCirefice:0.00000004 セントの場合、それは間違っています。財務計算には2進浮動小数点型を使用しないでください。
Daniel Pryden 2013年

2
@DanielPrydenああ、それは冗談でした...このタイプの問題を本当に解決する必要がある人々はあなたが知っている最も重要な仕事の1つを持っていて、人々の金銭的地位を保持しているという考えを捨てました。私は非常に皮肉でした...
Chris Cirefice

6
非常に乾燥している(そして古いが、それでも関連性がある):すべてのコンピューター科学者が浮動小数点演算について知っておくべきこと
ブライアン

回答:


276

この質問は愚かかもしれませんが、要素の順序を変更するだけで結果が影響を受けるのはなぜですか?

値の大きさに基づいて、値が丸められるポイントを変更します。このの例として、2進浮動小数点の代わりに、有効桁数が4桁の10進浮動小数点型を使用していたとします。各加算は「無限」の精度で実行され、次に丸められます。最も近い表現可能な数。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値として正確に表されないことを理解することが重要ですが、上記の問題のため、問題のみ。


10
May extend this later - out of time right now!それ@ジョンのために熱心に待っている
Prateek

3
私が後で回答に戻ると言うとき、コミュニティは私に少し親切ではありません<冗談ではなく、冗談ではないことを示すためにここにある種の軽い心の絵文字を入力してください> ...後でこれに戻ります。
Gradyプレーヤー

2
@ZongZhengLi:それを理解することは確かに重要ですが、それはこの場合の根本的な原因ではありません。あなたは値で同様の例を書くことができているバイナリで正確に表現し、同じ効果を参照してください。ここでの問題は、大規模な情報と小規模な情報を同時に維持することです。
Jon Skeet 2013年

1
@Buksy:10000に四捨五入-有効数字4桁しか格納できないデータ型を扱っているため。(so x.xxx * 10 ^ n)
Jon Skeet

3
@meteors:いいえ、オーバーフローは発生しません-間違った数値を使用しています。10001は10000に丸められ、1001は1000に丸められません。明確にするために、54321は54320に丸められます。これは、有効数字が4桁しかないためです。「有効数字4桁」と「最大値9999」の間には大きな違いがあります。前に述べたように、基本的にx.xxx * 10 ^ nを表します。10000の場合、x.xxxは1.000、nは4になります。これはdoubleandと同じfloatです。 1つ以上離れています。
Jon Skeet 2013年

52

これがバイナリで何が起こっているかです。ご存知のように、一部の浮動小数点値は、たとえ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の進表現。IEEE浮動小数点表現では、ビット2〜12は2進指数、つまり数値のスケールを表します。(最初のビットは符号ビットで、残りのビットは仮数です。)表される指数は、実際には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)は、より小さな値です。getにこれら2つの数値を加算するx + yと、2番目の数値(01)の最後の2ビットが範囲外にシフトされ、計算に含まれません。

第二添加を追加x + yしてz、同じ規模の2つの数値を追加します。

追加の2番目のセット

ここでx + zは、最初に発生します。それらは同じスケールですが、スケールがより高い数値を生成します。

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

2番目の加算によりx + zとが加算されy3ビットがから削除さyれて数値が加算されます(101)。4047866666666666最初の加算4047866666666667セットと2番目の加算セットでは結果が次の浮動小数点数になるため、ここでは上向きの丸めが必要です。そのエラーは、合計のプリントアウトに表示するのに十分なほど重大です。

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


スケールの違いは重要な部分です。入力としてバイナリで表されている正確な値を(10進数で)書いても、同じ問題が発生する可能性があります。
Jon Skeet 2013年

@rgettmanプログラマーとして、私はあなたの答えを=)16進プリンターヘルパーに+1するのが好きです... それは本当にすてきです!
ADTC 2013年

44

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

例を考えてみましょう:

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の値を見つけようとするのは楽しいことです。できるかどうか確認してください!


大量をどのように定義しますか?1000分の1程度で話しているのでしょうか。100分の1?1の???
たけ

3
@Cruncher:正確な数学的結果とx1およびx2値を計算します。真の結果と計算結果e1とe2の正確な数学的差異を呼び出します。エラーのサイズについて考える方法はいくつかあります。1つ目は、次のいずれかのシナリオを見つけることができますか。e1 / e2 | または| e2 / e1 | 大きい?のように、あなたは他のエラーの10倍のエラーをすることができますか?しかし、より興味深いのは、1つのエラーを正解のサイズのかなりの部分にできるかどうかです。
Eric Lippert

1
私は彼がランタイムについて話していることに気づきましたが、私は疑問に思います:式がコンパイル時(たとえば、constexpr)式であった場合、コンパイラーはエラーを最小限に抑えるのに十分スマートですか?
Kevin Hsu

@kevinhsu一般的にいいえ、コンパイラはそれほどスマートではありません。もちろん、コンパイラーは、正確に算術演算を行うことを選択できますが、通常はそうしません。
Eric Lippert、2013年

8
@frozenkoi:はい、エラーは非常に簡単に無限になります。たとえば、C#を考慮する:double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);-出力が0と無限である
ジョンスキート

10

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

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

とにかく、これがfloatコンバータです。

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

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

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

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

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

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

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


9

浮動小数点数は、仮数(仮数)に特定のサイズのビットを提供するIEEE 754形式を使用して表されます。残念ながら、これにより、特定の数の「小数の構成要素」を操作することができ、特定の小数値は正確に表すことができません。

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

残念ながらそれはあなたが対処しなければならない既知の問題です。


6

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

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

したがって、浮動小数点数が降りることのできる二次ステップがあります。

順番を変えると

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

4
この答えは本当の理由を避けていると思います。「浮動小数点数が降りることのできる二次ステップがあります」。明らかにこれは本当ですが、説明したいのはその理由です。
ゾン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.