DOBON.NET DOBON.NETプログラミング掲示板過去ログ

C#とVC++で同じdouble型変数の内部表現が異なるのはなぜ?

環境/言語:[Win7 Sp1 / VS 2010 / .Net 4.0]
分類:[その他]

現在、VC++で作成されたアプリをC#に移植する作業をしています。
VC++のアプリとC#のアプリで、同じdouble型数値の表示が異なる場合が
あるため、原因をさぐったところ下記のことがわかりました。

・VC++のdouble型変数に"26.7095"を格納し、デバッガで追いかけると
 内部表現は"26.7094999...."となり、表示部では小数点以下3桁なので
 アプリの表示は"26.709"となる。

・C#で同様にdouble型変数に"26.7095"を格納すると、内部表現も"26.7095"
 なので、アプリ上の表示は"26.710"となる。

1/1000程度の違いは問題視されないと思いますが、原因を顧客側に説明しないと
いけなのですが、同じIEEE表現のはずのVC++とC#で何故このような違いが
発生するのかわかりません。

どなたか解説していただけないでしょうか。

よろしくお願いいたします。
■No31557に返信(Xn68000さんの記事)

まずは提示された結果を得た過程が分からないので
予想ですが、
文字列をdouble値に変換しているためVC++(バージョン?)の
ライブラリとC#のライブラリの違いによる変換差異が発生しているのでは
ないでしょうか?

より正確な値を扱いたければdoubleではなくdecimalを使われた方が
良いかと思います。
shuさん、ありがとうございます。

記事中で数字を””でくくってますが、これは別に文字列を意味して
いるのではなく、単に強調したかっただけで、実際のソースコードでは
数値として扱っています。

また、本記事ではタイトルにもあります通り、
 「C#とVC++でdouble型変数の内部表現が異なるのは何故か?」
についてお聞きしたい というのが目的ですので、
精度を求めている訳ではありません。

以上よろしくお願いいたします。
■No31557に返信(Xn68000さんの記事)
> ・VC++のdouble型変数に"26.7095"を格納し、
> ・C#で同様にdouble型変数に"26.7095"を格納すると、

どのように格納したのか、にもよりますが、.7095 という値は、
Double の二進小数桁数では表しきれないと思いますので、この時点で
近似値になってしまっていると思います。

また、数値リテラルからの代入にしても、必ずしも同じ結果が
保証されるわけではありません。32bit/64bit の違いだけでも、
リテラルの結果が異なることはありえますし、下記の資料においても
『Double 値を使用した算術演算および代入演算の結果は
 プラットフォームによって多少異なる場合があります』
と明記されています。
http://msdn.microsoft.com/ja-jp/library/vstudio/643eft0t.aspx

両者の違いを厳密に調べたいのであれば、そもそもの比較データである
double 値の内部表現が、完全に一致するバイナリになっているかどうかを
確認してみてください。.NET の場合は、BitConverter.GetBytes を
用いて、byte 配列へと変換できます。


>  アプリの表示は"26.709"となる。
>  なので、アプリ上の表示は"26.710"となる。

これはどのように表示させていますか?
たとえば sprintf や String.Format で書式指定した場合はどうですか?
■No31557に返信(Xn68000さんの記事)
> ・C#で同様にdouble型変数に"26.7095"を格納すると、内部表現も"26.7095"
>  なので、アプリ上の表示は"26.710"となる。

これ、デバッガ上の表示が内部表現と等しくない気がします。
試しに以下のようなコードを書いてみました(Visual Studio 2012 Professional を利用)。

  double d = 26.7095;
  System.Console.WriteLine(d);

で、IL を見てみたところ、

  IL_0001:  ldc.r8     26.709499999999998

となっていました。
次に、http://social.msdn.microsoft.com/Forums/ja-JP/csharpgeneralja/thread/d99df996-5e63-4c21-b369-8e062af1e0d7/
に書いてあった以下のコードで、上記 d の内部表現を書き出してみました。

  byte[] barr = System.BitConverter.GetBytes(d);
  string []sarr = ( from a in barr select a.ToString("X2") ).Reverse().ToArray();
  System.Console.WriteLine( string.Join("",sarr) );

すると、"403AB5A1CAC08312" という結果が得られました。
これは、C++ で double 型の変数に 26.7095 を代入したときと全く同じ内部表現です(26.70949… を表す)。

最後に、適当なブレークポイントで止めてデバッガの表示を見たところ、
変数 d の値が "26.7095" と表示されました。

以上より、

> ・C#で同様にdouble型変数に"26.7095"を格納すると、内部表現も"26.7095"なので、

