Выдача результата для float в методе, возвращающем результат изменения float

Почему этот код печатает False в .NET 4? Кажется, что неожиданное поведение вызвано явным литом.

Я бы хотел, чтобы ответ за пределами "с плавающей точкой был неточным" или "не делайте этого".

float a(float x, float y)
{
  return ( x * y );
}

float b(float x, float y)
{
  return (float)( x * y );
}

void Main()
{
  Console.WriteLine( a( 10f, 1f/10f ) == b( 10f, 1f/10f ) );
}

PS: Этот код появился из unit test, а не для кода выпуска.
PPS: unit test проходил в .NET 3.5, но теперь он не работает после обновления до .NET 4.

Ответ 1

Комментарий Дэвида правильный, но недостаточно сильный. Нет никакой гарантии, что выполнение этого вычисления дважды в одной и той же программе даст одинаковые результаты.

Спецификация С# предельно ясна в этом пункте:


Операции с плавающей запятой могут выполняться с большей точностью, чем тип результата операции. Например, некоторые аппаратные архитектуры поддерживают "расширенный" или "длинный двойной" тип с плавающей запятой с большим диапазоном и точностью, чем двойной тип, и неявно выполняют все операции с плавающей запятой, используя этот более высокий тип точности. Только при чрезмерной стоимости производительности могут быть созданы такие аппаратные архитектуры для выполнения операций с плавающей запятой с меньшей точностью и вместо того, чтобы требовать, чтобы реализация лишилась производительности и точности, С# позволяет использовать более высокий тип точности для всех операций с плавающей запятой, Помимо предоставления более точных результатов, это редко имеет какие-либо измеримые эффекты. Однако в выражениях формы x * y / z, где умножение дает результат, выходящий за пределы двойного диапазона, но последующее деление возвращает временный результат обратно в двойной диапазон, тот факт, что выражение оценивается в формате более высокого диапазона может привести к получению конечного результата вместо бесконечности.


Компилятор С#, джиттер и среда выполнения имеют широкую широту, чтобы дать вам более точные результаты, чем это требуется в спецификации, в любое время по прихоти - им не требуется выбирать, чтобы делать это последовательно и в факт, что они этого не делают.

Если вам это не нравится, не используйте двоичные числа с плавающей запятой; либо используйте десятичные знаки или произвольные прецизионные рациональности.

Я не понимаю, почему кастинг для float в методе, который возвращает float, делает разницу в нем

Отличная точка.

Ваша примерная программа показывает, как небольшие изменения могут вызвать большие эффекты. Вы заметили, что в некоторой версии среды выполнения приведение в float явно дает другой результат, чем не делает этого. Когда вы явно используете float, компилятор С# дает подсказку для среды выполнения, чтобы сказать: "Извлеките эту вещь из режима сверхвысокой точности, если вы используете эту оптимизацию". Как отмечается в спецификации, это потенциальная стоимость исполнения.

То, что это происходит, округляется до "правильного ответа", это просто счастливая случайность; правильный ответ получается потому, что в этом случае потеря точности была потеряна в правильном направлении.

Как отличается .net 4?

Вы спрашиваете, в чем разница между 3.5 и 4.0 runtimes; разница в том, что в 4.0, дрожание решает перейти к более высокой точности в вашем конкретном случае, а 3,5-джиттер не хочет этого делать. Это не означает, что эта ситуация невозможна в 3.5; это было возможно в каждой версии среды выполнения и каждой версии компилятора С#. Вы только что столкнулись с ситуацией, когда на вашей машине они отличаются деталями. Но джиттеру всегда позволяли делать эту оптимизацию, и всегда делал это по своей прихоти.

Компилятор С# также полностью в пределах своих прав, чтобы выбрать аналогичные оптимизации при вычислении постоянных поплавков во время компиляции. Два, казалось бы, идентичные вычисления в константах могут иметь разные результаты в зависимости от деталей состояния выполнения компилятора.

В более общем плане, ваше ожидание того, что числа с плавающей запятой должны иметь алгебраические свойства действительных чисел, полностью не соответствует действительности; они не обладают этими алгебраическими свойствами. Операции с плавающей точкой даже не являются ассоциативными; они, конечно, не подчиняются законам мультипликативных инверсий, как вы, кажется, ожидаете их. Числа с плавающей запятой являются лишь приближением действительной арифметики; приближение, которое достаточно близко, например, для моделирования физической системы или вычисления сводных статистических данных или некоторой такой вещи.

Ответ 2

У меня нет компилятора Microsoft прямо сейчас, и Mono не имеет такого эффекта. Насколько я знаю GCC 4.3+ использует gmp и mpfr для вычисления некоторого материала во время компиляции. Компилятор С# может сделать то же самое для не виртуальных, статических или частных методов в одной сборке. Явное литье может помешать такой оптимизации (но я не вижу причин, по которым у нее не может быть такого же поведения). То есть он может встроить вычисление константного выражения на некоторый уровень (для b() он может быть, например, до приведения).

GCC также имеет оптимизацию, которая повышает эффективность работы до более высокой точности, если это имеет смысл.

Поэтому я бы рассмотрел обе оптимизации как потенциальную причину. Но для них обоих я не вижу причин, по которым явное литье результата может иметь некоторое дополнительное значение, например "быть ближе к стандарту".