C # выражение Float: странное поведение при приведении результата float к int - PullRequest
125 голосов
/ 18 января 2012

У меня есть следующий простой код:

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, с теми же результатами.

Ответы [ 7 ]

166 голосов
/ 18 января 2012

Прежде всего, я предполагаю, что вы знаете, что 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, что на лучше , чем неявное преобразование в double.

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

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

11 голосов
/ 18 января 2012

Описание

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

Проверьте Джон Скитс DoubleConverter класс. С помощью этого класса вы можете реально визуализировать значение с плавающей точкой в ​​виде строки. Double и float оба являются плавающими числами , десятичными не являются (это число с фиксированной запятой).

Пример

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

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

4 голосов
/ 18 января 2012

Посмотрите на ИЛ:

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 голос
/ 04 декабря 2013

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

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

EDIT : Согласно комментариям я перефразировал поведение intприведение к гораздо более точному определению.

0 голосов
/ 18 января 2012

Single содержит только 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

0 голосов
/ 18 января 2012

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

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

...