浮動小数点数の比較演算に関するメモ

二つの浮動小数点数を比較する演算は、C言語演算子としては6種類(<<=>>===!=)ありますが、NaNの取り扱いを考えるともっとあります。

一般的なのは、以下の四状況に関してそれぞれtruefalseがどうなるかを考えた14種類(24=16のうち、2種類は「常にtrue」「常にfalse」で除外)です*1

どちらかがNaN 左<右 左>右 左=右 備考
OEQ false false false true C言語==RISC-VのFEQ
OGT false false true false C言語>
OGE false false true true C言語>=
OLT false true false false C言語<RISC-VのFLT
OLE false true false true C言語<=RISC-VのFLE
ONE false true true false
ORD false true true true
UNO true false false false
UEQ true false false true
UGT true false true false
UGE true false true true
ULT true true false false
ULE true true false true
UNE true true true false C言語!=

C言語演算子は、!=以外はorderedな比較を行います。orderedな比較とは、オペランドの少なくとも片方がNaNの場合、falseになるというものです。 そのため、整数の場合と異なり、a > ba <= bの結果がともにfalseであるということが起こります(aもしくはbがNaNの場合に発生します)。

RISC系の命令セットでは、必要最小限の比較命令のみ用意して、後は条件を反転、などの方法で他の比較条件を実現することがあります。 しかし、こういうことがあるので、コンパイラは安易に条件を反転したりできません。 実際にRISC-Vコンパイラ(llc)がどのようにして各条件を実現しているかを調べてみると、以下のようになっていました。

比較 実現方法
OEQ FEQを使う
OGT FLTを使う(オペランド順序を逆にする)
OGE FLEを使う(オペランド順序を逆にする)
OLT FLTを使う
OLE FLEを使う
ONE ORDを計算して、FEQの結果を反転したものとANDをとる
ORD オペランドごとに自分とのFEQを計算してANDをとる
UNO ORDを計算して結果を反転
UEQ UNOを計算して、FEQの結果とORをとる
UGT FLEの結果を反転
UGE FLTの結果を反転
ULT オペランド順序を逆にしたFLEの結果を反転
ULE オペランド順序を逆にしたFLTの結果を反転
UNE FEQの結果を反転

いくつかはllvmの自動フォールバック(SelectionDAGLegalize::LegalizeSetCCCondCode関数)で生成されているので一部無駄のあるコード生成になっています。 例えば、ONEを計算するにはOGTの結果とOLTの結果をORするのが、命令数の観点から最適と思われます。

実際、(a > b) | (a < b)コンパイルしてみると以下の残念なコードが出力されます。 C言語フロントエンドは正しく最適化してllvm-IRにONEを出力するものの、RISC-Vバックエンドはその最適化をうまく活用できなかったようです。

        feq.d   a0, ft0, ft0
        feq.d   a1, ft1, ft1
        and     a0, a0, a1
        feq.d   a1, ft1, ft0
        not     a1, a1
        and     a0, a0, a1

ちなみに、UNOは、llvmの自動フォールバックでは「オペランドごとに自分とのUNEを計算してORをとる」ですが、RISC-Vコンパイラは最適な「オペランドごとに自分とのOEQを計算してANDしたものを反転」を出力します(RISCVInstrInfoD.tdに書かれています)。

*1:ほかにもオペランドとしてquiet NaNが与えられた場合に例外を発生させるかを考慮に入れるとさらに状況が二倍に増えます