Нет, это не другой вопрос «Почему (1 / 3.0) * 3! = 1» .
В последнее время я много читаю о числах с плавающей запятой; в частности, как один и тот же расчет может дать разные результаты для разных архитектур или настроек оптимизации.
Это проблема для видеоигр, в которых хранятся повторы или они работают в одноранговой сети (в отличие от сервера-клиента), в которых все клиенты получают одинаковые результаты при каждом запуске программы - небольшое расхождение в одном Вычисление с плавающей точкой может привести к совершенно разному игровому состоянию на разных машинах (или даже на одной машине! )
Это происходит даже среди процессоров, которые «следуют» IEEE-754 , главным образом потому, что некоторые процессоры (а именно x86) используют двойную расширенную точность . То есть они используют 80-битные регистры для выполнения всех вычислений, а затем усекаются до 64- или 32-битных, что приводит к результатам округления, отличным от тех, которые используют 64- или 32-битные для расчетов.
Я видел несколько решений этой проблемы в Интернете, но все для C ++, а не для C #:
- Отключите режим двойной расширенной точности (чтобы во всех
double
вычислениях использовался IEEE-754 64 -битный режим ) с использованием_controlfp_s
(Windows),_FPU_SETCW
(Linux?) Илиfpsetprec
(BSD). - Всегда запускайте один и тот же компилятор с одинаковыми настройками оптимизации и требуйте, чтобы все пользователи имели одинаковую архитектуру ЦП (без межплатформенного воспроизведения). Поскольку мой «компилятор» на самом деле является JIT, который может оптимизироваться по-разному при каждом запуске программы , я не думаю, что это возможно.
- Используйте арифметику с фиксированной запятой, избегайте
float
иdouble
вообще.decimal
будет работать для этой цели, но будет намного медленнее, и ни одна изSystem.Math
функций библиотеки не поддерживает его.
Так это вообще проблема в C #? Что если я собираюсь поддерживать только Windows (не Mono)?
Если это так, есть ли способ заставить мою программу работать с нормальной двойной точностью?
Если нет, то есть ли библиотеки, которые помогли бы поддерживать согласованность вычислений с плавающей точкой?
strictfp
ключевое слово, которое заставляет все вычисления выполняться в указанном размере (float
илиdouble
), а не в расширенном размере. Однако у Java все еще много проблем с поддержкой IEE-754. Очень (очень, очень) немногие языки программирования хорошо поддерживают IEE-754.Ответы:
Я не знаю способа сделать нормальные плавающие точки детерминированными в .net. JITter разрешено создавать код, который ведет себя по-разному на разных платформах (или между разными версиями .net). Поэтому использование нормальных
float
s в детерминированном .net-коде невозможно.Обходные пути, которые я рассмотрел:
Я только начал программную реализацию 32-битной математики с плавающей запятой. Он может делать около 70 миллионов операций добавления / умножения в секунду на моем 2,66 ГГц i3. https://github.com/CodesInChaos/SoftFloat . Очевидно это все еще очень неполно и глючит.
источник
decimal
сначала, так как это гораздо проще сделать. Только если он будет слишком медленным для поставленной задачи, стоит подумать о других подходах.Спецификация C # (§4.1.6 типы с плавающей запятой), в частности, позволяет выполнять вычисления с плавающей запятой, используя точность, превышающую точность результата. Так что нет, я не думаю, что вы можете сделать эти расчеты детерминированными непосредственно в .Net. Другие предлагали различные обходные пути, чтобы вы могли их попробовать.
источник
double
каждому разу после операции убирать ненужные биты, давая последовательные результаты?Следующая страница может быть полезна в случае, когда вам нужна абсолютная переносимость таких операций. В нем обсуждается программное обеспечение для тестирования реализаций стандарта IEEE 754, включая программное обеспечение для эмуляции операций с плавающей запятой. Однако большая часть информации, вероятно, относится к C или C ++.
http://www.math.utah.edu/~beebe/software/ieee/
Примечание о фиксированной точке
Двоичные числа с фиксированной запятой также могут хорошо заменить плавающую точку, как это видно из четырех основных арифметических операций:
Двоичные числа с фиксированной запятой могут быть реализованы для любого целочисленного типа данных, такого как int, long и BigInteger, а также для не-CLS-совместимых типов uint и ulong.
Как предлагается в другом ответе, вы можете использовать справочные таблицы, где каждый элемент в таблице является двоичным числом с фиксированной запятой, чтобы помочь реализовать сложные функции, такие как синус, косинус, квадратный корень и т. Д. Если таблица поиска менее детализирована, чем число с фиксированной точкой, рекомендуется округлить входные данные, добавив половину гранулярности таблицы поиска к входным данным:
источник
const
вместоstatic
констант, чтобы компилятор мог их оптимизировать; предпочитаю функции-члены статическим функциям (чтобы мы могли вызывать, например,myDouble.LeadingZeros()
вместоIntDouble.LeadingZeros(myDouble)
); старайтесь избегать однобуквенных имен переменных (MultiplyAnyLength
например, имеет 9, что делает его очень сложным для отслеживания)unchecked
не-CLS-совместимые типы, такие какulong
,uint
и т. Д. Для целей скорости - поскольку они используются так редко, JIT не оптимизирует их так агрессивно, поэтому их использование может быть медленнее, чем использование обычных типов, таких какlong
иint
. Кроме того, в C # есть перегрузка операторов , от которой этот проект сильно выиграл бы. Наконец, есть ли связанные юнит-тесты? Помимо этих маленьких вещей, потрясающая работа Питера, это смехотворно впечатляет!strictfp
.Это проблема для C #?
Да. Разные архитектуры - это наименьшее количество ваших забот, разные частоты кадров и т. Д. Могут привести к отклонениям из-за неточностей в представлениях с плавающей точкой - даже если они имеют одинаковые неточности (например, одинаковая архитектура, за исключением более медленного графического процессора на одной машине).
Могу ли я использовать System.Decimal?
Там нет причин, вы не можете, однако это собака медленно.
Есть ли способ заставить мою программу работать с двойной точностью?
Да. Хостинг среды CLR самостоятельно ; и скомпилировать все необходимые вызовы / флаги (которые изменяют поведение арифметики с плавающей запятой) в приложение C ++ перед вызовом CorBindToRuntimeEx.
Существуют ли какие-либо библиотеки, которые помогли бы поддерживать согласованность вычислений с плавающей запятой?
Не то, что я знаю о.
Есть ли другой способ решить эту проблему?
Я занимался этой проблемой раньше, идея в том, чтобы использовать QNumbers . Они являются формой реальных событий с фиксированной точкой; но не фиксированная точка в base-10 (десятичная) - скорее base-2 (двоичная); из-за этого математические примитивы на них (add, sub, mul, div) намного быстрее, чем наивные фиксированные точки base-10; особенно если
n
это одинаково для обоих значений (что в вашем случае будет). Кроме того, поскольку они являются интегральными, они имеют четко определенные результаты на каждой платформе.Имейте в виду, что частота кадров все еще может влиять на них, но она не так плоха, и ее легко исправить с помощью точек синхронизации.
Могу ли я использовать больше математических функций с QNumbers?
Да, туда и обратно десятичная дробь, чтобы сделать это. Кроме того, вы действительно должны использовать справочные таблицы для функций trig (sin, cos); поскольку они действительно могут давать разные результаты на разных платформах - и если вы правильно их кодируете, они могут напрямую использовать QNumbers.
источник
Согласно этой немного старой записи в блоге MSDN, JIT не будет использовать SSE / SSE2 для плавающей запятой, это все x87. Из-за этого, как вы упомянули, вам нужно беспокоиться о режимах и флагах, а в C # это невозможно контролировать. Таким образом, использование обычных операций с плавающей запятой не гарантирует одинаковый результат на каждой машине для вашей программы.
Чтобы получить точную воспроизводимость двойной точности, вам нужно будет выполнить программную эмуляцию с плавающей запятой (или с фиксированной запятой). Я не знаю библиотек C #, чтобы сделать это.
В зависимости от операций, которые вам нужны, вы можете уйти с единой точностью. Вот идея:
Большая проблема с x87 заключается в том, что вычисления могут выполняться с 53-битной или 64-битной точностью в зависимости от флага точности и от того, был ли регистр передан в память. Но для многих операций выполнение операции с высокой точностью и округление до более низкой точности гарантирует правильный ответ, что означает, что ответ будет гарантированно одинаковым во всех системах. Получите ли вы дополнительную точность, не имеет значения, поскольку у вас достаточно точности, чтобы гарантировать правильный ответ в любом случае.
Операции, которые должны работать в этой схеме: сложение, вычитание, умножение, деление, sqrt. Такие вещи, как грех, опыт и т. Д. Не сработают (результаты обычно совпадают, но гарантии нет). "Когда двойное округление безобидно?" Справочник ACM (оплаченный рег. Запрос)
Надеюсь это поможет!
источник
Как уже говорилось в других ответах: да, это проблема в C # - даже при использовании чистой Windows.
Что касается решения: вы можете уменьшить (и с некоторым усилием / ударом по производительности) полностью избежать проблемы, если вы используете встроенный
BigInteger
класс и масштабируете все вычисления с определенной точностью, используя общий знаменатель для любого вычисления / хранения таких чисел.В соответствии с просьбой OP - относительно производительности:
System.Decimal
представляет число с 1 битом для знака и 96-битное целое число и «масштаб» (представляющий, где находится десятичная точка). Для всех вычислений, которые вы делаете, он должен работать с этой структурой данных и не может использовать никакие инструкции с плавающей запятой, встроенные в ЦП.BigInteger
«Решение» делает нечто подобное - только то , что вы можете определить , сколько цифр вам нужно / хочу ... возможно , вы хотите только 80 бит или 240 бит точности.Медлительность всегда возникает из-за необходимости симулировать все операции с этими числами с помощью только целочисленных инструкций без использования встроенных инструкций CPU / FPU, что, в свою очередь, приводит к гораздо большему количеству инструкций на математическую операцию.
Чтобы уменьшить удар по производительности, есть несколько стратегий - например, QNumbers (см. Ответ Джонатана Дикинсона - Соответствует ли математика с плавающей запятой в C #? Может ли это быть? ) И / или кэширование (например, вычисления триггеров ...) и т. Д.
источник
BigInteger
доступно только в .Net 4.0.BigInteger
превышает даже снижение производительности десятичным.Decimal
снижение производительности при использовании (@Jonathan Dickinson - «медлительность собаки») илиBigInteger
(комментарий @CodeInChaos выше) - может кто-нибудь дать небольшое объяснение этим ударам производительности и относительно того, / почему они действительно мешают решению проблемы.Что ж, вот моя первая попытка сделать это :
(Я полагаю, что вы можете просто скомпилировать 32-битный файл .dll, а затем использовать его с x86 или AnyCpu [или, скорее всего, только для x86 в 64-битной системе; см. Комментарий ниже].)
Затем, если это сработает, если вы захотите использовать Mono, я полагаю, что вы сможете реплицировать библиотеку на другие платформы x86 аналогичным образом (конечно, не COM; хотя, возможно, с вином? мы идем туда, хотя ...).
Предполагая, что вы можете заставить его работать, вы должны иметь возможность настраивать пользовательские функции, которые могут выполнять несколько операций одновременно, чтобы исправить любые проблемы с производительностью, и у вас будет математика с плавающей запятой, которая позволяет получать согласованные результаты на разных платформах с минимальным количеством кода, написанного на C ++, и оставшаяся часть кода на C #.
источник
x86
сможет загрузить 32-битную DLL.Я не разработчик игр, хотя у меня большой опыт решения сложных вычислительных задач ... поэтому я сделаю все возможное.
Стратегия, которую я бы принял, по сути такова:
Суть в том, что вам нужно найти баланс. Если вы тратите 30 мс на рендеринг (~ 33 к / с) и только 1 мс на обнаружение коллизий (или вставляете какую-то другую высокочувствительную операцию) - даже если вы утраиваете время, необходимое для выполнения критической арифметики, это влияет на частоту кадров. Вы снижаетесь с 33,3 кадр / с до 30,3 кадр / с.
Я предлагаю вам профилировать все, учесть, сколько времени потрачено на выполнение каждого из заметно дорогих вычислений, затем повторить измерения одним или несколькими методами решения этой проблемы и посмотреть, как это повлияет.
источник
Проверка ссылок в других ответах дает понять, что у вас никогда не будет гарантии, «правильно» ли реализована плавающая точка, или вы всегда будете получать определенную точность для данного вычисления, но, возможно, вы могли бы приложить максимум усилий, (1) усечение всех вычислений до общего минимума (например, если разные реализации дадут вам точность от 32 до 80 бит, всегда обрезая каждую операцию до 30 или 31 бит), (2) иметь таблицу нескольких тестовых примеров при запуске (граничные случаи сложения, вычитания, умножения, деления, sqrt, косинуса и т. д.), и если реализация вычисляет значения, соответствующие таблице, тогда не стоит вносить какие-либо корректировки.
источник
float
делает тип данных на компьютерах с архитектурой x86, - однако это приведет к немного отличающимся результатам от машин, которые выполняют все свои вычисления с использованием только 32-битных данных, и эти небольшие изменения будут распространяться со временем. Отсюда и вопрос.Ваш вопрос в довольно сложных и технических вещах О_о. Однако у меня может быть идея.
Вы уверены, что процессор выполняет некоторые настройки после любых операций с плавающей запятой. И CPU предлагают несколько разных инструкций, которые выполняют разные операции округления.
Поэтому для выражения ваш компилятор выберет набор инструкций, которые приведут вас к результату. Но любой другой рабочий процесс инструкций, даже если они намереваются вычислить то же выражение, может дать другой результат.
«Ошибки», допущенные при корректировке округления, будут увеличиваться при каждой дальнейшей инструкции.
В качестве примера можно сказать, что на уровне сборки: a * b * c не эквивалентен a * c * b.
Я не совсем уверен в этом, вам нужно будет попросить кого-то, кто знает архитектуру процессора намного больше, чем я: p
Однако, чтобы ответить на ваш вопрос: в C или C ++ вы можете решить свою проблему, потому что у вас есть некоторый контроль над машинным кодом, сгенерированным вашим компилятором, однако в .NET у вас его нет. Поэтому, пока ваш машинный код может отличаться, вы никогда не будете уверены в точном результате.
Мне любопытно, каким образом это может быть проблемой, потому что вариация кажется очень минимальной, но если вам нужна действительно точная работа, единственное решение, о котором я могу подумать, - это увеличить размер ваших плавающих регистров. Используйте двойную точность или даже длинную двойную, если можете (не уверен, что это возможно с помощью CLI).
Я надеюсь, что я был достаточно ясен, я не совершенен в английском (... вообще: s)
источник