7

でMSDNSystem.Singleを読むとき:

Singleバイナリ浮動小数点演算のIEC60559:1989(IEEE 754)標準に準拠しています。

およびC#言語仕様:

およびタイプは、32ビット単精度および64ビット倍精度IEEE754形式を使用して表されます[... float]double

以降:

積は、IEEE754算術の規則に従って計算されます。

floatタイプとその乗算がIEEE754に準拠しているという印象を簡単に得ることができます。

乗算が明確に定義されているのはIEEE754の一部です。つまり、2つのインスタンスがある場合、それらの「正しい」製品はfloat1つだけ存在するということです。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(つまりab)とそれらの製品を考慮します。最後の4つの書き込み行は興味深いものです。Debug x86(左)、Release x86(中央)、およびx64(右)でコンパイルした後、64ビットマシンでこれを実行した場合の出力は次のとおりです。

デバッグx86(左)、リリースx86(中央)、およびx64(右)

float単純な操作の結果は、ビルド構成に依存すると結論付けます。

後の最初の行は、2つのs"case .58"が等しいかどうかの簡単なチェックです。floatビルドモードから独立していると期待していますが、そうではありません。float次の2行は、aをにキャストするために何も変更しないため、同一であると予想されますfloat。しかし、そうではありません。また、製品をそれ自体"True↩ True"と比較しているので、彼らが読むことを期待しています。a*b出力の最後の行はビルド構成から独立していると予想されますが、そうではありません。

正しい製品が何であるかを理解するために、手動で計算します。0.58a)のバイナリ表現は次のとおりです。

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より小さく、より大きくなります。59f0.60f * 100f60f

したがって、x64バージョンのコードが正しく機能しているように見えます(上の図の右端の出力ウィンドウ)。

注:この質問の読者のいずれかが古い32ビットCPUを使用している場合、上記のプログラムの出力がアーキテクチャーでどのようになっているのかを聞くのは興味深いことです。

そして今、質問のために:

  1. 上記はバグですか?
  2. これがバグではない場合、C#仕様では、ランタイムがfloat余分な精度で乗算を実行し、その精度を再び取り除くために「忘れる」ことを選択できると言っていますか?
  3. float式を型にキャストすると、どのようにfloat何かを変えることができますか?
  4. たとえば、一時的なローカル変数にを引き出すことによって式を2つの式に分割するなど、一見無害に見える操作が、(a*b)数学的に(IEEEによる)同等である必要があるときに動作を変更するのは問題ではありませんか?floatプログラマーは、ランタイムが「人工的な」追加(64ビット)精度で保持することを選択したかどうかを事前に知ることができますか?
  5. リリースモードでのコンパイルによる「最適化」で演算を変更できるのはなぜですか?

(これは、4.0バージョンの.NET Frameworkで行われました。)

4

1 に答える 1

8

私はあなたの算術をチェックしていませんが、私は確かに以前に同様の結果を見ました。デバッグモードが違いを生むだけでなく、ローカル変数とインスタンス変数を割り当てることも違いを生む可能性があります。これは、C#4仕様のセクション4.1.6に従って正当です。

浮動小数点演算は、演算の結果タイプよりも高い精度で実行できます。たとえば、一部のハードウェアアーキテクチャは、doubleタイプよりも範囲と精度が高い「extended」または「longdouble」浮動小数点型をサポートし、この高精度型を使用してすべての浮動小数点演算を暗黙的に実行します。このようなハードウェアアーキテクチャは、パフォーマンスに過度のコストがかかる場合にのみ、浮動小数点演算の精度を下げることができます。C#を使用すると、パフォーマンスと精度を失うための実装が必要になるのではなく、すべての浮動小数点演算に高精度の型を使用できます。より正確な結果を提供する以外に、これが測定可能な効果をもたらすことはめったにありません。[...]

それがここで起こっているのかどうかは定かではありませんが、驚くことではありません。

于 2012-09-28T17:30:47.357 に答える