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

変数に格納した計算結果とイミディエイトウィンドウでの計算結果が異なる

環境/言語:[Windows7 Pro SP1 x64, VB.NET(2012 Express)]
分類:[.NET]

いつもお世話になります。
表題の件について、奇妙な現象が発生しているのでお知恵を拝借したく。

小数点以下を丸める処理を行っているのですが、
変数に格納するときと、イミディエイトウィンドウで計算結果を表示するので
結果が異なってしまっています。
対処方法をどなたか教えていただけると幸いです。

_purchase_amount = 279350
_discount_rate = 0.03

・変数に格納した場合
Dim d as Double = Math.Round(_purchase_amount * _discount_rate, 0, MidpointRounding.AwayFromZero)

変数 d の中身は、8380.0

・イミディエイトウィンドウに結果表示した場合
?Math.Round(_purchase_amount * _discount_rate, 0, MidpointRounding.AwayFromZero)

結果は 8381.0

8381.0 という結果が欲しいのですが、何が原因で値が変わってしまっているのかがわかりません…
■No31715に返信(やむさんの記事)
> 対処方法をどなたか教えていただけると幸いです。
> _purchase_amount = 279350
> _discount_rate = 0.03
それぞれの変数を As Decimal にしておきましょう。


> 奇妙な現象が発生しているのでお知恵を拝借したく。
手元の環境で現象が再現しないので、変数の型や変数値の代入部分までも
含めた「現象を再現できるコード」を提示していただけないでしょうか。


> 変数 d の中身は、8380.0
これはどのようにして確認されましたか?
また、「Dim bin = BitConverter.GetBytes(d)」とした場合、
配列変数 bin の中身は何になってますか?


とりあえず関連情報として。
http://dobon.net/cgi-bin/vbbbs/cbbs.cgi?mode=all&namber=31557&type=0&space=0&no=0
>>魔界の仮面弁士様
_purchase_amount、_discount_rate それぞれの変数をDecimal型にすることで、
望んだ結果を得ることができました。
ありがとうございました。

なぜかというのは、要するに
http://dobon.net/vb/dotnet/beginner/floatingpointerror.html
うえのURLに記載されている通りだとの認識で間違いないでしょうか。
というか、なぜ先にこれを調べなかったのかと、2時間ほど正座していたい気分です…orz
解決済み!
ちなみに。
変数dの中身の確認方法は、デバッグ実行時にウォッチにて確認してました。
■No31720に返信(やむさんの記事)
> 変数dの中身の確認方法は、デバッグ実行時にウォッチにて確認してました。

Integer や Decimal であればそれでも十分ですが、
Single や Double は「概数」を扱うための物なので、
誤差に対するチェックの際には、ウォッチ式だけでは
不十分となることがあります。


> 望んだ結果を得ることができました。
ありゃ。ということは、現象を再現できるコードは提示して頂けないのでしょうか…。
>>魔界の仮面弁士様

> ありゃ。ということは、現象を再現できるコードは提示して頂けないのでしょうか…。

現象が再現される最小構成のものを作成したので、提示します。

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim cls As New Discount
        cls.purchase_amount = 279350
        cls.discount_rate = 0.03
        cls.rounding_type = Discount.ROUNDING_TYPE_NONE

        TextBox1.Text = cls.discount_amount
        'TextBox1.Text の中身は、【8380】となる。
    End Sub
End Class

Public Class Discount
    Public Const ROUNDING_TYPE_NONE As String = "0"
    Public Const ROUNDING_TYPE_TRUNCATION As String = "1"
    Public Const ROUNDING_TYPE_CONCLUSION As String = "2"
    Public Const ROUNDINF_TYPE_ROUNDED As String = "3"

    Public Property purchase_amount As Long
    Public Property discount_rate As Single
    Public Property rounding_type As String
    Public ReadOnly Property discount_amount As Integer
        Get
            Return getDiscountAmount()
        End Get
    End Property

    Private Function getDiscountAmount() As Integer
        Dim d As Double = Math.Round(_purchase_amount * _discount_rate, MidpointRounding.AwayFromZero)
        Dim bin = BitConverter.GetBytes(d)
        'binの中身
        '(0):0
        '(1):0
        '(2):0
        '(3):0
        '(4):0
        '(5):94
        '(6):192
        '(7):64

        If _rounding_type = ROUNDING_TYPE_NONE Then
            Return CInt(d)
        ElseIf _rounding_type = ROUNDING_TYPE_TRUNCATION Then
            '1の位切り捨て
            Return CInt(Math.Truncate(d / 10) * 10)
        ElseIf _rounding_type = ROUNDING_TYPE_CONCLUSION Then
            '1の位切り上げ
            Return CInt(Math.Ceiling(d / 10) * 10)
        Else
            '1の位四捨五入
            Return CInt(Math.Round(d / 10, 0, MidpointRounding.AwayFromZero) * 10)
        End If
    End Function
