C # Вираз поплавця: дивна поведінка під час передачі результату плаває до 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 з однаковими результатами.


9
Я здогадуюсь, що одну з операцій виконували в одноточній, а іншу - в подвійній точності. Один з них повернув значення трохи менше 62, отже, отримавши 61 при обрізанні до цілого числа.
Гейб

2
Це типові питання щодо точності поплавкової точки.
TJHeuvel

3
Пробуючи це на .Net / WinXP, .Net / Win7, Mono / Ubuntu та Mono / OSX, ви отримуєте результати для обох версій Windows, але 62 для speed1 та speed2 в обох версіях Mono. Дякуємо @BoltClock
Eugen Rieck

6
Містер Ліпперт ... Ви навколо ??
vc 74

6
Оцінювач постійного вираження компілятора тут не виграє жодних призів. Зрозуміло, що він обрізає 6.2f в першому виразі, він не має точного подання в базі 2, тому закінчується як 6.199999. Але це не робиться в другому виразі, ймовірно, встигаючи якось утримати його в подвійній точності. Інакше це номінально для курсу, послідовність з плаваючою комою ніколи не є проблемою. Це не вирішиться, ви знаєте, що це вирішило.
Ганс Пасант

Відповіді:


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формально є, 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, компілятор вирішив, що вільно використовувати найкращий числовий тип, а для збереження точності він пішов на подвійну (можливо). (також би пояснив, що ІЛ однаковий)
Джордж Дакетт

5
Гарна думка. Тип 6.2f * 10власне float, ні double. Я думаю, що компілятор оптимізує проміжний рівень, як це дозволено останнім пунктом 11.1.6 .
Реймонд Чен

3
Він має таке ж значення (значення 61,99999809265137). Різниця - шлях, який значення бере на шляху до цілого числа. В одному випадку воно переходить безпосередньо до цілого числа, а в іншому floatспочатку відбувається перетворення.
Реймонд Чен

38
Відповідь Реймонда тут, звичайно, абсолютно правильна. Зауважу, що компілятору C # і компілятору jit дозволяється в будь-який час використовувати більш точну точку , і робити це непослідовно . А насправді вони роблять саме це. Це питання виникало десятки разів на StackOverflow; див. stackoverflow.com/questions/8795550/… для останнього прикладу.
Ерік Ліпперт

11

Опис

Плаваючі числа рідко точні. 6.2fщось подібне 6.1999998.... Якщо ви передасте це до int, він уріже це, і це * 10 приведе до 61.

Ознайомтесь з DoubleConverterкласом Jon Skeets . За допомогою цього класу ви дійсно можете візуалізувати значення плаваючого числа у вигляді рядка. Doubleі floatобидва є плаваючими числами , десяткових немає (це число з фіксованою точкою).

Зразок

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

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


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)кастинг завжди скорочує десяткове значення , тому ви отримуєте таку поведінку.

EDIT : Відповідно до коментарів, я перефразував поведінку intкастингу до набагато більш точного визначення.


Передаваючи в intусікання десяткове значення, воно не округляється.
Jim D'Angelo

@James D'Angelo: Вибачте, англійська мова не є моєю основною мовою. Я не знав точного слова, тому я визначив поведінку як "округлення денаріїв при роботі з позитивними цифрами", що в основному описує ту саму поведінку. Але так, з точки зору, усічення - це саме те слово.
Між

немає проблем, це просто символіка, але може спричинити неприємності, якщо хтось почне думати float-> intпередбачає округлення. = D
Джим Д'Анджело

1

Я скомпілював і розібрав цей код (на Win7 / .NET 4.0). Я думаю, що компілятор оцінює плаваючий постійний вираз як подвійний.

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 

0

Singleзберігає лише 7 цифр, і під час передавання Int32їх компілятору обрізають усі цифри з плаваючою комою. Під час перетворення одна або кілька значущих цифр можуть бути втрачені.

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

дає результат 619999980 так (Int32) (6.2f * 10) дає 61.

Інакше, коли два Одинарних перемножуються, в цьому випадку немає усієї операції, а лише наближення.

Дивіться 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.