というのは、正しくない気がします。
魔界の仮面弁士さん ありがととうございます。


■No31562に返信(魔界の仮面弁士さんの記事)
> ■No31557に返信(Xn68000さんの記事)
>>・VC++のdouble型変数に"26.7095"を格納し、
>>・C#で同様にdouble型変数に"26.7095"を格納すると、
>
> どのように格納したのか、にもよりますが、.7095 という値は、
> Double の二進小数桁数では表しきれないと思いますので、この時点で
> 近似値になってしまっていると思います。
>
C#/VC++ともに
double dTemp = 26.7095;
と代入しています。
「近似値になる」ということは、この場合VC++の方が正しくC#の方が
間違った表記になっている ということでしょうか。

> また、数値リテラルからの代入にしても、必ずしも同じ結果が
> 保証されるわけではありません。32bit/64bit の違いだけでも、
> リテラルの結果が異なることはありえますし、下記の資料においても
> 『Double 値を使用した算術演算および代入演算の結果は
>  プラットフォームによって多少異なる場合があります』
> と明記されています。
> http://msdn.microsoft.com/ja-jp/library/vstudio/643eft0t.aspx
>
この現象を確認するだけのサンプルコードをVS2010で作成し確認しているので
「プラットフフォームの違い」の点はクリアしていると思います。



> 両者の違いを厳密に調べたいのであれば、そもそもの比較データである
> double 値の内部表現が、完全に一致するバイナリになっているかどうかを
> 確認してみてください。.NET の場合は、BitConverter.GetBytes を
> 用いて、byte 配列へと変換できます。
>
>
BitConverterをVC++上で使用するやり方がわからないので調査中です。


>> アプリの表示は"26.709"となる。
>> なので、アプリ上の表示は"26.710"となる。
>
> これはどのように表示させていますか?
> たとえば sprintf や String.Format で書式指定した場合はどうですか?
・VC++ ::sprintf(fmt, "Value = %06.3lf\r\n", dTemp);
・C# string.Format("Value = {0:00.000}", dTemp);
としています。
2013/05/21(Tue) 17:33:15 編集(投稿者)

■No31559に返信(Xn68000さんの記事)
> また、本記事ではタイトルにもあります通り、
>  「C#とVC++でdouble型変数の内部表現が異なるのは何故か?」
> についてお聞きしたい というのが目的ですので、
> 精度を求めている訳ではありません。

再掲:
まずは提示された結果を得た過程が分からないので

内部表現については既出ですが異なるという根拠がないです。



■No31564に返信(Xn68000さんの記事)
>>これはどのように表示させていますか?
>>たとえば sprintf や String.Format で書式指定した場合はどうですか?
> ・VC++ ::sprintf(fmt, "Value = %06.3lf\r\n", dTemp);
> ・C# string.Format("Value = {0:00.000}", dTemp);
> としています。
処理が異なるので結果が変わってくる可能性は否めないのでは。
>>両者の違いを厳密に調べたいのであれば、そもそもの比較データである
>>double 値の内部表現が、完全に一致するバイナリになっているかどうかを
>>確認してみてください。.NET の場合は、BitConverter.GetBytes を
>>用いて、byte 配列へと変換できます。
>>
> >
> BitConverterをVC++上で使用するやり方がわからないので調査中です。
>
>
C#/VC++でともにBitConverter.GetBytes()でバイナリを調べたところ、
両者とも完全に一致しました。
(こ)さん ありがとうございます。

■No31563に返信((こ)さんの記事)
> ■No31557に返信(Xn68000さんの記事)
>>・C#で同様にdouble型変数に"26.7095"を格納すると、内部表現も"26.7095"
>> なので、アプリ上の表示は"26.710"となる。
>
> これ、デバッガ上の表示が内部表現と等しくない気がします。
↑での調査で、VC++とC#でバイナリは一致することを確認できましたので、
デバッガ上での表示が異なる というのがおかしい気がしますね。
これってC#の側の問題ということでしょうか。

うーん、なんだか藪をつっついて蛇を出してしまった気が・・・・
C ランタイムと、.NET とで丸めの実装が違うのかな?
本筋については、確かなことを言えません。

■No31566に返信(Xn68000さんの記事)
> C#/VC++でともにBitConverter.GetBytes()でバイナリを調べたところ、
> 両者とも完全に一致しました。

本筋ではありませんが、念のため。

もし、BitConveter クラスを使えるように C++/CLI にしたということなら、
考え方がちょっとまずいと思います。
理由は、C++ 側を C# と同じように動くようにコンパイルした(.NET 化した)わけですから。

