Почему при вводе числа 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# (особенно верхний ответ).
Комментариев нет:
Отправить комментарий