二つの浮動小数点数を比較する演算は、C言語の演算子としては6種類(<、<=、>、>=、==、!=)ありますが、NaNの取り扱いを考えるともっとあります。
一般的なのは、以下の四状況に関してそれぞれtrueとfalseがどうなるかを考えた14種類(24=16のうち、2種類は「常にtrue」「常にfalse」で除外)です*1。
- オペランドの少なくとも片方がNaNの場合
- 両方のオペランドがNaNではなく、左オペランドが右オペランドより小さい場合
- 両方のオペランドがNaNではなく、左オペランドが右オペランドより等しい場合(ただし、
0.0と-0.0は等しいとみなされる) - 両方のオペランドがNaNではなく、左オペランドが右オペランドより大きい場合
| どちらかが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 > bとa <= 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に書かれています)。