End Class

SingleとDoubleは概数だから…というのは理解しましたが、
小数第一位が0の数値の、1の位までまるまってしまうのは、どうにも納得しがたいものが…
■No31722に返信(やむさんの記事)

イミディエイトウィンドウで
?Math.Round(_purchase_amount * CDbl(_discount_rate), MidpointRounding.AwayFromZero)

とすると同じ結果が得られるかと思います。
イミディエイトウィンドウの計算がSingleのまま計算されるのに対し
実際の処理ではMath.RoundはSingleを要求するものがないためDoubleを
要求してしまうため上記の計算が行われていると考えられます。

なので希望する処理を行いたければ
Dim s as Single = _purchase_amount * _discount_rate
Dim d as Double = Math.Round(s, MidpointRounding.AwayFromZero)
とすればよいです。

ただこういう問題をさけるにはSingle,Doubleを使わないで十進型のDecimalを
使われた方がよいかと思います。Single,Doubleはそんな事を気にしないだいたいの
数値がわかれば良い時だけにしておいた方がよいです。
■No31722に返信(やむさんの記事)
> SingleとDoubleは概数だから…というのは理解しましたが、

Single や Double は 2 進小数であるため、.5 な値は正確に表現できます。
そのため、今回の演算結果である「8380.5」は正確に表現できますし、
それによる Math.Round の結果に差異はありません。


では、どこで今回のような差が生じたのかといえば、それは演算結果では無く、
演算途中の Single と Double の精度の違いにあります。


> 小数第一位が0の数値の、1の位までまるまってしまうのは、どうにも納得しがたいものが…
気にするべき場所はそこではありません。

バイナリ表現を踏まえた上で、もう少し具体的に説明してみましょうか。
少し長くなりますが御付き合い下さい。


> _discount_rate = 0.03
そもそも浮動小数点型は、データを2進小数で管理しています。

10÷3 の結果を、10進小数で表現するのが難しいように(3進小数なら問題なし)、
3÷10 の結果を、2進小数で表現することも難しいのです(10進小数なら問題なし)。


なので、0.3 や 0.03 といった値を正確に保持したいのであれば、
2進小数で管理される Single や Double ではなく、
10進にて管理される Decimal を使うべきである、というのが大原則です。


> TextBox1.Text = cls.discount_amount
左辺が String 型、右辺が Integer 型になってしまっていますよね。
普段から『データ型』を意識した方が良いですよ。まぁ、それはさておき。



> Dim d As Double = Math.Round(_purchase_amount * _discount_rate, MidpointRounding.AwayFromZero)
さて本題。
まず上記の「_purchase_amount * _discount_rate」ですが、VB の仕様上、
「Integer * Single」の演算結果は「Single」となります。

しかし、Math.Round は Single を受け付けないため、ここで「Double」に
変換されてから処理され、ここで問題が生じているわけです。
(既に shu さんが解説して下さっていますね)

ちなみに、「Integer * Double」や「Double * Single」の演算結果は「Double」です。



ということで、ここで Single と Double の差を見てみましょう。


