C#Float式:結果のfloatをintにキャストするときの奇妙な動作


128

次の簡単なコードがあります:

int speed1 = (int)(6.2f * 10);
float tmp = 6.2f * 10;
int speed2 = (int)tmp;

speed1speed2同じ値でなければなりませんが、実際には次のようになっています。

speed1 = 61
speed2 = 62

キャストではなくMath.Roundを使用する必要があることはわかっていますが、値が異なる理由を理解したいと思います。

生成されたバイトコードを見ましたが、ストアとロードを除いて、オペコードは同じです。

私はjavaでも同じコードを試しましたが、62と62を正しく取得しました。

誰かがこれを説明できますか?

編集: 実際のコードでは、直接6.2f * 10ではなく、関数呼び出し*定数です。私は次のバイトコードを持っています:

のためにspeed1

IL_01b3:  ldloc.s    V_8
IL_01b5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ba:  ldc.r4     10.
IL_01bf:  mul
IL_01c0:  conv.i4
IL_01c1:  stloc.s    V_9

のためにspeed2

IL_01c3:  ldloc.s    V_8
IL_01c5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ca:  ldc.r4     10.
IL_01cf:  mul
IL_01d0:  stloc.s    V_10
IL_01d2:  ldloc.s    V_10
IL_01d4:  conv.i4
IL_01d5:  stloc.s    V_11

オペランドは浮動小数点数であり、唯一の違いはであることがわかりますstloc/ldloc

仮想マシンについては、Mono / Win7、Mono / MacOS、.NET / Windowsで試したところ、同じ結果でした。


9
私の推測では、演算の1つは単精度で行われ、もう1つは倍精度で行われました。それらの1つが62をわずかに下回る値を返したため、整数に切り捨てると61になります。
Gabe 2012年

2
これらは、典型的な浮動小数点精度の問題です。
TJHeuvel 2012年

3
.Net / WinXP、.Net / Win7、Mono / Ubuntu、Mono / OSXでこれを試すと、両方のWindowsバージョンで結果が得られますが、両方のMonoバージョンでspeed1とspeed2で62になります。ありがとう@BoltClock
Eugen Rieck 2012年

6
リッペルト氏...あなたの周り?
vc 74 2012

6
コンパイラーの定数式エバリュエーターは、ここで賞を獲得していません。明らかに、最初の式で6.2fを切り捨てていますが、基数2では正確な表現がないため、最終的に6.199999になります。しかし、おそらくなんとかして倍精度に保つことを管理することによって、2番目の式ではそうしません。これは他の点では並ぶものであり、浮動小数点の一貫性は問題になりません。これは修正される予定はありません。回避策はわかっています。
Hans Passant 2012年

回答:


168

まず、6.2f * 10浮動小数点の丸めのためにそれが正確に62 ではないことを知っていると想定します(実際にはdoubleとして表される場合、値は61.99999809265137です)。あなたの質問は、2つの一見同じ計算が誤った値になる理由についてのみであると仮定します。

答えは、の場合(int)(6.2f * 10)double値61.99999809265137を取得し、それを整数に切り捨てているため、61になります。

の場合float f = 6.2f * 10、double値61.99999809265137を取り、最も近い62に丸めfloatます。それfloatを整数に切り捨てると、結果は62になります。

演習:次の一連の操作の結果を説明してください。

double d = 6.2f * 10;
int tmp2 = (int)d;
// evaluate tmp2

更新:としては、コメントで指摘は、式が6.2f * 10正式にあるfloat2番目のパラメータが暗黙の型変換を持つためにfloatこれは、より良いへの暗黙的な変換よりdouble

実際の問題は、コンパイラーが正式な型(セクション11.2.2)よりも高い精度の中間体を使用することが許可されている(ただし必須ではない)ことです。そのため、システムによって動作が異なります。式(int)(6.2f * 10)では、コンパイラーは、6.2f * 10に変換する前に高精度の中間形式で値を保持するオプションを持っていますint。一致する場合、結果は61です。一致しない場合、結果は62です。

2番目の例では、への明示的な割り当てによりfloat、整数への変換前に強制的に丸めが行われます。


6
これが実際に質問に答えているかどうかはわかりません。なぜ(int)(6.2f * 10)取っdoubleた値を、としてf指定し、それですかfloat?要点(未回答)はここにあると思います。
ken2k 2012年

1
float型のリテラル* int型のリテラルであるため、コンパイラは最適な数値型を自由に使用できるようにし、精度を節約するために倍精度浮動小数点数(おそらく)を使用しないと判断しました。(ILが同じであることも説明します)
George Duckett

