でMSDNSystem.Single
を読むとき:
Single
バイナリ浮動小数点演算のIEC60559:1989(IEEE 754)標準に準拠しています。
およびC#言語仕様:
およびタイプは、32ビット単精度および64ビット倍精度IEEE754形式を使用して表されます[...
float
]double
以降:
積は、IEEE754算術の規則に従って計算されます。
float
タイプとその乗算がIEEE754に準拠しているという印象を簡単に得ることができます。
乗算が明確に定義されているのはIEEE754の一部です。つまり、2つのインスタンスがある場合、それらの「正しい」製品はfloat
1つだけ存在するということです。float
製品がそれを計算するシステムの「状態」または「セットアップ」に依存することは許されません。
ここで、次の簡単なプログラムについて考えてみます。
using System;
static class Program
{
static void Main()
{
Console.WriteLine("Environment");
Console.WriteLine(Environment.Is64BitOperatingSystem);
Console.WriteLine(Environment.Is64BitProcess);
bool isDebug = false;
#if DEBUG
isDebug = true;
#endif
Console.WriteLine(isDebug);
Console.WriteLine();
float a, b, product, whole;
Console.WriteLine("case .58");
a = 0.58f;
b = 100f;
product = a * b;
whole = 58f;
Console.WriteLine(whole == product);
Console.WriteLine((a * b) == product);
Console.WriteLine((float)(a * b) == product);
Console.WriteLine((int)(a * b));
}
}
Appartは、環境とコンパイル構成に関する情報を記述して、2つfloat
のs(つまりa
とb
)とそれらの製品を考慮します。最後の4つの書き込み行は興味深いものです。Debug x86(左)、Release x86(中央)、およびx64(右)でコンパイルした後、64ビットマシンでこれを実行した場合の出力は次のとおりです。
float
単純な操作の結果は、ビルド構成に依存すると結論付けます。
後の最初の行は、2つのs"case .58"
が等しいかどうかの簡単なチェックです。float
ビルドモードから独立していると期待していますが、そうではありません。float
次の2行は、aをにキャストするために何も変更しないため、同一であると予想されますfloat
。しかし、そうではありません。また、製品をそれ自体"True↩ True"
と比較しているので、彼らが読むことを期待しています。a*b
出力の最後の行はビルド構成から独立していると予想されますが、そうではありません。
正しい製品が何であるかを理解するために、手動で計算します。0.58
(a
)のバイナリ表現は次のとおりです。
0 . 1(001 0100 0111 1010 1110 0)(001 0100 0111 1010 1110 0)...
ここで、括弧内のブロックは、永久に繰り返される期間です。この数値の単精度表現は、次のように丸める必要があります。
0 . 1(001 0100 0111 1010 1110 0)(001 (*)
ここで、最も近い表現可能な値に切り捨てられます(この場合は切り捨てられます)Single
。さて、「百」(b
)の数は次のとおりです。
110 0100 . (**)
バイナリで。数値の完全な積を計算すると、次のよう(*)
になり(**)
ます。
11 1001 . 1111 1111 1111 1111 1110 0100
単精度に切り上げ(この場合は切り上げ)すると、
11 1010 . 0000 0000 0000 0000 00
次のビットが1
ではなく、0
(最も近いものに丸められる)ため、切り上げました。58f
したがって、結果はIEEEに準拠していると結論付けます。IEEEによれば、これは事前に与えられたものではありませんでした。たとえば、は、0.59f * 100f
より小さく、より大きくなります。59f
0.60f * 100f
60f
したがって、x64バージョンのコードが正しく機能しているように見えます(上の図の右端の出力ウィンドウ)。
注:この質問の読者のいずれかが古い32ビットCPUを使用している場合、上記のプログラムの出力がアーキテクチャーでどのようになっているのかを聞くのは興味深いことです。
そして今、質問のために:
- 上記はバグですか?
- これがバグではない場合、C#仕様では、ランタイムが
float
余分な精度で乗算を実行し、その精度を再び取り除くために「忘れる」ことを選択できると言っていますか? float
式を型にキャストすると、どのようにfloat
何かを変えることができますか?- たとえば、一時的なローカル変数にを引き出すことによって式を2つの式に分割するなど、一見無害に見える操作が、
(a*b)
数学的に(IEEEによる)同等である必要があるときに動作を変更するのは問題ではありませんか?float
プログラマーは、ランタイムが「人工的な」追加(64ビット)精度で保持することを選択したかどうかを事前に知ることができますか? - リリースモードでのコンパイルによる「最適化」で演算を変更できるのはなぜですか?
(これは、4.0バージョンの.NET Frameworkで行われました。)