ネイティブの C++ でバイト列を見たいのなら、
union なり、BYTE 型のポインタと見なして無理矢理アクセスしたりすればできます。
結果としては同じですが、検証方法を間違えると、意味がなくなってしまうのでご注意ください。

double dTemp = 26.7095;
unsigned char* p = reinterpret_cast<unsigned char*>(&dTemp);
size_t n = sizeof(double);
for (size_t i = 0; i < n; ++i)
{
// リトルエンディアンなので逆から
printf("%02X", p[n - i - 1]);
}
return 0;
2013/05/22(Wed) 11:06:43 編集(投稿者)

■No31567に返信(Xn68000さんの記事)
> (こ)さん ありがとうございます。
>
> ■No31563に返信((こ)さんの記事)
>>■No31557に返信(Xn68000さんの記事)
> >>・C#で同様にdouble型変数に"26.7095"を格納すると、内部表現も"26.7095"
> >> なので、アプリ上の表示は"26.710"となる。
>>
>>これ、デバッガ上の表示が内部表現と等しくない気がします。
> ↑での調査で、VC++とC#でバイナリは一致することを確認できましたので、
> デバッガ上での表示が異なる というのがおかしい気がしますね。
> これってC#の側の問題ということでしょうか。
問題でもなんでもない気がします。そういう表示をさせているというだけで
しょう。26.7095という値に対し"26.7095"が表示されるのが問題なはずが
ないです。有効桁数で切らないVC++の表示と有効桁数できちんと切って
表示しているC#の表示を比べることに問題があるのでは?
Azuleanさん ありがとうございます。

■No31569に返信(Azuleanさんの記事)
> C ランタイムと、.NET とで丸めの実装が違うのかな?
> 本筋については、確かなことを言えません。
>
> ■No31566に返信(Xn68000さんの記事)
>>C#/VC++でともにBitConverter.GetBytes()でバイナリを調べたところ、
>>両者とも完全に一致しました。
>
> 本筋ではありませんが、念のため。
>
> もし、BitConveter クラスを使えるように C++/CLI にしたということなら、
> 考え方がちょっとまずいと思います。
> 理由は、C++ 側を C# と同じように動くようにコンパイルした(.NET 化した)わけですから。
>
確かにそうですね。安易に考えていました。


> ネイティブの C++ でバイト列を見たいのなら、
> union なり、BYTE 型のポインタと見なして無理矢理アクセスしたりすればできます。
> 結果としては同じですが、検証方法を間違えると、意味がなくなってしまうのでご注意ください。
>
> double dTemp = 26.7095;
> unsigned char* p = reinterpret_cast<unsigned char*>(&dTemp);
> size_t n = sizeof(double);
> for (size_t i = 0; i < n; ++i)
> {
> // リトルエンディアンなので逆から
> printf("%02X", p[n - i - 1]);
> }
> return 0;

この方法でも、両者のバイナリは一致することを確認できました。

うーん・・・そうなると一体何か原因なのか・・・
クライアントを納得させられる説明が考えつきません。
困ったな。
shuさん ありがとうごさいます。

■No31572に返信(shuさんの記事)
> 2013/05/22(Wed) 11:06:43 編集(投稿者)
>
> ■No31567に返信(Xn68000さんの記事)
>>(こ)さん ありがとうございます。
>>
>>■No31563に返信((こ)さんの記事)
> >>■No31557に返信(Xn68000さんの記事)
>>>>・C#で同様にdouble型変数に"26.7095"を格納すると、内部表現も"26.7095"
>>>> なので、アプリ上の表示は"26.710"となる。
> >>
> >>これ、デバッガ上の表示が内部表現と等しくない気がします。
>>↑での調査で、VC++とC#でバイナリは一致することを確認できましたので、
>>デバッガ上での表示が異なる というのがおかしい気がしますね。
>>これってC#の側の問題ということでしょうか。
> 問題でもなんでもない気がします。そういう表示をさせているというだけで
> しょう。26.7095という値に対し"26.7095"が表示されるのが問題なはずが
> ないです。有効桁数で切らないVC++の表示と有効桁数できちんと切って
> 表示しているC#の表示を比べることに問題があるのでは?
>
デバッガの表示上だけの問題なら良いのですが、プログラム中の
sprintf()/string.Format()での「文字列化」の部分で、小数点以下4桁目での
丸め方に違いが出てくる以上、内部の「数値」としての扱いのレベルで
違いがあるのではないか と思います。

まぁ、違いといっても小数点以下10数桁レベルの話ですので
今回移植しているアプリに関しては、目くじらたてる必要はないかな?と
思ってますが。
まだこのスレ、続きそうですが、当方に別件の業務が入り、
本件のフォローができなくなってしまいましたので、
手前勝手で申し訳ありませんが、これでクローズとさせて
いただきます。

