Help us understand the problem. What is going on with this article?

[C#] decimal型はどうやって加減算をしているのか

More than 1 year has passed since last update.

こんにちは、Niaです。
今回は.NET Frameworkの実数型の1つ、decimal型(Decimal型)の加減算の様子を見てみました。

1. decimal型とは

decimal型は1ビットの符号部7ビットの指数部96ビットの仮数部で構成された、10進数表現の浮動小数点値です。但し、仮数部は整数として表しており、指数部で小数点の位置を指定します。

fd1-3.PNG

decimal型の値 = $(-1)^{[符号部]} \times [仮数部] \times 10^{-[指数部]}$

参考 : Decimal 構造体 | MSDN
https://msdn.microsoft.com/ja-jp/library/system.decimal.aspx

2. decimal型同士の加減算の様子

実際にdecimal型同士の加算の様子を見てみましょう。

decimal-add.cs
using System;
using System.Linq;

class Program {
    static void Main( string[] args ) {

        decimal d1, d2, d3;
        d1 = 1024m;
        d2 = 0.2048m;
        d3 = d1 + d2;
        DecimalUnit d1Unit = new DecimalUnit( d1 ),
            d2Unit = new DecimalUnit( d2 ),
            d3Unit = new DecimalUnit( d3 );

        Console.WriteLine( "d1 : " );
        d1Unit.ViewInfo();
        Console.WriteLine( "d2 : " );
        d2Unit.ViewInfo();
        Console.WriteLine( "d3 = d1 + d2 : " );
        d3Unit.ViewInfo();

    }
}

// decimal型の値から符号部、指数部、仮数部の値を求めます。
struct DecimalUnit {
    private decimal org;    // 元の値
    public bool Sign;       // 指数部
    public int Scale;       // 指数部
    public int[] Part;      // 仮数部

    public DecimalUnit( decimal d ) {
        org = d;
        // decimal型の内部表現の値はGetBitsメソッドで取得します。
        int[] ds = decimal.GetBits( d );
        // 符号部は4番目の要素の最上位1ビットです
        Sign = ds[3] >> 31 != 0;
        // 指数部は4番目の要素の下位17~24ビット目( 7ビット分 )です。
        Scale = ( ds[3] >> 16 ) & 0x7F;
        // 1~3番目の要素は仮数部( 96ビット整数 )です。
        Part = ds.Take( 3 ).ToArray();
    }

    // 情報をコンソール画面に出力します。
    public void ViewInfo() {
        Console.WriteLine( "値 : {0:G}", org );
        Console.WriteLine( "バイナリ値 : {0}", string.Join( " | ", decimal.GetBits( org ).Reverse().Select( b => string.Join( " ", BitConverter.GetBytes( b ).Reverse().Select( b2 => string.Format( "{0:X2}", b2 ) ) ) ) ) );
        Console.WriteLine( "符号部 : {0}", Sign );
        Console.WriteLine( "指数部 : {0}", Scale );
        // decimal型はint型やlong型など整数型の上位互換なので、
        // 指数部を0にすれば、仮数部の値を求めることができます。
        Console.WriteLine( "仮数部 : {0:F0}", new Decimal( Part[0], Part[1], Part[2], false, 0 ) );
        Console.WriteLine();
    }
}

実行結果
d1 :
値 : 1024
バイナリ値 : 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 04 00
符号部 : False
指数部 : 0
仮数部 : 1024

d2 :
値 : 0.2048
バイナリ値 : 00 04 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 08 00
符号部 : False
指数部 : 4
仮数部 : 2048

d3 = d1 + d2 :
値 : 1024.2048
バイナリ値 : 00 04 00 00 | 00 00 00 00 | 00 00 00 00 | 00 9C 48 00
符号部 : False
指数部 : 4
仮数部 : 10242048

指数部の値から d1(仮数部 = 1024, 指数部 = 0)と d2( 仮数部= 2048, 指数部 = 4)の加算の場合、d1の指数部を 4 にして仮数部に $10^4$ を掛けてから、仮数部同士で加算していることがわかります(※10240000の16進数は 0x9C4000 です)。

つまり、decimal型同士の加減算は以下のように行われます。

  1. 指数部を比較し、指数部の小さい実数の仮数部を指数部の大きい実数の指数部(すなわち小数部の精度が大きい方)に合わせて指数部の値を増やし、その分だけ10のべき乗を掛けます
  2. 仮数部同士で加減算をします。

3. decimal型での情報落ち

もし、decimal型で極端に大きな値と極端に小さな値の加算を行ったらどうなるのでしょう。

先ほどのプログラム「decimal-add.cs」にて、d1とd2を以下の値に設定し、実行してみましょう。

decimal-add.cs内
// 仮数部の上位32ビットを「0xFFFF / 100」とし、指数部と符号部を0とします。
d1 = new decimal( 0, 0, int.MaxValue / 100, false, 0 );
// 指数部が10になるように、小数点以下10桁の値を代入します。
d2 = 0.0123456789m;

実行結果
d1 :
値 : 396140803716884532587134976
バイナリ値 : 00 00 00 00 | 01 47 AE 14 | 00 00 00 00 | 00 00 00 00
符号部 : False
指数部 : 0
整数部 : 396140803716884532587134976

d2 :
値 : 0.0123456789
バイナリ値 : 00 0A 00 00 | 00 00 00 00 | 00 00 00 00 | 07 5B CD 15
符号部 : False
指数部 : 10
整数部 : 123456789

d3 = d1 + d2 :
値 : 396140803716884532587134976.01
バイナリ値 : 00 02 00 00 | 7F FF FF D0 | 00 00 00 00 | 00 00 00 01
符号部 : False
指数部 : 2
整数部 : 39614080371688453258713497601

d1の指数部が0、d2の指数部が10なので、それらの和であるd3の指数部は10になると思いきや、この結果では2になりました。

実はdecimal型で指数部に合わせて仮数部を調節する時、仮数部が表現可能な値の範囲を超えてしまう(オーバーフローする)場合、ギリギリ超えないところまで10のべき乗をかけ、それに合わせて指数部の値を調整します。その後、もう一方の指数部を先ほどの指数部に合わせて減らし、その分だけ10のべき乗で割ります。

例えば、先ほどのd1、d2の場合、d1の指数部が0、d2の指数部が10なので、d1の仮数部に $10^{10}$ を掛けますが、値の範囲を超えない最大の10のべき乗は $10^{2}$ なので、d1の指数部を2だけ増やし、仮数部に $10^{2}$ を掛けます。d2の指数部は調整したd1の指数部に合わせて2に設定し、それに合わせて仮数部を $10^8$ で割ります。これによって、d2の小数点以下第3位以降は切り捨てられてしまいます。

表現可能な値の範囲に気を付けてプログラミングしていきましょう。

それでは、See you next time!

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away