Страницы

Поиск по вопросам

вторник, 30 октября 2018 г.

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

Почему при вводе числа 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));


Ответ

Дело в том, что 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.

А что нужно делать? Есть несколько вариантов.
Вместо неустойчивого к мелким ошибкам отбрасывания дробной части
(int)(engine.power * 1000)
использовать гораздо более здравое округление
(int)Math.Round(engine.power * 1000) Если вам похожие проблемы встречаются часто, имеет смысл перейти на тип данных 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# (особенно верхний ответ).

Комментариев нет:

Отправить комментарий