何比較してもfalseになるNaN

評価計画を立てているときに、浮動小数点演算の結果が無限大になったときの処理も確認しないとな。

というところでIEEE754形式の浮動小数点数の資料を見たら、

NaNというのもあることを思いだし、これがけっこうなくせ者だった。


特殊な浮動小数点数として +0, –0, +∞, –∞とNaNがある。

+0と-0はどちらも0なのだが、符号ビットの概念が存在する。

通常は+0を使うのだが、計算によっては-0が生じることがある。

とはいえ大概の場合、意識する必要はない。

+∞, -∞はゼロ除算で生じることが比較的多い。

1.0/0.0=+∞, -1.0/0.0=-∞ といった具合である。

数学的には正しいとは言えないけど、概念的にはわかりますよね。


NaNは “Not a Number”の略で、漢字で書けば「非数」ってことになる。

なんのこっちゃという話だが、何らか未定義の値が出てくるとこうなる。

ゼロ除算でも 0.0/0.0 の場合は±∞ではなく NaN となる。

∞が絡む演算では 0.0×(±∞) や ∞-∞ は NaNとなる。

また、虚数が生じる演算 sqrt(-1.0) のような計算も NaN となるそう。

NaNが絡む演算も通常はNaN になるので 0.0/0.0+1.0 も NaN+1.0=NaNとなる。


浮動小数点数をスケーリング・整数化して表示する処理がある。

ここに +∞, –∞とNaN をそれぞれぶち込んでみた。

+∞, -∞は上限・下限に貼り付くような動作で、これはこれでよい。

しかしNaNは0になってしまった。一体なぜか。

float fval = roundf(forg * fscale);
if(fval < (float)SHRT_MIN){
     shortval = (short)SHRT_MIN;  //(1)
}else if(fval > (float)SHRT_MAX){
     shortval = (short)SHRT_MAX;  //(2)
}else{
     shortval = (short)fval;      //(3)
}

まず、forg×fscaleを四捨五入で整数化した値をfvalとして求める。

この値が -32768(SHRT_MIN)~32767(SHRT_MAX) であればそのままshortvalに代入する。

-32768未満であれば-32768、32767超であれば 32767 に丸める。

この処理で forg=-∞, fscaleが適当な正の数だと fval=-∞ となる。

-∞<SHRT_MIN なので(1)に分岐し、-32768 となり下限に貼り付く。

forg=+∞もfval=+∞となれば +∞>SHRT_MAX なので(2)に分岐し 32767 に貼り付く。


問題は forg=NaNで、fval=NaNとなった場合である。

実はNaNと他の数の比較はすべてfalseとなるルールがあるそうで、

NaN<0 も NaN>0 も NaN==NaN すらもfalseとなるのである。

すなわち、上記の(1)にも(2)にも該当せず、

あたかも -32768~32767 の範囲にあるかのように(3)に入る。

こうなったNaNをどのように整数変換するかは処理系依存だが、ARMでは0となるようだ。


ところでここまでの処理で例外とか発生しないの? と気になった人もいるかもしれない。

ARMではゼロ除算など不正な演算をしても例外にならないのが通常である。

レジスタ設定により例外を発生させることもできるみたいだけど。

ARMでは整数演算で0除算をすると0になるというルールを持っており、

100/0 のような演算をうっかりやってしまうと 0になるんですね。

x86はゼロ除算で例外が発生するのだが、必ずしもそうではないと。


何を比較してもfalseになるのが功を奏したものもあって、

a<bの場合に動作する処理を if(a<b){ … }else{ … } と書くと、aかbがNaNだとelse側に入ることになる。

数値比較してfalse側にフォールバック的な動作があればよいのだが、

逆に範囲外の処理をtrue側に書くと、ひっかからないということが起こる。

とりあえずNaNを何か範囲外相当の処理に落とし込みたいわけである。

というわけで考えたのはこういう書き方である。

if(!(fval >= (float)SHRT_MIN)){

「SHRT_MIN未満」という書き方を、あえて「SHRT_MIN以上でなければ」としている。

NaN以外の数に対する動作は何ら変わらない。

NaN >= SHRT_MIN は falseになるので、これを反転するとtrue になる。

これにより従来の書き方では分岐しなかった(1)の処理に入り、

fval=NaNは便宜上 -32768 に変換されることになる。

いかにも異常な数字であることが気づける可能性が高まる。


実際のところ +∞, –∞, NaN なんて発生させちゃいけないんですけどね。

ただ、設定次第で発生する可能性は否定できないので、

その場合でもなんらか説明ができる動作にしておくべきという話ですね。

比較順序によってはあまり問題にならないんですけどね。

ただ、意識して作らないとなかなかうまくいかないだろうなとは思った。