先日、浮動小数点数の非正規化数と絡む機会があった。
で、非正規化数のことをよく知らなかったので調べた。
せっかく調べたのでメモを書いておく。
はじめに
まあ、ふつうの環境の C / C++ の double
、ふつうの環境の ruby の Float
、いわゆる倍精度のことを念頭に。
以下、IEEE754 の 倍精度のみが唯一の浮動小数点数であるかのような感じで文章を書くけどもちろん本当はそうではないのでご注意を。
よく、C/C++ の DBL_MIN
に
double型の浮動小数点で表現できる正の値の最小値
などという説明が付与されているけど、これは間違っている。実際
#include <stdio.h>
#include <float.h>
int main(void) {
printf(
"DBL_MIN=%e DBL_MIN/10=%e DBL_MIN/1000=%e",
DBL_MIN,
DBL_MIN/10,
DBL_MIN/1000);
return 0;
}
は
DBL_MIN=2.225074e-308 DBL_MIN/10=2.225074e-309 DBL_MIN/1000=2.225074e-311
を出力する。DBL_MIN
は最小値ではない。
正しい説明は
double
で表現できる正の正規化数の最小値
なんだけど、じゃあ正規化数って何? とか、そういう話。
浮動小数点数
浮動小数点数には5つの種類がある。たぶん、実際に計算する場合にはこの5つのどれであるかで条件分岐して計算する必要がある。
分類 | 説明 |
---|---|
無限大 | 正の無限大と負の無限大がある。 |
非数 | 多くの不幸の原因になっている。いろいろ種類がある。この記事では触れない。 |
正規化数 | 普通の浮動小数点数。 |
非正規化数 | この記事の話題の中心。 |
ゼロ | 正のゼロと負のゼロがある。 |
今回話題にするのはあまり光があたっていない気がする、非正規化数。
正規化数
非正規化数の話をするためには、正規化数の話をせざるを得ない。
正規化数は $s×m×2^{e}$ という形をしている。
s は符号。+1 または -1。
e は、-1022〜1023 の整数。
m は2進数で「1.」から始まる小数点以下52桁の有限小数。
言い換えると
m=\sum_{i=-52}^{0} d_i×2^{i} \\
d_i=\begin{cases}
1 \ \ \ \ \ \ \ \ \ \ {\rm if} \ i=0 \\
1 \ or \ 0 \ \ {\rm otherwise}
\end{cases}
となる。
というわけで、正の正規化数の最小値は $ 1×2^{-1022} $ になる。
試してみると以下の通り。
1*2r**-1022 - Float::MIN.to_r
#=> (0/1)
念の為に書いておくと、 Float::MIN
は正の正規化数の最小値を意味する。
正規化数から正規化数を引く
ruby には Float#next_float
, Float#prev_float
という便利なメソッドがあるので、気軽に隣の浮動小数点数を得ることができる。
こんな具合
Float::MIN.then{ |e| [e, e.next_float ] }
#=> [2.2250738585072014e-308, 2.225073858507202e-308]
これだと値の趣旨がよくわからないのでちょっと加工してみる。
Float::MIN.
then{ |e|
[e, e.next_float ].
map{ |v| "(2**52%+d)/(2**(1022+52))" % [v.to_r*2**(1022+52)-2**52]
}
}
#=> ["(2**52+0)/(2**(1022+52))", "(2**52+1)/(2**(1022+52))"]
わかりやすくなった?
というわけで、
\begin{eqnarray}
{\texttt {Float::MIN}} = \frac{2^{52}}{2^{1022+52}} \\
{\texttt {Float::MIN.next_float}} = \frac{2^{52}+1}{2^{1022+52}}
\end{eqnarray}
であることがわかる。
で。
Float::MIN.next_float - Float::MIN
を計算することを考える。
Float::MIN.next_float
が $(2^{52}+1)×2^{-1074}$ で、
Float::MIN
が $2^{52}×2^{-1074}$ なんだから、
引き算の結果は $1×2^{-1074}$ になってほしそうだ。
しかし前述の通り、指数部の最小値は -1022 なので、正規化数には入らない。
表現できる最小値を下回っているんだから 0 にしちゃえ、というのも一つの見識だが、そうすると
Float::MIN.next_float != Float::MIN
なのに
Float::MIN.next_float - Float::MIN == 0
ということになる。これは気持ち悪い。
というわけで、この気持ち悪さを救うために非正規化数が登場する。
非正規化数の集合
非正規化数は、相異なるふたつの浮動小数点数 a
, b
があるとき、 a-b
をゼロにしないために導入された。
んだと思う。
a
, b
は、正規化数かもしれないし、非正規化数かもしれない。ゼロかもしれない。
(正の0と負の0が「相異なる」かどうかは微妙なんだけど、この文脈では正の0と負の0は等しいことになる)
「正規化数および0で作られる集合」から相異なる2数を選んで距離を計算する場合。
作れる値の最小値は前述の $2^{-1074}$ になる。
というわけで、この値 $2^{-1074}$ の整数倍を表現できるようにしておけば、 a!=b
なのに a-b==0
となる事態を避けられる。
$2^{-1074}$ の整数倍にもいろいろある。
- 0
0倍も倍数のうち。0は正規化数ではないが、別枠で確保されているので「非正規化数」とは呼ばれない。 - 絶対値が $2^{-1022}$ か、それ以上のもの。
これは正規化数なので、非正規化数ではない。
残ったもの、つまり、
- $2^{-1074}$ の整数倍で、
- 0 でなく、
- 絶対値が $2^{-1022}$ 未満のもの
が非正規化数。これらの値を浮動小数点数として表現できるようにしておけば、前述の気持ち悪い現象を回避することができる。
ので、実際表現できるようになっている。
非正規化数の特徴
値
非正規化数は $s×m×2^{-1074}$ という形をしている。
s は符号。+1 または -1。
m は整数で、$ 0 \lt m \lt 2^{52}$
つまり、小数点が浮動しない。
単なる52bitの整数みたいなもんである。
精度
正規化数は必ず2進数53桁分の精度があるので、
x*(1+1e-15)-x
のような計算をしてもゼロにならない。
しかし、非正規化数は値が小さくなるにつれて有効桁数が減っていくので上記のような計算をするとゼロになってしまうことがある。
試しに上記の計算をいくつかの値に適用してみよう
x |
x*(1+1e-15)-x |
x の種類 |
---|---|---|
1e308 |
1.1975041857208319e+293 |
正規化数 |
1e0 |
1.1102230246251565e-15 |
正規化数 |
1e-308 |
1.0e-323 |
正規化数 |
1e-320 |
0.0 |
非正規化数 |
また。x
も x/2
も正規化数であるならば、
x/2*2 - x
は必ずゼロになる。指数部の減算と加算が行われるだけで、仮数部の値には変化がないからだ。
しかし、 x/2
が非正規化数で、 $x×2^{1074}$ が奇数の場合、x/2
を正確に表現できなくなるので x/2*2 - x
は、ゼロにならない。
試してみよう。
x=(2r**-1074*(2**52+0)).to_f;x/2*2-x
#=> 0.0
x=(2r**-1074*(2**52+1)).to_f;x/2*2-x
#=> -5.0e-324
x=(2r**-1074*(2**53+0)).to_f;x/2*2-x
#=> 0.0
x=(2r**-1074*(2**53+1)).to_f;x/2*2-x
#=> 0.0
おまけ
「倍精度の最小値」みたいな趣旨の名前で、各種処理系で定数が提供されている。
思いつく範囲でまとめてみた。
処理系 | 名前 | 種類 |
---|---|---|
C/C++ | DBL_MIN |
正の正規化数の最小値 |
C11/C++17 | DBL_TRUE_MIN |
正の最小値(しばしば非正規化数) |
C# | Double.MinValue |
正の正規化数の最小値 |
Go | math.SmallestNonzeroFloat64 |
正の最小値(たぶん非正規化数) |
Java | Double.MIN_NORMAL |
正の正規化数の最小値 |
Java | Double.MIN_VALUE |
正の最小値(非正規化数) |
JavaScript | Number.MIN_VALUE |
正の最小値(非正規化数) |
Julia | floatmin(Float64) |
正の正規化数の最小値 |
Python3 | sys.float_info.min |
正の正規化数の最小値 |
ruby | Float::MIN |
正の正規化数の最小値 |
Rust | f64::MIN_POSITIVE |
正の正規化数の最小値 |
正の正規化数の最小値を返すのなら「正」「正規化数」「最小」の3つの情報が入った doble.min_positive_normal
みたいな名前が良いと思うんだけど、そういう人はいない模様。
非正規化数を含めた正の最小値を返すなら 「正」「最小」の2つの情報が入った double.min_positive
みたいな名前が良いと思う。そういう人もいない。
Go での名前 SmallestNonzeroFloat64
は、不適切だよね。Nonzero だったら負でもよさそう。なんで SmallestPositiveFloat64
にしなかったんだろう。
まとめ
-
DBL_MIN
やFloat::MIN
などより小さな正の浮動小数点数があるよ。それらは非正規化数と呼ばれているよ。 - 非正規化数は、固定小数点数なので、値が小さくなるにつれて有効桁数が減るよ。
- 非正規化数は正規化数とちがって有効桁数が可変なので、正規化数とは振る舞いが若干異なるよ。