フォローいただいたみなさん、ありがとうございました。
解決済み!
■No31576に返信(Xn68000さんの記事)

> デバッガの表示上だけの問題なら良いのですが、プログラム中の
> sprintf()/string.Format()での「文字列化」の部分で、小数点以下4桁目での
> 丸め方に違いが出てくる以上、内部の「数値」としての扱いのレベルで
> 違いがあるのではないか と思います。

ここにきて内部の数値というのが何を表しているか分かりませんが、
既にレスしたように
sprintf()

string.Format()
の処理が同じではないという事です。
言語上の問題点ではありません。

ユーザーへの説明だけなら言語の仕様により丸め方が変更になっています
と言えばいいだけではないでしょうか?それを前と同じにしてくれというので
あればそういう計算を加味した処理にする必要があります。
■No31577に返信(Xn68000さんの記事)
> まだこのスレ、続きそうですが、当方に別件の業務が入り、
> 本件のフォローができなくなってしまいましたので、
> 手前勝手で申し訳ありませんが、これでクローズとさせて
> いただきます。

未解決の問題を「解決済み」にしないようお願いいたします。
今一度、掲示板の書き込みルールを再読ください。
http://dobon.net/vb/bbs/index.html

一応、本投稿では解決済みチェックを解除しておきますね。