5
いい視点ね。のタイプ6.2f * 10は、実際floatではありませんdouble11.1.6の最後の段落で許可されているように、コンパイラは中間体を最適化していると思います。
Raymond Chen

3
同じ値です(値は61.99999809265137)。違いは、値が整数になるための経路です。ある場合には直接整数になり、別の場合にはfloat最初に変換が行われます。
Raymond Chen

38
ここでのレイモンドの答えはもちろん完全に正しいです。私は、C#コンパイラとjitコンパイラのどちらも、いつでもより高い精度を使用することが許可されており一貫性のない方法で使用していることに注意してください。そして実際、彼らはまさにそれをします。この質問はStackOverflowで何十回も出てきました。最近の例については、stackoverflow.com / questions / 8795550 /…を参照してください。
Eric Lippert、2012年

11

説明

浮動小数点数はほとんど正確ではありません。6.2fのようなものです6.1999998...。これをintにキャストすると、切り捨てられ、この* 10は61になります。

Jon Skeets DoubleConverterクラスをご覧ください。このクラスを使用すると、浮動小数点数の値を文字列として実際に視覚化できます。Doubleおよびfloatはどちらも浮動小数点数ですが、10進数はそうではありません(固定小数点数です)。

サンプル

DoubleConverter.ToExactString((6.2f * 10))
// output 61.9999980926513671875

詳しくは


5

ILを見てください。

IL_0000:  ldc.i4.s    3D              // speed1 = 61
IL_0002:  stloc.0
IL_0003:  ldc.r4      00 00 78 42     // tmp = 62.0f
IL_0008:  stloc.1
IL_0009:  ldloc.1
IL_000A:  conv.i4
IL_000B:  stloc.2

コンパイラーは、コンパイル時の定数式を定数値に削減しますint。定数をに変換するとき、ある時点で間違った近似をすると思います。の場合speed2、この変換はコンパイラではなくCLRによって行われ、それらは異なるルールを適用しているようです...


1

私の推測では、6.2ffloat精度での実際の表現は6.1999999while 62fはおそらくに似てい62.00000001ます。(int)キャストでは常に10進数値切り捨てられるため、その動作が得られます。

編集:コメントによると、intキャストの動作をより正確な定義に言い換えました。


int10進数値を切り捨ててキャストすると、丸められません。
ジムダンジェロ

@James D'Angelo:申し訳ありませんが、英語は私の第一言語ではありません。正確な単語がわからなかったので、基本的に同じ動作を表す「正の数を処理するときの切り捨て」と動作を定義しました。しかし、そうです、要点は、切り捨てが正確な言葉です。
2012年

問題ありません、それは単なる意味論ですが、誰かが考え始めると問題を引き起こす可能性がありますfloat-> int丸めが含まれます。= D
ジムダンジェロ

1

このコードをコンパイルして逆アセンブルしました(Win7 / .NET 4.0上)。コンパイラは浮動定数式をdoubleとして評価すると思います。

int speed1 = (int)(6.2f * 10);
   mov         dword ptr [rbp+8],3Dh       //result is precalculated (61)

float tmp = 6.2f * 10;
   movss       xmm0,dword ptr [000004E8h]  //precalculated (float format, xmm0=0x42780000 (62.0))
   movss       dword ptr [rbp+0Ch],xmm0 

int speed2 = (int)tmp;
   cvttss2si   eax,dword ptr [rbp+0Ch]     //instrunction converts float to Int32 (eax=62)
   mov         dword ptr [rbp+10h],eax 

0

Single必要なのは7桁のみでありInt32、コンパイラにキャストするときに、すべての浮動小数点桁が切り捨てられます。変換中に、1つ以上の有効数字が失われる可能性があります。

Int32 speed0 = (Int32)(6.2f * 100000000); 

619999980の結果を返すため、(Int32)(6.2f * 10)は61を返します。

2つのSingleが乗算される場合は異なります。その場合、切り捨て演算はなく、近似のみが行われます。

http://msdn.microsoft.com/en-us/library/system.single.aspxを参照してください


-4

int構文解析ではなく型キャストを行う理由はありますか?

int speed1 = (int)(6.2f * 10)

次に読む

int speed1 = Int.Parse((6.2f * 10).ToString()); 

違いは、おそらく丸めに関係しています。キャストするdoubleと、おそらく61.78426のようになります。

次の出力に注意してください

int speed1 = (int)(6.2f * 10);//61
double speed2 = (6.2f * 10);//61.9999980926514

そのため、異なる値を取得しています。


1
Int.Parse文字列をパラメータとして受け取ります。
ken2k 2012年

あなただけの解析文字列は、私はあなたがSystem.Convert使用していない、なぜあなたが意味を推測することができます
VCを74
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.