Странное поведение при приведении float в 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, а вызов функции * константа. У меня есть следующий байт-код:

для скорости 1:

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

для скорости 2:

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

мы можем видеть, что операнды являются float и что единственное различие заключается в том, что stloc/ldloc

Что касается виртуальной машины, я попытался использовать Mono/Win7, Mono/MacOS и .NET/Windows с теми же результатами

Ответ 1

Прежде всего, я предполагаю, что вы знаете, что 6.2f * 10 не является точно 62 из-за округления с плавающей запятой (это фактически значение 61.99999809265137, если оно выражено как double), и что ваш вопрос связан только с тем, почему два, казалось бы, одинаковые вычисления приводят к неправильному значению.

Ответ заключается в том, что в случае (int)(6.2f * 10) вы берете значение double 61.99999809265137 и обрезаете его до целого числа, которое дает 61.

В случае float f = 6.2f * 10 вы берете двойное значение 61.99999809265137 и округляете до ближайшего float, то есть 62. Затем вы усекаете это число float на целое число, а результат равен 62.

Упражнение: Объясните результаты следующей последовательности операций.

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

Обновление: как отмечено в комментариях, выражение 6.2f * 10 формально является float, поскольку второй параметр имеет неявное преобразование в float, которое better, чем неявное преобразование в double.

Фактическая проблема заключается в том, что компилятор разрешен (но не обязательно) для использования промежуточного элемента, который более высокой точности, чем формальный тип. Вот почему вы видите другое поведение в разных системах: в выражении (int)(6.2f * 10) у компилятора есть возможность сохранить значение 6.2f * 10 в промежуточной форме высокой точности перед преобразованием в int. Если это так, то результат равен 61. Если это не так, то результат равен 62.

Во втором примере явное назначение float заставляет округление проходить до преобразования в целое число.

Ответ 2

Описание

Плавающие числа редко точны. 6.2f - это что-то вроде 6.1999998.... Если вы примените это к int, он усечет его, и это * 10 приведет к 61.

Ознакомьтесь с классом Jon Skeets DoubleConverter. С помощью этого класса вы можете реально визуализировать значение плавающего числа как строки. Double и float являются плавающими числами, decimal не является (это номер фиксированной точки).

Пример

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

Дополнительная информация

Ответ 3

Посмотрите на 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, и они, похоже, применяют разные правила...

Ответ 4

Я предполагаю, что реальное представление 6.2f с точностью до float 6.1999999, а 62f, вероятно, похоже на 62.00000001. (int) литье всегда сокращает десятичное значение, поэтому вы получаете это поведение.

РЕДАКТИРОВАТЬ. Согласно комментариям, я перефразировал поведение приведения int к гораздо более точному определению.

Ответ 5

Single mantains только 7 цифр, и при передаче его в Int32 компилятор усекает все числа с плавающей запятой. Во время преобразования одна или несколько значимых цифр могут быть потеряны.

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

дает результат 619999980, поэтому (Int32) (6.2f * 10) дает 61.

Это разное, когда два Single умножаются, в этом случае нет операции усечения, а только аппроксимация.

См. http://msdn.microsoft.com/en-us/library/system.single.aspx

Ответ 6

Я скомпилировал и разобрал этот код (на 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 

Ответ 7

Есть ли причина, по которой вы выполняете литье типов до 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

Вот почему вы получаете разные значения!