web-dev-qa-db-ja.com

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

私は次の簡単なコードを持っています:

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

speed1およびspeed2は同じ値でなければなりませんが、実際には次のようになります。

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で試しましたが、同じ結果になりました。

127
Baalrukh

まず、浮動小数点の丸めにより6.2f * 10が正確に62ではないことを知っていると仮定します(doubleとして表される場合、実際には値61.99999809265137です)。一見同一の計算は間違った値をもたらします。

答えは、(int)(6.2f * 10)の場合、double値61.99999809265137を取得して整数に切り捨てて、61を生成するということです。

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

演習:次の一連の操作の結果を説明します。

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

更新:コメントで述べたように、式6.2f * 10は正式にfloatです。これは、2番目のパラメーターがfloatに暗黙的に変換されるためです。これは better than doubleへの暗黙的な変換。

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

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

167
Raymond Chen

説明

浮動小数点数はめったに正確ではありません。 6.2f6.1999998...。これをintにキャストすると、切り捨てられ、この* 10は61になります。

Jon Skeets DoubleConverterクラスを確認してください。このクラスを使用すると、浮動小数点数の値を文字列として実際に視覚化できます。 Doublefloatは両方とも浮動小数点数であり、10進数ではありません(固定小数点数です)。

サンプル

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

詳しくは

11
dknaack

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によって行われ、異なる規則を適用するようです...

5
Thomas Levesque

このコードをコンパイルして逆アセンブルしました(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 
1
Rodji

私の推測では、6.2f float精度の実数表現は6.1999999 while 62fはおそらく62.00000001(int)常にキャストする10進数値を切り捨てるであるため、この動作が発生します。

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

1
InBetween

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 を参照してください

0
Max Zerbini