Ошибка округления .NET в ToString(f2)

Здравствуйте, у меня есть этот код на С#:

float n = 2.99499989f;
MessageBox.Show("n = " + n.ToString("f2", CultureInfo.InvariantCulture));

И этот код на С++:

float n = 2.99499989f;
printf("n = %.2f", n);

Первый выдает 3,00.
Второй выдает 2,99.

Я понятия не имею, почему это происходит.

Обновление:

Я также пробовал NSLog на Objective-C, и результат был 2,99.

Мне нужно было исправить это быстро, поэтому я использовал следующий метод:

float n = 2.99499989f;
float round = (float)Math.Round(n, 2);
MessageBox.Show("round = " + round.ToString(CultureInfo.InvariantCulture));

Этот код показывает 2,99, но вычисляет округление с двойной точностью. Я не могу найти Math.RoundF.


person Filip Kunc    schedule 18.01.2010    source источник
comment
Возможный дубликат Как printf и co различают float и двойной   -  person    schedule 17.04.2018


Ответы (6)


(Я понимаю, что это старый вопрос, но я отвечаю на него, поскольку сам понимаю некоторые вещи...)

Мое предположение состоит в том, что «printf» преобразует число с плавающей запятой в двойное перед применением форматирования. Я попытался преобразовать число с плавающей запятой в двойное, а затем посмотрел на результат ToString («f2») двойного значения, который он округлил в противоположном направлении, поскольку значение числа с плавающей запятой было округлено.

Другие поддерживают эту идею о printf:

C автоматически преобразует значения с плавающей запятой в двойные (это стандартное преобразование, выполняемое при вызове функции, которая принимает переменные аргументы, например, int printf...

https://stackoverflow.com/a/7480244/119418

https://stackoverflow.com/a/6395747/119418

Я считаю, что число с плавающей запятой округляется в большую сторону потому, что 2,995F не полностью преобразуется в двоичное число и равно 2,99499989F, а мы ожидаем, что 2,995 будет округлено в большую сторону.

Я считаю, что причина, по которой число с плавающей запятой, скопированное в двойное округление, заключается в том, что 2,995 как двойное ‹> 2,99499989 как двойное, а фактическое двойное значение 2,995 больше, чем это значение (более точное значение ближе к его истинному десятичному значению), и поэтому это значение мы ожидаем округлить вниз.

Может показаться неправильным, что 2,99499989F округляется до 3,00, хотя на первый взгляд оно меньше 2,995, но имейте в виду, что 2,99499989 — это десятичное число, а не число с плавающей запятой, и вы «конвертируете» его в число с плавающей запятой, фактически преобразуя основание- 10 по основанию 2, и на самом деле это 1 и 0, а затем запрашивается округление до числа с основанием 10, что означает, что должно произойти преобразование. Что ж, есть по крайней мере 2 значения с основанием 10, которые я упомянул, которые могут быть преобразованы в это число как число с плавающей запятой, и самое простое из них — 2,995.

person Mafu Josh    schedule 25.10.2013

Использование BitConverter.GetBytes и распечатка полученных фактических байтов показывает, что это не отличие компилятора — в обоих случаях фактическое сохраненное значение с плавающей запятой равно 0x403FAE14, что этот удобный калькулятор говорит мне точное значение

2.99499988555908203125

Таким образом, разница должна заключаться в различном поведении printf и ToString. Большего я не могу сразу сказать.

person AakashM    schedule 18.01.2010

Первый неправильно округляет число.

Думаю, второй выбирает только две цифры после запятой.

Может быть, какие-то проблемы с точностью с плавающей запятой? 2,9949... на самом деле округляется до более чем 2,995?

person Gacek    schedule 18.01.2010

Точность с плавающей запятой составляет 2^{-23} или около 0,0000001192 (относительная ошибка). Смотри, 2,995-2,99499989 = 0,00000011. Относительная ошибка 3,6*10^{-8}.

Если какой-то компилятор считывает все цифры константы 2.99499989f, результатом будет число больше 2,995. Но если другой компилятор читает только 2.994999 (поскольку точность меньше 2^{-23} и последние цифры не важны), то результатом будет число меньше 2,995.

person Alexey Malistov    schedule 18.01.2010
comment
Не уверен, что вы имеете в виду. Как первый компилятор получит 2.99499989f > 2.995? - person jalf; 18.01.2010
comment
Без проблем. Компилятор должен выбрать между 2.9950001239776611328125E0 и 2.99499988555908203125. Только эти значения действительны для IEEE754. - person Alexey Malistov; 18.01.2010

Разве это не связано с тем, что «поплавок» не является «этим» точным?

Проверьте это, когда вы используете десятичную дробь:

        // float
        Console.WriteLine (2.99499989f.ToString ("f2"));
        // The above line outputs 3.00

        // decimal
        Console.WriteLine (2.99499989M.ToString ("f2"));
        // The above line outputs 2.99

Возможно, float не может представлять 2,99499989 или что-то вроде 2,99.
Посмотрите также, что происходит, когда вы делаете это:

// float
Console.WriteLine (2.98499f.ToString ("f2"));
// The above line outputs 2.99
person Frederik Gheysels    schedule 18.01.2010
comment
Что происходит при их использовании? Разве вы не можете распечатать вывод вместе с кодом, чтобы нам не пришлось вставлять его в компилятор, чтобы понять ваш ответ? - person jalf; 18.01.2010

Число 2.99499989f не может быть точно представлено в IEEE754. По-видимому, printf и ToString по-разному обрабатывают эту ситуацию.

person Maurits Rijk    schedule 18.01.2010
comment
Как показывает AakashM, фактическое сохраненное значение равно 2,99499988555908203125, что более чем достаточно для того, чтобы ToString должен дать правильный ответ. - person jalf; 18.01.2010
comment
Конечно, но давайте предположим, что ToString просто добавляет 0,005 к округлению до ближайших двух знаков после запятой. В этом случае вы получите 2.99999989f, представление которого равно 0x40400000. Которых оказывается ровно 3. - person Maurits Rijk; 19.01.2010