15

数学的に、この問題の有理数を考えてください

8725724278030350 / 2**48

ここ**で、分母は累乗を表します。つまり、分母は2348乗です。(分数は最低条件ではなく、2 で減数できます。) この数はとして正確に表現できSystem.Doubleます。その10進展開は

31.0000000000000'49'73799150320701301097869873046875 (exact)

ここで、アポストロフィは欠落している数字を表すのではなく、単に15に丸める境界を示すだけです。17桁が実行されます。

次の点に注意してください: この数値が 15 桁に丸められる場合、次の桁 ( ) が (切り捨てを意味する) で始まるため、結果は31(その後に 13 が続きます) になります。しかし、数値が最初に 17 桁に丸められ、次に15 桁に丸められると、結果は. これは、最初の丸めでは桁数を(次の桁は) に増やすことで切り上げられ、2 回目の丸めでは再び切り上げられる可能性があるためです (中点の丸めルールで「ゼロから四捨五入する」と指定されている場合)。049...431.000000000000149...50 (terminates)73...

(もちろん、上記の特徴を持つ数字は他にもたくさんあります。)

ここで、この数値の .NET の標準文字列表現は"31.0000000000001". 質問: これはバグではありませんか? 標準的な文字列表現とはString、parameterlesDouble.ToString()インスタンス メソッドによって生成される を意味します。これは、 によって生成されるものと同じですToString("G")

注目すべき興味深い点は、上記の数値を にキャストすると、正確にSystem.Decimalが得られるdecimalことです! aを toにキャストすると最初に 15 桁に丸められるという驚くべき事実については、この Stack Overflow の質問31を参照してください。これは、 to にキャストすると 15 桁に正しく丸められるのに対し、呼び出すと正しくない丸めが行われることを意味します。DoubleDecimalDecimalToSting()

要約すると、ユーザーへの出力時には である浮動小数点数が得られます31.0000000000001が、変換するとDecimal( 29桁が使用可能)、31正確になります。これは残念です。

問題を確認するための C# コードを次に示します。

static void Main()
{
  const double evil = 31.0000000000000497;
  string exactString = DoubleConverter.ToExactString(evil); // Jon Skeet, http://csharpindepth.com/Articles/General/FloatingPoint.aspx 

  Console.WriteLine("Exact value (Jon Skeet): {0}", exactString);   // writes 31.00000000000004973799150320701301097869873046875
  Console.WriteLine("General format (G): {0}", evil);               // writes 31.0000000000001
  Console.WriteLine("Round-trip format (R): {0:R}", evil);          // writes 31.00000000000005

  Console.WriteLine();
  Console.WriteLine("Binary repr.: {0}", String.Join(", ", BitConverter.GetBytes(evil).Select(b => "0x" + b.ToString("X2"))));

  Console.WriteLine();
  decimal converted = (decimal)evil;
  Console.WriteLine("Decimal version: {0}", converted);             // writes 31
  decimal preciseDecimal = decimal.Parse(exactString, CultureInfo.InvariantCulture);
  Console.WriteLine("Better decimal: {0}", preciseDecimal);         // writes 31.000000000000049737991503207
}

上記のコードは Skeet のToExactString方法を使用しています。彼のもの (URL から見つけることができます) を使用したくない場合は、上記の依存関係のコード行を削除してくださいexactStringDouble問題の ( evil) がどのように丸められてキャストされるかをまだ確認できます。

添加:

わかりましたので、さらにいくつかの数値をテストしました。これが表です。

  exact value (truncated)       "R" format         "G" format     decimal cast
 -------------------------  ------------------  ----------------  ------------
 6.00000000000000'53'29...  6.0000000000000053  6.00000000000001  6
 9.00000000000000'53'29...  9.0000000000000053  9.00000000000001  9
 30.0000000000000'49'73...  30.00000000000005   30.0000000000001  30
 50.0000000000000'49'73...  50.00000000000005   50.0000000000001  50
 200.000000000000'51'15...  200.00000000000051  200.000000000001  200
 500.000000000000'51'15...  500.00000000000051  500.000000000001  500
 1020.00000000000'50'02...  1020.000000000005   1020.00000000001  1020
 2000.00000000000'50'02...  2000.000000000005   2000.00000000001  2000
 3000.00000000000'50'02...  3000.000000000005   3000.00000000001  3000
 9000.00000000000'54'56...  9000.0000000000055  9000.00000000001  9000
 20000.0000000000'50'93...  20000.000000000051  20000.0000000001  20000
 50000.0000000000'50'93...  50000.000000000051  50000.0000000001  50000
 500000.000000000'52'38...  500000.00000000052  500000.000000001  500000
 1020000.00000000'50'05...  1020000.000000005   1020000.00000001  1020000