たとえば、イミディエイトにて、型を明示した下記の演算を行ってみてください。
(末尾の R や # は Double 型を表し、末尾の F や ! は Single 型を意味します)

? 0.03R - 0.03F
? 0.03# - 0.03!

その答えはゼロとはならず、「0.00000000067055225261292151」という微細値で表現されます。
VB では、「Double - Single」は「Double」となる仕様の為、Single→Double変換が生じるためです。


具体的には、下記に相当する処理が途中で発生しているというワケです。
? CDbl(CSng("0.03"))
? CDbl(0.03F)
これらの結果は「0.029999999329447746」となります。



「? 279350F * 0.03F」や「? 279350R * 0.03R」は「8380.5」ですが、
「? 279350R * 0.03F」だと「8380.4998126812279」になってしまいます。

この .5000 と .4998 の違いが、五捨五入(ROUNDING_TYPE_NONE)や
四捨五入(ROUNDINF_TYPE_ROUNDED)の結果に影響を与えています。



では、そもそも何故、CDbl(0.03F) は 0.03R にならないのかというと、
それは浮動小数点数が「概数」であるため、値の間隔が異なっているからです。
…と言っても何のことか分からないと思いますので、具体例を伴って示してみます。


まず、0.03 を Single 型で扱った場合の内部バイナリを見てみると、これは
&H3CF5C28Fui となります。一緒に、その前後値も掲載しておきます。★の箇所が 0.03 のバイナリです。

 &H3CF5C26Fui → 2進小数 0.00000111101011100001010001101 = 10進小数 0.02999999560415744781494140625 ≒ 0.02999995
 &H3CF5C27Fui → 2進小数 0.00000111101011100001010001110 = 10進小数 0.02999999746680259704589843750 ≒ 0.02999997
★&H3CF5C28Fui → 2進小数 0.00000111101011100001010001111 = 10進小数 0.02999999932944774627685546875 ≒ 0.03000000
 &H3CF5C29Fui → 2進小数 0.00000111101011100001010010000 = 10進小数 0.03000000119209289550781250000 ≒ 0.03000003
 &H3CF5C2AFui → 2進小数 0.00000111101011100001010010001 = 10進小数 0.03000000305473804473876953125 ≒ 0.03000005

それぞれ、約 0.00000002 ほどの間隔がありますね。

有効桁数の関係上、この間隔は 0 に近い値ほど細かくなり、0 から遠ざかるほど荒くなります。
また、Single よりも Double の方が、より細かい間隔で表現できることになります。


このことが、先のような Single → Double 変換への問題を生じます。

先の実験で、CDbl(0.03F) の結果はイミディエイトでは「0.029999999329447746」でしたので、
0.03R《★マーク》付近のバイナリと、CDbl(0.03F)《☆マーク》付近のバイナリを、それぞれ見ていきましょう。


 &H3F9EB851DFFFFFFEuL → 2進小数 0.0000011110101110000101000111011111111111111111111111111110
                      = 10進小数 0.0299999993294477393379615648427716223523020744323730468750
                      ≒ 0.029999999329447739
 &H3F9EB851DFFFFFFFuL → 2進小数 0.0000011110101110000101000111011111111111111111111111111111
                      = 10進小数 0.0299999993294477428074085167963858111761510372161865234375
                      ≒ 0.029999999329447743
☆&H3F9EB851E0000000uL → 2進小数 0.0000011110101110000101000111100000000000000000000000000000
                      = 10進小数 0.0299999993294477462768554687500000000000000000000000000000
                      ≒ 0.029999999329447746
 &H3F9EB851E0000001uL → 2進小数 0.0000011110101110000101000111100000000000000000000000000001
                      = 10進小数 0.0299999993294477497463024207036141888238489627838134765625
                      ≒ 0.02999999932944775
 &H3F9EB851E0000002uL → 2進小数 0.0000011110101110000101000111100000000000000000000000000010
                      = 10進小数 0.0299999993294477532157493726572283776476979255676269531250
                      ≒ 0.029999999329447753
--------------------

 &H3F9EB851EB851EB6uL → 2進小数 0.0000011110101110000101000111101011100001010001111010110110
                       = 10進小数 0.0299999999999999919508830714676150819286704063415527343750
                       ≒ 0.029999999999999992
 &H3F9EB851EB851EB7uL → 2進小数 0.0000011110101110000101000111101011100001010001111010110111
                      = 10進小数 0.0299999999999999954203300234212292707525193691253662109375
                      ≒ 0.029999999999999995
★&H3F9EB851EB851EB8uL → 2進小数 0.0000011110101110000101000111101011100001010001111010111000
                      = 10進小数 0.0299999999999999988897769753748434595763683319091796875000
                      ≒ 0.03
 &H3F9EB851EB851EB9uL → 2進小数 0.0000011110101110000101000111101011100001010001111010111001
                      = 10進小数 0.0300000000000000023592239273284576484002172946929931640625
                      ≒ 0.030000000000000002
 &H3F9EB851EB851EBAuL → 2進小数 0.0000011110101110000101000111101011100001010001111010111010
                      = 10進小数 0.0300000000000000058286708792820718372240662574768066406250
                      ≒ 0.030000000000000006


つまり、CDbl(0.03F) というのは、CDbl("0.03") に相当する変換では無く、
CDbl("0.0299999993294477463") 相当の変換が生じているということです。
■No31725に返信(魔界の仮面弁士さんの記事)
> ★&H3CF5C28Fui → 2進小数 0.00000111101011100001010001111 = 10進小数 0.02999999932944774627685546875 ≒ 0.03000000

ここの 『0.02999999932944774627685546875』がDoubleに変換され
有効桁数が増えるので
0.03
ではなく
0.029999999329447746
がDoubleの値として使われているのではないでしょうか?
■No31726に返信(shuさんの記事)
>>★&H3CF5C28Fui → 2進小数 0.00000111101011100001010001111 = 10進小数 0.02999999932944774627685546875 ≒ 0.03000000
> ここの 『0.02999999932944774627685546875』がDoubleに変換され
> 有効桁数が増えるので
> 0.03
> ではなく
> 0.029999999329447746
> がDoubleの値として使われているのではないでしょうか?

VB.NET だけではなく、VBA でも同様でした。蛇足までに。


' ---- VBA ----
? CDbl(CSng("0.03"))
 2.99999993294477E-02 

? CDbl(CSng("0.3"))
 0.300000011920929 


一応、数値リテラルで指定した場合と、変数から指定した場合について。


Module Module1
  Sub Main()
    Dim f1 As Single = 0.03F      ' ldc.r4 0.03
    Dim f2 As Single = 0.03R      ' ldc.r4 0.03
    Dim f3 As Single = 0.03D      ' ldc.r4 0.03

    Dim r1 As Double = 0.03F      ' ldc.r8 0.029999999329447746
    Dim r2 As Double = 0.03R      ' ldc.r8 0.03
    Dim r3 As Double = 0.03D      ' ldc.r8 0.03

    Dim d1 As Decimal = 0.03F     ' New Decimal(&H31086e8d, &H110d9, 0, 0, 16)
    Dim d2 As Decimal = 0.03R     ' New Decimal(3, 0, 0, 0, 2)
    Dim d3 As Decimal = 0.03D     ' New Decimal(3, 0, 0, 0, 2)

    Console.WriteLine("数値リテラルからの直接指定")
    Console.WriteLine("Float  : {0}, {1}, {2}", f1, f2, f3)
    Console.WriteLine("Real   : {0}, {1}, {2}", r1, r2, r3)
    Console.WriteLine("Decimal: {0}, {1}, {2}", d1, d2, d3)

    Dim r4 As Double = f1         ' conv.r8
    Dim r5 As Double = f2         ' conv.r8
    Dim r6 As Double = f3         ' conv.r8

    Dim d4 As Decimal = f1        ' New Decimal( Single )
    Dim d5 As Decimal = f2        ' New Decimal( Single )
    Dim d6 As Decimal = f3        ' New Decimal( Single )

    Console.WriteLine("Single 変数からの拡大変換")
    Console.WriteLine("Real   : {0}, {1}, {2}", r4, r5, r6)
    Console.WriteLine("Decimal: {0}, {1}, {2}", d4, d5, d6)

    Console.ReadLine()
  End Sub
End Module

'-----------------
数値リテラルからの直接指定
Float  : 0.03, 0.03, 0.03
Real   : 0.0299999993294477, 0.03, 0.03
Decimal: 0.0299999993294477, 0.03, 0.03
Single 変数からの拡大変換
Real   : 0.0299999993294477, 0.0299999993294477, 0.0299999993294477
Decimal: 0.03, 0.03, 0.03
>>shu様、魔界の仮面弁士様

細かい解説までしていただき、ありがとうございます。
また、返事が遅れたこと、ご容赦ください。

細かい精度が求められる計算には、decimal型を使うことにします。
解決済み!

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