答え合わせ
タイトルに惹かれて、答えがすぐに知りたい方のために早速回答をお出しします!
-
NaN == NaN→false -
NaN < NaN→false -
NaN < 1→false
です。ちなみに、
-
NaN == 1→false -
NaN > NaN→false -
NaN > 1→false
つまり、NaNに対する比較は、相手がNaNであっても全てfalseになります。
なぜこんな話題?
知っている方からしたらなんてことない話題かと思いますが、最近Rustを学習中に気になることがありました。
PartialEq
Rustにはこのようなトレイトがあります(トレイトについては割愛)。
部分的に等しい?どういうこと?
どんなデータも突き詰めたら0と1のバイナリだし、等しくならないことなんて部分的にもありえるの?
調べると、ほとんどのソースでNaNの比較に関する話題が出てきます。
そうです。NaNを取りうる浮動小数点数に関しては、部分的にしか等しくならないことがありえるのです。(NaNを含む比較は常に等しくないから。)
さらに深掘り
でも、NaNだって突き詰めたら0と1のバイナリであることは変わらない。
仮にCPUで1ビットづつ比較していたら等しくなることもありえる。
じゃあ、ソースコードのコンパイル時に、比較処理では毎回NaNを含んでいるかどうかの判定をして、falseになるように命令を変えているのか?
、、、答えはNoです!
NaNが自身を含めたどの値とも等しくない、ということはIEEE 754に標準規格として定められているのです。
そしてその挙動は、CPUレベルで実装されています。
どういう仕組みか
CPUにNaNを含んだ比較命令を実行させると、特定のフラグレジスタが立ちます。
(x86→PF=1、ARM64→V=1)
そのフラグや他のフラグを活用して、NaNの比較結果がfalseになるようにしているのです。
ARM64でさらに深掘り
Apple Silicon Macを使っているのでARM64で深掘りさせてください。
ARM64では浮動小数点数を比較するfcmpという命令があります。
この命令を実行すると、比較対象の値に応じて以下のようにフラグレジスタに値が入ります。
| 比較結果 | N | Z | C | V |
|---|---|---|---|---|
| a < b | 1 | 0 | 0 | 0 |
| a == b | 0 | 1 | 1 | 0 |
| a > b | 0 | 0 | 1 | 0 |
| unordered(NaN含む) | 0 | 0 | 1 | 1 |
これをもとにb.{x}命令で条件分岐をしますが、代表的なものは以下のようになっています。
| 命令 | 意味 | 条件 |
|---|---|---|
b.eq |
equal | Z = 1 |
b.ne |
not equal | Z = 0 |
b.mi |
minus (negative) | N = 1 |
b.pl |
plus (positive or zero) | N = 0 |
b.vs |
overflow set | V = 1 |
b.vc |
overflow clear | V = 0 |
b.hi |
unsigned > | C=1 & Z=0 |
b.ls |
unsigned <= | C=0 or Z=1 |
b.ge |
signed >= | N=V |
b.lt |
signed < | N≠V |
b.gt |
signed > | Z=0 & N=V |
b.le |
signed <= | Z=1 or N≠V |
コンパイラによる違いはあると思いますが、実数の比較結果に応じた分岐を満たしながら、NaNを含んだC=1,V=1の場合にfalse側に分岐するような命令にコンパイルされます。
例えばC言語で以下のプログラムを書いてアセンブルすると、
#include <stdio.h>
#include <math.h>
int main() {
float a = NAN;
float b = 1.0f;
if (a < b) printf("a < b\n");
return 0;
}
このようなアセンブリが得られます。(LBB0_1がtrue側の処理、LBB0_2がfalse側の処理)
fcmp s0, s1
b.pl LBB0_2
b LBB0_1
b.plなので、フラグレジスタN = 0の時にLBB0_2のfalse側の処理が動きます。
つまり、N = 1となるa < bの場合はtrue側の処理が動き、それ以外のケースとNaNを含むケースはN = 0なのでfalse側の処理が動きます。
このような形で、NaNを含んだ比較では(N,Z,C,V)=(0,0,1,1)という特別な組み合わせでフラグレジスタがセットされて、それをうまく活用した分岐命令を使うことによって、実数の比較結果を保ちつつNaNの比較では必ずfalseになっているのです。
おまけ
検証のために使用した、NZCVフラグ確認用のRustプログラムを掲載します。
use std::arch::asm;
pub fn fcmp_nzcv_f64(a: f64, b: f64) -> u64 {
let nzcv: u64;
unsafe {
asm!(
"fcmp d0, d1",
"mrs {}, nzcv",
out(reg) nzcv,
in("d0") a,
in("d1") b,
);
}
nzcv
}
fn decode(nzcv: u64) -> (u8, u8, u8, u8) {
let n = ((nzcv >> 31) & 1) as u8;
let z = ((nzcv >> 30) & 1) as u8;
let c = ((nzcv >> 29) & 1) as u8;
let v = ((nzcv >> 28) & 1) as u8;
(n, z, c, v)
}
fn main() {
let nan = f64::NAN;
let cases = [
("1.0 vs 1.0", 1.0, 1.0),
("1.0 vs 2.0", 1.0, 2.0),
("2.0 vs 1.0", 2.0, 1.0),
("NaN vs 1.0", nan, 1.0),
("1.0 vs NaN", 1.0, nan),
("NaN vs NaN", nan, nan),
];
for (label, a, b) in cases {
let nzcv = fcmp_nzcv_f64(a, b);
let (n, z, c, v) = decode(nzcv);
println!("{label} N={n} Z={z} C={c} V={v}");
}
}
出力結果
1.0 vs 1.0 N=0 Z=1 C=1 V=0
1.0 vs 2.0 N=1 Z=0 C=0 V=0
2.0 vs 1.0 N=0 Z=0 C=1 V=0
NaN vs 1.0 N=0 Z=0 C=1 V=1
1.0 vs NaN N=0 Z=0 C=1 V=1
NaN vs NaN N=0 Z=0 C=1 V=1