最初の列は、 が表す正確な (切り捨てられた) 値をDouble示します。"R"2 番目の列は、フォーマット文字列からの文字列表現を示します。3 列目は通常の文字列表現です。最後に、4 番目の列は、System.Decimalthis を変換した結果の を示しますDouble

結論は次のとおりです。

  • 15 桁にToString()丸め、変換で 15 桁に丸めるとDecimal、非常に多くの場合、一致しません。
  • への変換Decimalも多くの場合、正しく丸められず、これらの場合のエラーは「2 回のラウンド」エラーとは言えません。
  • 私の場合、それらが一致しない場合、変換ToString()よりも大きな数値が得られるようです(2 つのラウンドのどちらが正しくても)。Decimal

上記のようなケースでのみ実験しました。他の「フォーム」の数に丸め誤差があるかどうかは確認していません。

4

5 に答える 5

9

したがって、あなたの実験から、Double.ToString正しい丸めを行わないように見えます。

これはかなり残念なことですが、特に驚くべきことではありません。正しく丸められた double から string への変換と string から double への変換に関係するものの 1 つの例については、David Gay のdtoa.cコードを参照してください。(Python は現在、浮動小数点数から文字列への変換と文字列から浮動小数点数への変換に、このコードの変形を使用しています。)

浮動小数点演算の現在の IEEE 754 標準でさえ、 を推奨していますが、バイナリ浮動小数点型から 10 進文字列への変換が常に正しく丸められることを要求していません。セクション 5.12.2「有限数を表す外部の 10 進文字シーケンス」からのスニペットを次に示します。

サポートされているバイナリ形式との間で正しい丸めを使用して変換できる有効桁数には、実装定義の制限がある場合があります。その極限 H は、H ≥ M+3 であり、H は無制限でなければなりません。

ここでは、サポートされているすべてのバイナリ形式Mの最大値として定義されています。は として定義されており、.NET はタイプを介して float64 形式をサポートしているため、少なくとも.NET では必要です。つまり、.NET が標準に従う場合、少なくとも 20 桁の有効数字まで正しく丸められた文字列変換を提供することになります。したがって、.NETはこの標準を満たしていないように見えます。Pmin(bf)bfPmin(float64)17DoubleM17Double

「これはバグですか」という質問への回答として、バグであってほしいと思いますが、正確性や IEEE 754 準拠の主張は、数値形式のドキュメントのどこにも見当たりません。 .NET 用。したがって、望ましくないと考えられるかもしれませんが、実際のバグとは言い難いでしょう。


編集: Jeppe Stig Nielsen は、MSDN のSystem.Doubleページに次のように記載されていることを指摘しています。

Double は、バイナリ浮動小数点演算の IEC 60559:1989 (IEEE 754) 標準に準拠しています。

この準拠宣言が何をカバーすることになっているのか正確にはわかりませんが、IEEE 754 の古い 1985 年バージョンでさえ、説明されている文字列変換はその標準の 2 進数から 10 進数への要件に違反しているようです。

それを踏まえて、喜んで評価を「バグの可能性」にアップグレードします。

于 2012-06-18T16:36:22.017 に答える
6

最初に、このページの下部を見てください。これは、非常によく似た「二重丸め」の問題を示しています。

次の浮動小数点数の 2 進数/16 進数表現を確認すると、指定された範囲が double 形式の同じ数値として格納されていることがわかります。

31.0000000000000480 = 0x403f00000000000e
31.0000000000000497 = 0x403f00000000000e
31.0000000000000515 = 0x403f00000000000e

他の何人かが指摘しているように、これは最も近い表現可能な double の正確な値が 31.00000000000004973799150320701301097869873046875 であるためです。

