Странное поведение при приведении 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
Вот почему вы получаете разные значения!