LoginSignup
271
141

decimal型(十進小数)に夢を見ている輩が多すぎる

Last updated at Posted at 2024-06-08

よく二進浮動小数点数の問題として、

> 0.1 + 0.1 + 0.1
0.30000000000000004
> 0.1 + 0.1 + 0.1 === 0.3
false

だとか

> 4.8 - 4.7 - 0.1
-3.608224830031759e-16
> 4.8 - 4.7 - 0.1 === 0.0
false

みたいなのが挙げられます。これが話題になった時にSNSで見かける言説が「十進小数 (decimal) 型ならこういう問題はない」です。

ですが、decimal型は十進小数を正確に表現できるという話でしかなく、全ての実数を正確に表現できるわけではありません。例えば、 1.0 / 3.0 * 3.0 の計算を考えてみましょう。数学的には、これはちょうど 1.0 になるはずです。

C#の場合

C#には標準の decimal 型があります。これで 1.0 / 3.0 * 3.0 を計算してみましょう。

decimal a = 1.0m;
decimal b = 3.0m;
decimal c = a / b * b;
Console.WriteLine("{0} {1} {2}", a, b, c);

実行結果:

1.0 3.0 0.9999999999999999999999999999

1.0 にはなりませんでした。

C#の decimal は何者なのかというと、整数の仮数部 $M$ と指数部 $e$ について

$$
M\times 10^e
$$

と表現できる実数のことです。$M$ は絶対値が $2^{96}-1=79228162514264337593543950335$ 以下の整数で、$e$ は $-28$ 以上 $0$ 以下の整数です。

decimal が表現できる桁数は28桁程度と固定です。この点は、後述するJavaやPythonとの大きな違いでしょう。任意精度ではないのです。

また、通常の double と比べると、decimal 型は表現できる値の範囲が狭いdouble は $10^{308}$ のような巨大な数を表現できるが、decimal は $10^{28}$ 程度が限界)ことに注意してください。科学計算には不向きでしょう。

参照:

Javaの場合

Javaには BigDecimal 型があります。これで 1.0 / 3.0 * 3.0 を計算してみましょう。

jshell> new BigDecimal("1.0").divide(new BigDecimal("3.0")).multiply(new BigDecimal("3.0"))
|  例外java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
|        at BigDecimal.divide (BigDecimal.java:1736)
|        at (#1:1)

エラーが出ました。不正確な結果を返すくらいならエラーにしてしまえ、ということですかね。嫌いではないです。

演算時に MathContext 等の引数を適宜指定すると、指定した精度で計算してくれます。

jshell> new BigDecimal("1.0").divide(new BigDecimal("3.0"), MathContext.DECIMAL128).multiply(new BigDecimal("3.0"))
$2 ==> 0.99999999999999999999999999999999990

Javaの BigDecimal は何者なのかというと、任意精度の十進小数です。つまり、整数の仮数部 $M$ と指数部 $e$ について

$$
M\times 10^e
$$

と表現できる実数のことです。$M$ は多倍長整数で、$e$ は32ビット整数です。

参照:

Pythonの場合

Pythonにも Decimal クラスがあります。これで 1.0 / 3.0 * 3.0 を計算してみましょう。

>>> from decimal import Decimal
>>> Decimal("1.0") / Decimal("3.0") * Decimal("3.0")
Decimal('0.9999999999999999999999999999')

1.0 にはなりませんでした。

Pythonの Decimal任意精度の十進小数です。デフォルトの精度は getcontext() 関数によって決まり、これの初期値は28桁です。100桁で計算する例は次のようになります:

>>> from decimal import getcontext
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> getcontext().prec = 100
>>> Decimal("1.0") / Decimal("3.0") * Decimal("3.0")
Decimal('0.9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999')

参照:

C言語の場合(IEEE 754の十進浮動小数点数)

C23にはオプショナルな機能としてIEEE 754準拠の十進浮動小数点数型のサポートが入ります。_Decimal32 とか _Decimal64 とか _Decimal128 のような型名です。

執筆時点では、GCCが実験的にサポートしています。これで 1.0 / 3.0 * 3.0 を計算してみましょう。

#include <stdio.h>

// C23がいい感じに実装された未来では strtod128 で文字列化できるが、現時点のlibcではサポートされていないようなので自前でやる
void print_d128(_Decimal128 x)
{
    if (x < 0.0dl || 1.0dl / x < 0.0dl) {
        putchar('-');
        x = -x;
    }
    unsigned long long int intPart = (unsigned long long int)x;
    printf("%llu.", intPart);
    x -= (_Decimal128)intPart;
    while (x != 0.0dl) {
        x *= 10.dl;
        intPart = (unsigned long long int)x;
        putchar('0' + intPart);
        x -= (_Decimal128)intPart;
    }
}

int main()
{
    _Decimal128 a = 1.0dl;
    _Decimal128 b = 3.0dl;
    _Decimal128 c = a / b * b;
    print_d128(c);
    putchar('\n');
}
$ gcc -std=c2x test.c
$ ./a.out
0.9999999999999999999999999999999999

1.0 にはなりませんでした。

_Decimal128 は何者なのかというと、整数の仮数部 $M$ と指数部 $e$ について

$$
M\times 10^{e-33}
$$

と表現できる実数のことです。ただし、$M$ は絶対値が $10^{34}-1$ 以下の整数(精度34桁)で、$e$ は $-6143$ 以上 $6144$ 以下の整数です。

_Decimal64 の精度は16桁、_Decimal32 の精度は7桁です。いずれにせよ任意精度ではないことに注意してください。

IEEE 754準拠の十進浮動小数点数をハードウェア実装しているのは、現時点ではIBMのCPUくらいしかなさそうです(筆者が把握している限り)。十進小数が好きすぎて仕方がない人はIBMの株とCPUを買いましょう。

【追記】コメントで教えていただきましたが、SPARC64X以降も十進浮動小数点数をハードウェア実装しているようです。補足ありがとうございます。【追記終わり】

おまけ:doubleの場合

【追記】普通の倍精度二進浮動小数点数では 1.0 / 3.0 * 3.0 はちょうど 1.0 になります。もちろん二進でも 1.0 / 3.0 は循環小数になり途中で丸めが発生しますが、その後の掛け算でたまたま 1.0 という正しい値が得られるわけです。

> 1.0 / 3.0 * 3.0
1
> 1.0 / 3.0 * 3.0 === 1.0
true

一方で、倍精度二進浮動小数点数では 1.0 / 49.0 * 49.01.0 以外の値になります。

> 1.0 / 49.0 * 49.0
0.9999999999999999
> 1.0 / 49.0 * 49.0 === 1.0
false

どの値でこういう現象が起こるかは浮動小数点形式に依存し、二進であっても単精度であれば 1.0 / 49.0 * 49.0 はちょうど 1.0 になります(しかし、今度は 1.0 / 41.0 * 41.01.0 からずれるようになります)。【追記終わり】

「浮動小数点数」だからいけないのか?固定小数点数だとどうなのか?

SNSを観察していると、「固定小数点数が最強!浮動小数点数はクソ!」とでも言いたげな意見を見かける気がします(印象です)。

では十進固定小数点数で 1.0 / 3.0 * 3.0 を計算してみましょう。Haskellの Data.Fixed を使います。

ghci> import Data.Fixed
ghci> 1.0 / 3.0 * 3.0 :: Fixed E12
0.999999999999

はい。1.0にはなりませんでした。固定小数点数とはいえ有限の桁数で実数を表現することに変わりはないので、割り算とかで循環小数になるとアウトです。

そもそも、実数の表現で「二進」とか「十進」とか呼んでいるのは表現の際に有限桁で打ち切るからであり、無限精度の実数表現であれば「二進」とか「十進」とかつける必要はないのです。多倍長整数型や有理数型に「decimal」とついているのを見たことがありますか?「十進小数」(decimal)と呼ばれるデータ型があったらその時点で正確さが何らかの形で犠牲となることは確定しているのです。

有理数などの正確な表現方法

小数ベースの表現方法ではなく、分母分子を多倍長整数で表現した有理数型を用意している言語はちょいちょいあります。Haskellの Rational、Rubyの Rational、Pythonの Fraction などです。有理数型であれば四則演算の際に誤差が出ることはありません。四則演算で済む用途で誤差が許されない場合は有理数型を使うと良いでしょう。

しかし、有理数の範囲では平方根とか指数関数とか三角関数が自由に計算できません。平方根とか、超越関数の一部を代数的に取り扱いたいとなると、数式処理システムに片足を突っ込むことになるでしょう。あるいは、計算可能実数を使えば表現力の問題はなくなりますが、今度は比較演算が停止するとは限らなくなります。

結局のところ、計算機上で実数を表現するための万能で使いやすい方法はないのです。用途に応じて最適な表現方法を考える必要があるのです。

実数の厄介さを何も知らんのに「二進だからだめ!小数点が動くからだめ!」と喚いている蒙昧な輩がSNSには多すぎませんか?

271
141
31

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
271
141