Почему арифметика с плавающей запятой в С# неточна?

Почему следующая программа печатает то, что она печатает?

class Program
{
    static void Main(string[] args)
    {
        float f1 = 0.09f*100f;
        float f2 = 0.09f*99.999999f;

        Console.WriteLine(f1 > f2);
    }
}

Выход

false

Ответ 2

Главное, что это не просто .Net: это ограничение базовая система, которую большинство языков будет использовать для представления float в памяти. Точность только до сих пор.

Вы также можете немного повеселиться с относительно простыми цифрами, если учесть, что он даже не основывается на десяти. 0,1, например, является повторяющимся десятичным числом, представленным в двоичном формате.

Ответ 3

В этом конкретном случае его, потому что .09 и .999999 не могут быть представлены с точной точностью в двоичном формате (аналогично, 1/3 не может быть представлена ​​точной точностью в десятичной форме). Например, 0.11111111111111111111101111 base 2 является 0.999998986721038818359375 базой 10. Добавление 1 к предыдущему двоичному значению, 0.1111111111111111111111 base 2 - 0.99999904632568359375 base 10. Существует не двоичное значение точно для 0.999999. Точность плавающей точки также ограничена пространством, выделенным для хранения экспоненты и дробной части мантиссы. Кроме того, как и целые типы, плавающая точка может переполнять свой диапазон, хотя его диапазон больше целых диапазонов.

Запуск этого бита кода С++ в отладчике Xcode,

float myFloat = 0.1;

показывает, что myFloat получает значение 0.100000001. Он отключен на 0.000000001. Не много, но если вычисление имеет несколько арифметических операций, неточность может быть усугублена.

imho очень хорошее объяснение с плавающей точкой находится в главе 14 "Введение в компьютерную организацию" с языком сборки x86-64 и GNU/Linux Боба Планца из Калифорнийского государственного университета в Сономе (на пенсии) http://bob.cs.sonoma.edu/getting_book.html. В основе этой главы лежит следующее.

Плавающая точка похожа на научную нотацию, где значение хранится как смешанное число, большее или равное 1,0 и менее 2,0 (мантисса), разное число до некоторой степени (экспонента). Плавающая точка использует базовую 2, а не базу 10, но в простой модели Plantz дает, он использует базу 10 для ясности. Представьте себе систему, в которой для мантиссы используются два положения хранения, одна позиция используется для знака показателя * (0, представляющего + и 1, представляющего -), а для экспонента используется одна позиция. Теперь добавьте 0.93 и 0.91. Ответ 1,8, а не 1,84.

9311 представляет собой 0,93 или 9,3 раза 10 до -1.

9111 представляет собой 0,91 или 9,1 раза 10 до -1.

Точный ответ равен 1,84, или 1,84 раза 10 до 0, что было бы 18400, если бы у нас было 5 позиций, но, имея только четыре позиции, ответ равен 1800, или 1,8 раза 10 до нуля или 1,8. Конечно, типы данных с плавающей запятой могут использовать более четырех позиций хранения, но количество позиций по-прежнему ограничено.

Не только точность ограничена пространством, но "точное представление дробных значений в двоичном выражении ограничено суммами обратных степеней двух". (Plantz, op. Cit.).

0.11100110 (двоичный) = 0,89843750 (десятичный)

0.11100111 (двоичный) = 0.90234375 (десятичный)

Точного представления десятичного десятичного числа в двоичном выражении нет. Даже перенос фракции из большего количества мест не работает, так как вы получаете повторение 1100 навсегда справа.

Начальные программисты часто видят арифметику с плавающей запятой, как больше точнее, чем целое число. Это правда, что даже добавление двух очень больших целые числа могут вызвать переполнение. Умножение делает его еще более вероятным что результат будет очень большим и, следовательно, переполнением. И когда используется с двумя целыми числами, оператор/в C/С++ вызывает дробную часть потеряться. Однако... представления с плавающей запятой имеют свои собственные набор неточностей. (Plantz, op. Cit.)

* В плавающей точке представлены как знак числа, так и знак экспоненты.