Выражение C # Float: странное поведение при приведении результата float к int

128

У меня есть такой простой код:

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

Baalrukh
источник
9
Я предполагаю, что одна из операций была выполнена с одинарной точностью, а другая - с двойной точностью. Одно из них вернуло значение чуть меньше 62, следовательно, при усечении до целого числа получилось 61.
Гейб
2
Это типичные проблемы точности с плавающей запятой.
TJHeuvel
3
Попытка этого на .Net / WinXP, .Net / Win7, Mono / Ubuntu и Mono / OSX даст ваши результаты для обеих версий Windows, но 62 для скорости1 и скорости2 в обеих версиях Mono. Спасибо @BoltClock
Ойген Рик
6
Мистер Липперт ... вы здесь ??
vc 74
6
Оценщик константных выражений компилятора здесь не выигрывает. Очевидно, что это усечение 6.2f в первом выражении, у него нет точного представления в базе 2, поэтому в итоге получается 6.199999. Но не делает этого во втором выражении, вероятно, из-за того, что каким-то образом удалось сохранить его с двойной точностью. В остальном это обычное дело, согласованность с плавающей запятой никогда не является проблемой. Это не будет исправлено, вы знаете обходной путь.
Hans Passant

Ответы:

168

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

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

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

Раймонд Чен
источник
6
Я не уверен, что это действительно ответ на вопрос. Почему (int)(6.2f * 10)берется doubleзначение, как fуказано float? Я думаю, что главный вопрос (до сих пор без ответа) здесь.
ken2k
1
Я думаю, что это делает компилятор, так как это float literal * int literal, компилятор решил, что он может использовать лучший числовой тип, а для сохранения точности он пошел на double (возможно). (также объяснило бы, что IL такой же)
Джордж Дакетт
5
Хорошая точка зрения. Типа на 6.2f * 10самом деле floatнет double. Я думаю, что компилятор оптимизирует промежуточное звено, как разрешено последним абзацем 11.1.6 .
Raymond Chen
3
У него такое же значение (61.99999809265137). Разница в том, как значение превращается в целое число. В одном случае он переходит непосредственно в целое число, а в другом - floatсначала выполняется преобразование.
Raymond Chen
38
Ответ Раймонда здесь, конечно, совершенно правильный. Я отмечаю, что компилятор C # и jit-компилятор могут использовать большую точность в любое время и делать это непоследовательно . Фактически, они именно так и поступают. Этот вопрос поднимался десятки раз в StackOverflow; см. недавний пример на stackoverflow.com/questions/8795550/… .
Эрик Липперт
11

Описание

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

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

Образец

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

Больше информации

dknaack
источник
5

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

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.2fреальное представление с плавающей точкой точности является в 6.1999999то время 62f, вероятно , что - то подобное 62.00000001. (int)приведение всегда обрезает десятичное значение , поэтому вы получаете такое поведение.

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

Между
источник
Приведение к intдесятичному значению усекает, оно не округляется.
Джим Д'Анджело
@ Джеймс Д'Анджело: Извините, английский не является моим основным языком. Не знал точного слова, поэтому я определил поведение как «округление в меньшую сторону при работе с положительными числами», что в основном описывает то же поведение. Но да, точка взята, усечение - точное слово для этого.
Между
нет проблем, это просто симантика, но может вызвать проблемы, если кто-то начнет думать 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 
Rodji
источник
0

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

Массимо Зербини
источник
-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
Вы можете только разбирать строки, я думаю, вы имеете в виду, почему бы вам не использовать System.Convert
vc 74