Получение неправильного ответа

257
23 мая 2017, 00:04

Почему при вводе числа 2 в e_power_enter программа выводит в MessageBox ответ 19, а не 20? (engine.power типа float)

int k;
engine.power = float.Parse(e_power_enter.Text) / 100;            
k = (int)(engine.power * 1000);
MessageBox.Show(Convert.ToString(k));
Answer 1

Дело в том, что float/double в C# (а также в Java, С++ и т. д.) — двоичные дроби. Они представлены внутри как набор из целого числа (мантиссы) и степени двойки (порядка). Число 2 может быть представлено в виде двоичной дроби, а вот 0.02 — нет, т. к. оно равно 1/50 (а в знаменателе не только степени двойки). Поэтому число 0.02 не может быть точно представлено в виде float.

Что же содержится в переменной engine.power? Там содержится двоичная дробь, наиболее близкая к 0.02. Проверим это таким кодом:

float enginepower = 2f / 100;
Console.WriteLine(enginepower * 100 - 2);

Он выдаёт не 0, как можно было бы ожидать, а -4,470348E-08 (на моей машине).

Это значит, что значение engine.power реально немного меньше двух. При умножении на 1000 результат будет немного меньше 20, приведение к int отбрасывает дробную часть, и результат получается равным 19.

А что нужно делать? Есть несколько вариантов.

  1. Вместо неустойчивого к мелким ошибкам отбрасывания дробной части

    (int)(engine.power * 1000)
    

    использовать гораздо более здравое округление:

    (int)Math.Round(engine.power * 1000)
    
  2. Если вам похожие проблемы встречаются часто, имеет смысл перейти на тип данных decimal, в котором числа внутри хранятся как десятичные, а не двоичные дроби. Учтите, что операции с этим типом данных медленнее, т. к. нету нативной поддержки процессорами.

Продвинутое расследование, с копанием в ассемблерном выхлопе и спецификации.

На моей машине вот такой код:

float f = 0.02f;
float ff = f * 1000;
int k = (int)(ff);

вычисляет в k значение 20, а вот такой:

float f = 0.02f;
int k = (int)(f * 1000);

— 19. (Это в Debug-режиме; в Release-режиме обе версии текста производят один и тот же ассемблерный код и одинаковый результат — 19.) Расследую, почему так.

Произведённый JIT ассемблерный код такой:

            ; float f = 0.02f;
mov         dword ptr [ebp-40h],3CA3D70Ah   ; 32bit f = 0.02f
            ; float ff = f * 1000;
fld         dword ptr [ebp-40h]             ; extend f to 80bit prec and push
fmul        dword ptr ds:[1453D00h]         ; multiply by 32bit 1000f
fstp        dword ptr [ebp-44h]             ; pop and convert 80bit result to 32 bit -> ff
            ; int k = (int)(ff);
fld         dword ptr [ebp-44h]             ; extend ff to 80bit prec and push
fstp        qword ptr [ebp-50h]             ; pop and convert to 64bit -> double temp
movsd       xmm0,mmword ptr [ebp-50h]       ; extend temp to 128bit and copy to xmm0
cvttsd2si   eax,xmm0                        ; truncate to 32bit int eax
mov         dword ptr [ebp-48h],eax         ; store to k

и

            ; float f = 0.02f;
mov         dword ptr [ebp-40h],3CA3D70Ah   ; 32bit f = 0.02f
            ; int k = (int)(f * 1000);
fld         dword ptr [ebp-40h]             ; extend f to 80bit prec and push
fmul        dword ptr ds:[1393CF4h]         ; multiply by 32bit 1000f
fstp        qword ptr [ebp-4Ch]             ; pop and convert to 64bit -> double temp
movsd       xmm0,mmword ptr [ebp-4Ch]       ; extend temp to 128bit and copy to xmm0
cvttsd2si   eax,xmm0                        ; truncate to 32bit int eax
mov         dword ptr [ebp-44h],eax         ; store to k

Это можно условно записать так:

float32 f = 2f / 100;
float80 r1 = f;
r1 *= float32(1000f);
float32 ff = r1;
r1 = ff;              // <-- тут потеря точности
float64 temp = r1;
float128 r2 = temp;
int32 k = (int32)r2;

и

float32 f = 2f / 100;
float80 r1 = f;
r1 *= float32(1000f);
float64 temp = r1;
float128 r2 = temp;
int32 k = (int32)r2;

Разница, как мы видим, в том, что для вычисления промежуточного результата значение f * 1000 обрезается до 32-битной точности, а потом загружается назад в 80-битный регистр, а оттуда через 64-битное значение загружается в XMM-регистр. Во втором варианте кода сохранение промежуточного результата отсутствует, код не обрезает значение, и до 64 бит обрезается более точное 80-битное значение, что и приводит к разнице в результате.

Явное разрешение на такие разные пути вычисления есть в спецификации языка, §4.1.6 Floating Point Types:

Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an “extended” or “long double” floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type.

Ещё по теме: Strange behavior when casting a float to int in C# (особенно верхний ответ).

READ ALSO
&#39;System.Data.Entity.Core.EntityCommandExecutionException&#39; occurred in mscorlib.dll. ASP.NET MVC 5

'System.Data.Entity.Core.EntityCommandExecutionException' occurred in mscorlib.dll. ASP.NET MVC 5

Скачал пример интернет-магагзина SportStore, который создан по книге ASPNET MVC 5 Freeman

302
Перенос строки в UrlEncode

Перенос строки в UrlEncode

Добрый вечерТакая проблема, есть документ который читаю построчно и заношу в массив

472
Как объединить две формы в программе?

Как объединить две формы в программе?

Form1 это сама программа, а Form2 это тоже самое что и Form1 только сама форма меньше и не имеет всех кнопок с Form1Это что-то вроде музыкального плеера,...

334
Доступ к файлу через приложение

Доступ к файлу через приложение

Доброе время сутокСкажите пожалуйста возможно ли такое реализовать: когда пользователь работает с файлом через приложение(в моем случае...

244