非正規化数を意識する

浮動小数点数がおおまかに 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       最小正規化数
                :       :
非正規化数なし -+-------+-+-+-+-+---+---+---+---+-------+-------+-------+-----
                :       :
非正規化数あり -+-+-+-+-+-+-+-+-+---+---+---+---+-------+-------+-------+-----
                :       :

怖いところ

ここでのポイントは

  1. 通常の指数形式で表せる数 == 正規化数の絶対値には下限がある。
  2. 最小正規化数と 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 になる可能性がある。

TODO

戻る

y.fujii <y-fujii at mimosa-pudica.net>