BETA

浮動小数点数の非正規化数の存在意義と特徴

投稿日:2020-05-10
最終更新:2020-05-10

先日、浮動小数点数の非正規化数と絡む機会があった。
で、非正規化数のことをよく知らなかったので調べた。
せっかく調べたのでメモを書いておく。

はじめに

まあ、ふつうの環境の 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 非正規化数

また。xx/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_MINFloat::MIN などより小さな正の浮動小数点数があるよ。それらは非正規化数と呼ばれているよ。
  • 非正規化数は、固定小数点数なので、値が小さくなるにつれて有効桁数が減るよ。
  • 非正規化数は正規化数とちがって有効桁数が可変なので、正規化数とは振る舞いが若干異なるよ。
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

まだ何を書くのか、そもそもここに何書くのかも決めてない。

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう