。NETでdoubleの乗算が壊れていますか? に関連する簡単な実験を実行し、C#文字列のフォーマットに関する記事をいくつか読んで、これを考えました。
{
double i = 10 * 0.69;
Console.WriteLine(i);
Console.WriteLine(String.Format(" {0:F20}", i));
Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i));
Console.WriteLine(String.Format("= {0:F20}", 6.9));
}
このCコードに相当するC#になります。
{
double i = 10 * 0.69;
printf ( "%f\n", i );
printf ( " %.20f\n", i );
printf ( "+ %.20f\n", 6.9 - i );
printf ( "= %.20f\n", 6.9 );
}
ただし、C#は出力を生成します。
6.9
6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000
デバッガーで(6.9ではなく)値6.89999999999999946709に等しいと表示されているにもかかわらず、
フォーマットで要求された精度を示すCと比較して:
6.900000
6.89999999999999946709
+ 0.00000000000000088818
= 6.90000000000000035527
どうしたの?
(Microsoft .NET Frameworkバージョン3.51 SP1/Visual Studio C#2008 Express Edition)
数値計算のバックグラウンドがあり、さまざまなプラットフォームで間隔計算(複雑な数値システムの精度の限界による誤差を推定する手法)の実装経験があります。賞金を得るには、ストレージの精度について説明しようとしないでください。この場合、64ビットdoubleの1 ULPの違いです。
報奨金を得るために、.NetがCコードで表示されるように要求された精度にdoubleをフォーマットする方法(またはどうか)を知りたいです。
問題は、.NETがdouble
を常に15桁の有効桁数に丸める前に、要求された精度に関係なくフォーマットを適用することですバイナリ形式の正確な10進値に関係なく、フォーマットを変更します。
Visual Studioデバッガーには、内部2進数に直接アクセスする独自の形式/表示ルーチンがあるため、C#コード、Cコード、およびデバッガー間の相違があると思います。
double
の正確な10進数値にアクセスしたり、double
を特定の小数点以下の桁数にフォーマットできるようにする組み込み機能はありませんが、これを行うことはできます。内部の2進数を分解し、10進数値の文字列表現として再構築することにより、自分で作成します。
あるいは、Jon Skeetの DoubleConverter
class (彼の "Binary floating point and .NET" article からリンク)を使用することもできます。これには、ToExactString
の正確な10進値を返すdouble
メソッドがあります。これを簡単に変更して、出力を特定の精度に丸めることができます。
double i = 10 * 0.69;
Console.WriteLine(DoubleConverter.ToExactString(i));
Console.WriteLine(DoubleConverter.ToExactString(6.9 - i));
Console.WriteLine(DoubleConverter.ToExactString(6.9));
// 6.89999999999999946709294817992486059665679931640625
// 0.00000000000000088817841970012523233890533447265625
// 6.9000000000000003552713678800500929355621337890625
Digits after decimal point
// just two decimal places
String.Format("{0:0.00}", 123.4567); // "123.46"
String.Format("{0:0.00}", 123.4); // "123.40"
String.Format("{0:0.00}", 123.0); // "123.00"
// max. two decimal places
String.Format("{0:0.##}", 123.4567); // "123.46"
String.Format("{0:0.##}", 123.4); // "123.4"
String.Format("{0:0.##}", 123.0); // "123"
// at least two digits before decimal point
String.Format("{0:00.0}", 123.4567); // "123.5"
String.Format("{0:00.0}", 23.4567); // "23.5"
String.Format("{0:00.0}", 3.4567); // "03.5"
String.Format("{0:00.0}", -3.4567); // "-03.5"
Thousands separator
String.Format("{0:0,0.0}", 12345.67); // "12,345.7"
String.Format("{0:0,0}", 12345.67); // "12,346"
Zero
Following code shows how can be formatted a zero (of double type).
String.Format("{0:0.0}", 0.0); // "0.0"
String.Format("{0:0.#}", 0.0); // "0"
String.Format("{0:#.0}", 0.0); // ".0"
String.Format("{0:#.#}", 0.0); // ""
Align numbers with spaces
String.Format("{0,10:0.0}", 123.4567); // " 123.5"
String.Format("{0,-10:0.0}", 123.4567); // "123.5 "
String.Format("{0,10:0.0}", -123.4567); // " -123.5"
String.Format("{0,-10:0.0}", -123.4567); // "-123.5 "
Custom formatting for negative numbers and zero
String.Format("{0:0.00;minus 0.00;zero}", 123.4567); // "123.46"
String.Format("{0:0.00;minus 0.00;zero}", -123.4567); // "minus 123.46"
String.Format("{0:0.00;minus 0.00;zero}", 0.0); // "zero"
Some funny examples
String.Format("{0:my number is 0.0}", 12.3); // "my number is 12.3"
String.Format("{0:0aaa.bbb0}", 12.3);
これを見てください MSDNリファレンス 。ノートでは、数値は要求された小数点以下の桁数に丸められると記載されています。
代わりに「{0:R}」を使用すると、「ラウンドトリップ」値と呼ばれる値が生成されます。詳細については、こちらをご覧ください MSDNリファレンス :
double d = 10 * 0.69;
Console.WriteLine(" {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);
output
6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000
この質問はその間閉じられていますが、この残虐行為がどのように発生したかについて言及する価値があると思います。ある意味では、C#仕様のせいにすることができます。これは、doubleの精度が15桁または16桁(IEEE-754の結果)でなければならないことを示しています。さらに少し(セクション4.1.6)実装はhigher精度を使用することが許可されていると述べています。注意:higher、低くはありません。 IEEE-754から逸脱することも許可されています。タイプx * y / z
の式で、x * y
は+/-INF
を生成しますが、分割後に有効な範囲内にあり、エラー。この機能により、コンパイラはパフォーマンスが向上するアーキテクチャでより高い精度を使用しやすくなります。
しかし、私は「理由」を約束しました。 clr/src/vm/comnumber.cpp
の- Shared Source CLI からの引用です(最近のコメントの1つでリソースをリクエストしました)。
」表示しやすくラウンドトリップ可能な数値を提供するために、15桁を使用して数値を解析し、同じ値に往復するかどうかを判断します。そのNUMBERを文字列に変換します。そうでない場合は、17桁を使用して再解析し、表示します。 "
言い換えると、MSのCLI開発チームは、ラウンドトリップ可能であり、読むのがそれほど苦痛ではないきれいな値を示すことにしました。良いか悪いか?オプトインまたはオプトアウトを希望します。
特定の番号のこのラウンドトリップ可能性を見つけるために行うトリックは?ジェネリックNUMBER構造体(doubleのプロパティ用の個別のフィールドがある)への変換とその逆、および結果が異なるかどうかの比較。異なる場合、正確な値が使用されます(6.9 - i
の中間値のように)、同じ場合は「きれいな値」が使用されます。
Andypへのコメントで既に述べたように、6.90...00
はビット単位で6.89...9467
と等しくなります。これで、0.0...8818
が使用される理由がわかりました。0.0
とはビット単位で異なります。
この15桁の障壁はハードコードされており、CLIを再コンパイルするか、Monoを使用するか、Microsoftを呼び出してオプションを追加するように説得することによってのみ変更できます完全な「精度」を印刷します(実際には精度ではありませんが、より良いWordがないためです)。自分で52ビットの精度を計算するか、前述のライブラリを使用する方が簡単でしょう。
編集:IEE-754浮動小数点を試してみたい場合は、 このオンラインツールを検討してください 。浮動小数点のすべての関連部分が表示されます。
つかいます
Console.WriteLine(String.Format(" {0:G17}", i));
これにより、17桁すべてが得られます。デフォルトでは、Double値には15桁の精度が含まれますが、内部では最大17桁が維持されます。 {0:R}は常に17桁を与えるわけではなく、その精度で数値を表現できる場合は15を与えます。
数値をその精度で表現できる場合は15桁、最大精度でしか表現できない場合は17桁を返します。 doubleが実装する方法であるより多くの数字を返すようにするためにできることは何もありません。気に入らない場合は、新しいダブルクラスを自分で実行してください...
.NETのダブルカントは17を超える桁を格納できないため、デバッガで6.89999999999999946709が表示されず、6.8999999999999995が表示されます。間違っていることを証明するために画像を提供してください。
これに対する答えは簡単で、 [〜#〜] msdn [〜#〜] にあります。
浮動小数点数は10進数にしか近似できないこと、および浮動小数点数の精度によってその数値が10進数に近似する精度が決まることに注意してください。デフォルトでは、Double値には10桁の精度の15桁の精度が含まれますが、内部では最大17桁が維持されます。
あなたの例では、iの値は6.89999999999999946709であり、3桁目から16桁目までのすべての位置に9の数字があります(数字の整数部分をカウントすることを忘れないでください)。文字列に変換するとき、フレームワークは数値を15桁に丸めます。
i = 6.89999999999999 946709
digit = 111111 111122
1 23456789012345 678901
結果を再現しようとしましたが、デバッガで「i」を見ると、質問で書いたように「6.89999999999999946709」ではなく「6.8999999999999995」と表示されていました。見たものを再現する手順を提供できますか?
デバッガーが表示する内容を確認するには、次のコード行のようにDoubleConverterを使用できます。
Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));
お役に立てれば!
編集:私は思ったよりも疲れていると思います。もちろん、これはラウンドトリップ値への書式設定と同じです(前述のとおり)。
答えは「はい」です。NETでは二重印刷が壊れており、末尾のゴミ数字を印刷しています。
あなたはそれを正しく実装する方法を読むことができます here 。
IronSchemeでも同じことをしなければなりませんでした。
> (* 10.0 0.69)
6.8999999999999995
> 6.89999999999999946709
6.8999999999999995
> (- 6.9 (* 10.0 0.69))
8.881784197001252e-16
> 6.9
6.9
> (- 6.9 8.881784197001252e-16)
6.8999999999999995
注:CとC#の両方に正しい値があり、印刷が壊れています。
更新:私はまだこの発見につながったメーリングリストの会話を探しています。
この簡単な修正が見つかりました。
double i = 10 * 0.69;
System.Diagnostics.Debug.WriteLine(i);
String s = String.Format("{0:F20}", i).Substring(0,20);
System.Diagnostics.Debug.WriteLine(s + " " +s.Length );