二つの浮動小数点数を比較する演算は、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に書かれています)。