浮動小数点数がおおまかに x * 2 ^ y のような指数形式で表現されていることはよく知られていて、多くの場合それだけ知っていれば何ら問題がなかったりする。でも、実はもう少しだけ知っておかないと危険な場合があるよ、という話。
具体的なビット表現がどうなっているは知らなかった方 (∋少し前の自分) が、ここから下をチラ見して、一歩掘り下げてみるきっかけになれば幸い。
浮動小数点数の符号化方式として標準的な IEEE754 では、
± (1.xxxx) * 2 ^ (yyyy)
の形で符号、仮数部 xxxx 、指数部 yyyy を符号化する。仮数部の 1 は符号化されないのがポイント。 1-bit 節約できる以上に、仮数部が自然に [1, 2) の範囲に制限されることで、任意のビット列 xxxx yyyy と浮動小数点数が 1:1 対応するというメリットがある。この形で表される数を正規化数と呼ぶ。
ただ、このままでは表現できる値の絶対値に下限ができてしまう。0 も表現できないし、仮に 0 に対応する表現を例外的に設けたとしても、 0 と最小の正規数の間がもの凄ーく開いてしまう。
そこで非正規化数 (denormal number / denormalized number) というものを考える。これは 0 と最小正規化数の間を固定小数点でつなぐもので、指数部が最小のときは例外的に仮数部の解釈を
± (0.xxxx) * 2 ^ (最小の指数 + 1)
と変えることで導入される。
刻み幅は以下のようなイメージ。
0 最小正規化数
: :
非正規化数なし -+-------+-+-+-+-+---+---+---+---+-------+-------+-------+-----
: :
非正規化数あり -+-+-+-+-+-+-+-+-+---+---+---+---+-------+-------+-------+-----
: :
ここでのポイントは
の二点。
例として、信号処理でよく使われる sinc 関数の実装を考える。
double sinc( double x ) {
if( x == 0.0 )
return 1.0;
else
return sin( x ) / x;
}
|x| << 1 のとき、いかにも危険そうだ。上の(非)正規化数の知識があれば、 x が正規化数で表せる間は問題なくて、非正規化数に突入すると精度的にまずくなると分かる。つまり、
double sinc( double x ) {
// std::numeric_limits<double>::min() は double の最小正規化数。
if( x < numeric_limits<double>::min() )
return 1.0;
else
return sin( x ) / x;
}
こう実装するのが正しい(ただし sinc(x) = 1 + O(x^2) なので、この場合は 1e-8 くらいで分けても十分だったりする。例が良くない…)。
sinc 関数の例は、ある意味で非正規化数を避ける方法と言える。次は非正規化数が直接役立つ例。
問: 次の二つの条件は等価か。ただし x, y は Inf, NaN でないとする。
0. x == y
1. x - y == 0.0
正解は、非正規化数があれば等価。非正規化数がなければ、 x != y でも x - y == 0.0 になる可能性がある。
NaN != NaN なので、 x == x は恒真式ではないとか。