よく二進浮動小数点数の問題として、
> 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.0
が 1.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.0
が 1.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には多すぎませんか?