■No31564に返信(Xn68000さんの記事)
>> これはどのように表示させていますか?
>> たとえば sprintf や String.Format で書式指定した場合はどうですか?
> ・VC++ ::sprintf(fmt, "Value = %06.3lf\r\n", dTemp);
> ・C# string.Format("Value = {0:00.000}", dTemp);
上記の逆質問の意図は、他の書式や、別の変換方法でも
テストしてみてください…ということだったのですけれどね。(^^;

質問がクローズされてしまってはいますが、参考までに、
当方の C# で書式を変更して出力した結果を掲載しておきます。

string s0 = dbl.ToString("R");    // "26.7095"                (ラウンドトリップ)
string s1 = dbl.ToString("F");    // "26.71"      (固定小数、桁数はカルチャ依存)
string s2 = dbl.ToString("E");    // "2.670950E+001"   (指数表現、桁数は既定の6)
string s3 = dbl.ToString("E15");  // "2.670950000000000E+001"   (指数表現、15桁)
string s4 = dbl.ToString("E16");  // "2.6709499999999998E+001"  (指数表現、16桁)
string s5 = dbl.ToString("E17");  // "2.67094999999999980E+001" (指数表現、17桁)
string s6 = dbl.ToString("E18");  // "2.670949999999999800E+001"(指数表現、18桁)



■No31576に返信(Xn68000さんの記事)
> 小数点以下4桁目での丸め方に違いが出てくる以上、
> 内部の「数値」としての扱いのレベルで
> 違いがあるのではないか と思います。

内部バイナリに差がなかった以上、その数値を文字列化する段階で、
桁数や丸め方法としての差が生じているのでしょうね。
(この「文字列化」というのは、デバッガ上の表示の違いも含まれます)

そもそも浮動小数点数というものは、概数を表すためのものなので、
それをどの近似値として「表示」するかで、差が生じることがあります。
それが
>>>>> "26.7094999...."
>>>>> "26.709"
>>>>> "26.710"
の違いとなって表れているのでしょう。


今回問題視している「0x403ab5a1cac08312」のバイナリというのは、
 26.70949999999999846522769075818359851837158203125
という値を意味しています。

浮動小数の内部表現仕様は、IEEE 754 や下記をご覧ください。
http://www.cc.kyoto-su.ac.jp/~yamada/programming/float.html
http://ja.wikipedia.org/wiki/%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0


この 0x403ab5a1cac08312 という値は、64桁の2進で表すと

0100 0000 0011 1010  1011 0101 1010 0001
1100 1010 1100 0000  1000 0011 0001 0010

になります。
そして、double型は「符号部1ビット、指数部11ビット、仮数部52ビット」なので

符号部 = 0
指数部 = 10000000011
仮数部 = 1010101101011010000111001010110000001000001100010010

となり、実際に格納されている値は
 二進小数 0b11010.101101011010000111001010110000001000001100010010
ということになるわけです。


このバイナリを、double 型で表せる前後の値と比較してみましょう。

(A) 11010.101101011010000111001010110000001000001100010000
(B) 11010.101101011010000111001010110000001000001100010001
(C) 11010.101101011010000111001010110000001000001100010010  ※格納値
(D) 11010.101101011010000111001010110000001000001100010011
(E) 11010.101101011010000111001010110000001000001100010100


上記 2 進小数群を 10 進小数に変換してみると

(A) 26.709499999999991359800333157181739807128906250000
(B) 26.709499999999994912514011957682669162750244140625
(C) 26.709499999999998465227690758183598518371582031250  ※格納値
(D) 26.709500000000002017941369558684527873992919921875
(E) 26.709500000000005570655048359185457229614257812500

に相当する値ということになります。今回の期待値は 26.7095 だったわけですが、
C は期待値よりも 0.0000000000000015347723092418…… ほど小さく、
D は期待値よりも 0.0000000000000020179413695586…… ほど大きい値ですね。


ただし、上記はあくまで内部表現上の値であり、
既定の有効桁数は上記の半分以下の精度です。そのため、

(a) 26.709499999999991
(b) 26.709499999999995
(c) 26.709499999999998
(d) 26.709500000000002
(e) 26.709500000000006

などとして表示されることになるようです。

この部分について、Visual C#/C++ 2012 での表示結果は下図のようになりました。
http://www.vb-user.net/junk/replySamples/2013.05.23.16.43/Double.png

上図を見ると、C# の dbl3 では、期待値 26.7095 が格納されているかのように
見えていますが、これは格納誤差が解消されているというわけではなく、
Round-Trip 表現が採用されているのであろうと予想しています。

すなわち、本投稿の冒頭に書いた
≫ string s0 = dbl.ToString("R");    // "26.7095"                (ラウンドトリップ)
の表現であるということです。利用者にとっての期待値に近い結果で
出力させるために、少々ややこしい変換ルールになっています。
http://msdn.microsoft.com/ja-jp/library/dwhawy9k.aspx#RFormatString

なお .NET においては、
『既定では、Double 値に含まれる有効桁数は 15 桁ですが、
 内部的には最大 17 桁が保持されています』
とのことです。(VC++ は…専門外なので調べていません)
http://msdn.microsoft.com/ja-jp/library/system.double.aspx
■No31578に返信(shuさんの記事)
> ■No31576に返信(Xn68000さんの記事)
>
>>デバッガの表示上だけの問題なら良いのですが、プログラム中の
>>sprintf()/string.Format()での「文字列化」の部分で、小数点以下4桁目での
>>丸め方に違いが出てくる以上、内部の「数値」としての扱いのレベルで
>>違いがあるのではないか と思います。
>
> ここにきて内部の数値というのが何を表しているか分かりませんが、
> 既にレスしたように
> sprintf()
> と
> string.Format()
> の処理が同じではないという事です。
> 言語上の問題点ではありません。
>
> ユーザーへの説明だけなら言語の仕様により丸め方が変更になっています
> と言えばいいだけではないでしょうか?それを前と同じにしてくれというので
> あればそういう計算を加味した処理にする必要があります。

今回問題視している部分ですが、1/1000の値の違いが他に即影響を
及ぼすものにはならないようですし、この誤差が蓄積されていく
ものではないことを確認できましたので、shuさんの仰るように
「丸め方が異なる」で何とか通ると思います。

ありがとうございました。
魔界の仮面弁士さん、丁寧に解説いただきありがとうございます。

つまり、数値としての内部で扱われているものはVC++で表示されていた
ものではあるが、C#ではデバッガや、Format()などの「文字列化」する
部分で「ラウンドトリップ表現(初めて聞きました。私が使用している
C#の参考書にも出ていなかったので)」が使われ、あたかも変数に
"26.7095"が代入されているかのようにふるまう ということですね。

クライアントにこのまま話すとかえって混乱しそうなので
やめておきますが、友人に何人かC#のプログラマがあるので
彼らとの間で情報共有しておきます。



■No31580に返信(魔界の仮面弁士さんの記事)
> ■No31577に返信(Xn68000さんの記事)
>>まだこのスレ、続きそうですが、当方に別件の業務が入り、
>>本件のフォローができなくなってしまいましたので、
>>手前勝手で申し訳ありませんが、これでクローズとさせて
>>いただきます。
>
> 未解決の問題を「解決済み」にしないようお願いいたします。
> 今一度、掲示板の書き込みルールを再読ください。
> http://dobon.net/vb/bbs/index.html
>
> 一応、本投稿では解決済みチェックを解除しておきますね。
>
折角返信いただいても、チェックできずに放置することになると
失礼になるな と思って「クローズ」にしました。
こちらの掲示板の「ルール」もわからなくはないのですが・・

本件、これで「クローズ」とします。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板