Является ли арифметика с плавающей запятой стабильной?
Я знаю, что числа с плавающей запятой имеют точность, а цифры после точности не надежны.
Но что, если уравнение, используемое для вычисления числа, одинаково? могу ли я предположить, что результат будет таким же?
например, у нас есть два числа с плавающей точкой 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++, но рассуждения одинаковы)