Является ли арифметика с плавающей запятой стабильной?

Я знаю, что числа с плавающей запятой имеют точность, а цифры после точности не надежны.

Но что, если уравнение, используемое для вычисления числа, одинаково? могу ли я предположить, что результат будет таким же?

например, у нас есть два числа с плавающей точкой x и y. Можно ли считать результат x/y из машины 1 точно таким же, как результат с машины 2? IE == сравнение вернет true

Ответы

Ответ 1

Но что, если уравнение, используемое для вычисления числа, одинаково? могу ли я предположить, что результат будет таким же?

Нет, не обязательно.

В частности, в некоторых ситуациях JIT разрешено использовать более точное промежуточное представление - например, 80 бит, когда ваши исходные данные равны 64 бит, тогда как в других ситуациях это не будет. Это может привести к появлению разных результатов, если выполняется одно из следующих утверждений:

  • У вас немного другой код, например, с использованием локальной переменной вместо поля, которая может изменить, сохраняется ли значение в регистре или нет. (Это один относительно очевидный пример: есть и другие гораздо более тонкие, которые могут влиять на вещи, такие как наличие блока try в методе...)
  • Вы выполняете работу на другом процессоре (я наблюдал различия между AMD и Intel, могут быть различия между разными процессорами от того же производителя)
  • Вы выполняете различные уровни оптимизации (например, под отладчиком или нет)

В разделе 4.1.6 спецификации спецификации С# 5:

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

Ответ 2

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

С# автоматически обрезает любой поплавок обратно в его каноническое представление 32 или 64 бит при следующих обстоятельствах:

  • Вы добавили избыточное явное выражение: x + y может иметь x и y как более высокие числа, которые затем добавляются. Но (double)((double)x+(double)y) гарантирует, что все будет усечено до 64-битной точности до и после математики.
  • Любое хранилище в поле экземпляра класса, статического поля, элемента массива или разыменованного указателя всегда усекает. (Магазины для локальных жителей, параметры и временные рамки не гарантируются усечением, они могут быть зарегистрированы. Поля структуры могут находиться в кратковременном пуле, который также может быть зарегистрирован.)

Эти гарантии не задаются спецификацией языка, но реализации должны соблюдать эти правила. Реализации Microsoft С# и CLR делают.

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

Жалобы на эту ужасную ситуацию следует адресовать Intel, а не Microsoft; это те, кто разработал чипы, делающие предсказуемую арифметику медленнее.

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

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

Почему этот расчет с плавающей запятой дает разные результаты на разных машинах?

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

(. 1f+.2f ==. 3f)! = (. 1f+.2f).Equals(.3f) Почему?

Принудительное использование плавающей запятой в.NET?

С# XNA Visual Studio: Разница между режимами "выпуска" и "отладки"?

С# - результат несогласованной математической операции по 32-разрядной и 64-разрядной

Ошибка округления в С#: разные результаты на разных ПК

Странное поведение компилятора с поплавковыми литералами vs float variables

Ответ 3

Нет. Результат расчета может различаться для каждого процессора, поскольку реализация арифметики с плавающей запятой может различаться в зависимости от производителя процессора или процессора. Я даже помню ошибку в арифметике с плавающей запятой в некоторых процессорах Intel, которые привили наши расчеты.

И тогда есть разница в том, как код оценивается в JIT-компиляторе.

Ответ 4

Честно говоря, я бы не ожидал, что два места в одной и той же кодовой базе возвратят ту же самую вещь для x/y для тех же x и y - в том же процессе на той же машине; это может зависеть от того, как точно x и y оптимизируются компилятором /JIT - если они зарегистрированы по-разному, они могут иметь разные промежуточные оценки. Многие операции выполняются с использованием большего количества бит, чем вы ожидаете (размер регистра); и именно тогда, когда это становится вынужденным до 64 бит, это может повлиять на результат. Выбор этого "точно когда" может зависеть от того, что еще происходит в окружающем коде.

Ответ 5

Теоретически, в соответствующей системе IEEE 754 одна и та же операция с одинаковыми входными значениями должна давать тот же результат.

Как Википедия резюмирует это:

IEEE 754-1985 допускал множество вариантов реализации (например, кодирование некоторых значений и обнаружение определенных исключений). IEEE 754-2008 укрепил многие из них, но осталось несколько изменений (особенно для двоичных форматов). В предложении воспроизводимости рекомендуется, чтобы языковые стандарты должны были предоставлять средства для записи воспроизводимых программ (т.е. Программ, которые будут давать одинаковый результат во всех реализациях языка) и описывают, что необходимо сделать для достижения воспроизводимых результатов.

Однако, как обычно, теория отличается от практики. Большинство используемых языков программирования, включая С#, строго не соответствуют IEEE 754 и не обязательно предоставляют средство для записи воспроизводимой программы.

Кроме того, современные CPU/FPU делают несколько неудобными для обеспечения строгого соответствия IEEE 754. По умолчанию они будут работать с "расширенной точностью", сохраняя значения с большим количеством бит, чем двойной внутри. Если вам нужна строгая семантика, вам нужно вывести значения из FPU в регистр CPU, проверить и обработать различные исключения FPU, а затем переместить значения обратно между каждой операцией FPU. Из-за этой неловкости строгое соответствие имеет штраф за производительность даже на аппаратном уровне. Стандарт С# выбрал более "неряшливое" требование, чтобы избежать введения штрафа за производительность в более распространенном случае, когда небольшие изменения не являются проблемой.

Ничто из этого часто не является проблемой на практике, поскольку большинство программистов усвоили (неправильную или, по крайней мере, вводящую в заблуждение) идею о том, что математика с плавающей запятой неточна. Кроме того, ошибки, о которых мы говорим здесь, все очень малы, достаточно, чтобы они были затмеваны гораздо более распространенной потерей точности, вызванной преобразованием из десятичной.

Ответ 6

В дополнение к другим ответам, возможно, x/y != x/y даже на одной машине.

Вычисления с плавающей запятой в x86 выполняются с использованием 80-битных регистров, но при сохранении усечены до 64 бит. Таким образом, возможно, чтобы первое деление было вычислено, усечено, затем загружено обратно в память и сравнивается с не усеченным делением.

См. Здесь для получения дополнительной информации (это ссылка C++, но рассуждения одинаковы)