この前の Java で NaN や Infinity を int にキャストしたときの値 を C# でやってみたら、面白い挙動になったのでメモ。
C# で NaN や Infinity を int にキャストしたときの値
C# で NaN や Infinity を int にキャストしてみたところ、結果はいずれも "0" になりました。
Console.WriteLine(unchecked((int)double.NaN)); // 0 Console.WriteLine(unchecked((int)double.Infinity)); // 0
ところが、NaN や Infinity を一度ローカル変数に格納してからキャストしてみたところ、今度は "-2147483648" になりました。
NaN でやねん。
double value = double.NaN; Console.WriteLine ((int) value); // -2147483648 double infinity = double.Infinity; Console.WriteLine ((int) infinity); // -2147483648
言語仕様は?
言語仕様によれば、このような NaN から int へのキャストをした結果は「未指定値 (unspecified value)」*1。
つまり、特に値の指定はないので 0 になろうが -2147483648 になろうが、言語仕様通りということ。
6.2.1 明示的な数値変換の一覧表
でも、「変数に代入するかどうか」でキャストの結果が変わるのは不思議です。
よくわからなかったので、Stack Overflow に質問を投げてみました。
すると、返ってきた回答は「手元の環境で再現しない」。しかも、複数の人からこれが vote up されていました。あれ…?
疑問
なぜ、0 になったり -2147483648 になったりするのでしょうか。
なぜ、変数に代入するかしないかで値が変わってしまうのでしょうか。
なぜ、ほかの人の環境では再現しなかったのでしょうか。
言語仕様通りと言えばそれまでです。
でも、この動きにはその先があるはずです。
さらに調べてみました。
変数に格納するかどうかで値が変わった理由
Stack Overflow のコメントの中で「コンパイラなに使ってる?」というのがありました。
これがヒントになりました。
自分が試していたのは Visual Studio 2013 *2 と古いバージョンだったためです。
そこで、Visual Studio 2015 の Roslyn コンパイラーで再度実行してみたところ、(int)double.NaN の値が 0 ではなく -2147483648 になりました。
Console.WriteLine(unchecked((int)double.NaN)); // -2147483648
つまり、(int)double.NaN と書いたときはコンパイラがこれを評価して結果を定数としてバイナリに埋め込んでいるということ。
リバースアセンブルしてILを確認したところ、たしかにそうなっていました。
// Visual Studio 2013 IL_0000: ldc.i4.0 IL_0001: call void [mscorlib]System.Console::WriteLine(int32) // Visual Studio 2015 IL_0000: ldc.i4 0x80000000 IL_0005: call void [mscorlib]System.Console::WriteLine(int32)
たぶん、以前のコンパイラーは NaN を int にキャストしたときの評価結果を決め打ちで 0 にしているのではないかと思います*3。
一方、変数に格納してからキャストした場合は実行時に評価されるので、その時の実行環境に依存しています(これについては後述)。
この評価するタイミングの差によって、値が変わってしまうようです。
-2147483648 はどこから来た値?
なぜ、NaN を変数に格納して int にキャストすると -2147483648 になるのでしょうか。
これは、x86 (SSE2) の仕様のようです。
該当部分を逆アセンブルして確認したところ、SSE2 の cvttsd2si 命令*4が使われていました。
Console.WriteLine((int)value); 00B73544 movsd xmm0,mmword ptr [ebp-0Ch] 00B73549 cvttsd2si ecx,xmm0 00B7354D call 6DC26C0C
この命令は、もし入力が NaN だった場合は結果は整数不定値*5 0x80000000 になるそうです。
不定値という名前なので紛らわしいですが、0x80000000 固定のようです。
E.4.2.2. SSE、SSE2、SSE3数値命令でNaNオペランドまたはNaN結果を含む演算の結果
IA-32 インテル® アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル
以下の表(E-1.〜E-10.)は、NaNの入力値(またはNaNの結果を生じさせるNaNでない入力値)に対する、SSE、SSE2、SSE3の応答を示している。単精度 QNaN 不定値は0xffc00000であり、倍精度 QNaN 不定値は0xfff8000000000000であり、整数不定値は0x80000000である。この値は浮動小数点値ではないが、浮動小数点値から整数への変換命令の結果になりうる。
0x80000000 は 10進数で -2147483648。
よって、NaN を変数に格納して int にキャストした場合に -2147483648 が表示されていたのは、この命令の仕様によるもののようです。
CPUが変われば値も変わるのか?
SSE2 命令の仕様で -2147483648 になるのなら、仕様が異なる ARM などの CPU で動かした場合は値も変わるのでしょうか。
そこで、Raspberry Pi 上に mono を入れて*6コンパイル&実行してみました。
すると、結果はいずれの場合も 0 になりました。やはり、NaN を int にキャストしたときの値は CPU に依存するようです。
Console.WriteLine(unchecked((int)double.NaN)); // 0 (on ARM) double value = double.NaN; Console.WriteLine ((int) value); // 0 (on ARM)
*1:初期値(initial value) の 0 ではないです。
*2:.NET Framework 4.6 がインストールされている環境の "C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe" と同じものです。再現実験する際は、これをご利用ください。
*3:Roslyn は、実際にその場でコードを走らせて、その結果を定数として格納したのではないかと思います。
*4:「切り捨てを使用して、xmm/m64の1つの倍精度浮動小数点値をr32の1つの符号付きダブルワード整数に変換する。」 IA-32 インテル® アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル より
*5:「整数不定値は、x87 FPUが整数値を操作するときに戻すことがある、特殊な値である。」IA-32 インテル® アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル より
*6:.NET Core で実験しようとしたのですが、まだ ARM は未対応なので諦めました。