特に .NET 環境では、IEEE 754 から文字列への順方向および逆方向の変換で考慮すべき追加の 2 つの側面があります。

まず (一次情報源が見つかりません) ウィキペディアから:

10 進数の有効桁数が最大 15 の 10 進文字列を IEEE 754 倍精度に変換してから、同じ桁数の有効桁数に戻す場合、最終的な文字列は元の文字列と一致する必要があります。また、IEEE 754 倍精度が 10 進数の有効桁数が 17 以上の 10 進数文字列に変換されてから倍精度に戻される場合、最終的な数値は元の数値と一致する必要があります。

したがって、標準への準拠に関して、文字列 31.0000000000000497 を double に変換すると、文字列に戻すときに必ずしも同じになるとは限りません (指定された小数点以下の桁数が多すぎます)。

2 番目の考慮事項は、double から string への変換が 17 桁の有効数字を持たない限り、その丸め動作も標準で明示的に定義されていないことです。

さらに、Double.ToString()に関するドキュメントは、現在のカルチャ設定の数値書式指定子によって管理されていることを示しています。

考えられる完全な説明:

2 回の丸めが次のように発生していると思われます。最初の 10 進文字列は 16 桁または 17 桁の有効数字に作成されます。これは、31.00000000000005 または 31.000000000000050 の中間結果を与える「往復」変換に必要な精度であるためです。次に、既定のカルチャ設定により、結果は有効数字 15 桁 (31.00000000000001) に丸められます。

一方、Decimal への中間変換を行うと、別の方法でこの問題を回避できます。有効桁数 15 桁に直接切り捨てられます。

于 2012-06-18T15:11:55.967 に答える
-1

私はもっ​​と単純な疑いを持っています: 犯人はおそらく pow 演算子 => **; です。数値double として正確に表現できますが、便宜上の理由から (べき乗演算子が正しく機能するには多くの作業が必要です)、べき乗は指数関数によって計算されます。これは、pow() が非常に高価であるため、pow() を使用する代わりに数値を繰り返し乗算することによってパフォーマンスを最適化できる理由の 1 つです。

したがって、正しい 2^48 は得られませんが、少し間違っているため、丸めの問題があります。2^48 が正確に何を返すかを調べてください。

編集:申し訳ありませんが、私は問題をスキャンしただけで、間違った疑いを与えました。Intel プロセッサの二重丸めには既知の問題があります。古いコードは、エラーを引き起こす可能性がある SSE 命令の代わりに、FPU の内部 80 ビット形式を使用します。値は 80 ビット レジスタに正確に書き込まれ、2 回丸められるため、Jeppe はすでに問題を発見し、きちんと説明しています。

バグですか?まあ、プロセッサはすべてを正しく行っています。これは、Intel FPU が内部的に浮動小数点演算の精度を高めていることが問題です。

さらなる編集と情報: 「二重丸め」は既知の問題であり、Jean-Michel Muller らによる「Handbook of Floating-Point Arithmetic」で明示的に言及されています。アル。75ページの「3.3.1典型的な問題:「二重丸め」」の「改訂の必要性」の章:

使用されているプロセッサは、プログラムの変数の精度よりも広い内部精度を提供する場合があります (典型的な例は、プログラムの変数が単精度または倍精度である場合に Intel プラットフォームで使用可能な拡張倍精度形式です)。浮動小数点数)。このセクションで説明するように、これには奇妙な副作用がある場合があります。Cプログラムを考えてみましょう[...]

#include <stdio.h>

int main(void) 
{
  double a = 1848874847.0;
  double b = 19954562207.0;
  double c;
  c = a * b;
  printf("c = %20.19e\n", c);
  return 0;
}

32 ビット: Linux/Debian 上の GCC 4.1.2 20061115

Compilerswitch または -mfpmath=387 (80 ビット FPU) を使用: 3.6893488147419103232e+19 -march=pentium4 -mfpmath=sse (SSE) または 64 ビット: 3.6893488147419111424e+19

本で説明されているように、不一致の解決策は、80 ビットと 53 ビットの二重丸めです。

于 2012-06-18T18:55:48.340 に答える