Принудительное использование плавающей запятой в .NET?
Я много читал о детерминизме с плавающей запятой в .NET, то есть гарантируя, что один и тот же код с одинаковыми входами даст одинаковые результаты на разных машинах. Так как .NET не имеет таких параметров, как fpstrict Java и fp: strict, MSVC, консенсус, похоже, заключается в том, что в этой проблеме нет пути использования чистого управляемого кода. Игра C # AI Wars решила использовать математику с фиксированной точкой вместо этого, но это громоздкое решение.
Основная проблема заключается в том, что CLR позволяет промежуточным результатам жить в регистрах FPU, которые имеют более высокую точность, чем собственная точность типа, что приводит к непредсказуемо более высоким результатам точности. В статье MSDN инженера CLR Дэвида Нотарио объясняется следующее:
Обратите внимание, что с текущей спецификацией, это все еще выбор языка, чтобы дать «предсказуемость». Язык может вставлять команды conv.r4 или conv.r8 после каждой операции FP, чтобы получить «предсказуемое» поведение. Очевидно, что это действительно дорого, а разные языки имеют разные компромиссы. Например, C # ничего не делает, если вы хотите сузить, вам придется вручную вводить (float) и (double).
- Плавающая точка и целочисленные вычисления на современном оборудовании
- Драйвер Microsoft ACE изменяет точность с плавающей запятой в остальной части моей программы
- Оптимизация для быстрого умножения, но медленное добавление: FMA и doubleedouble
- Проблема с поплавками в Objective-C
- Как напечатать двойное значение с полной точностью с помощью cout?
Это говорит о том, что можно достичь детерминизма с плавающей запятой, просто вставив явные приведения для каждого выражения и подвыражения, которое вычисляет float. Можно написать тип оболочки вокруг float для автоматизации этой задачи. Это было бы простое и идеальное решение!
Однако другие комментарии указывают на то, что это не так просто. Эрик Липперт недавно заявил (акцент мой):
в какой-то версии среды выполнения приведение в float явно приводит к другому результату, чем к такому результату. Когда вы явно бросаете float, компилятор C # дает подсказку для среды выполнения, чтобы сказать: «Извлеките эту вещь из режима сверхвысокой точности, если вы используете эту оптимизацию».
Что это за «намек» на время выполнения? Указывает ли спецификация C #, что явное приведение в float вызывает вложение conv.r4 в IL? Определяет ли спецификация CLR, что команда conv.r4 приводит к сужению значения до его собственного размера? Только если оба они являются истинными, мы можем полагаться на явные приведения, чтобы обеспечить «предсказуемость» с плавающей запятой, как объяснил Дэвид Нотарио.
Наконец, даже если мы действительно можем принудить все промежуточные результаты к собственному размеру типа, достаточно ли этого, чтобы гарантировать воспроизводимость на разных машинах или существуют другие факторы, такие как настройки времени выполнения FPU / SSE?
- (.1f + .2f ==. 3f)! = (.1f + .2f) .Equals (.3f) Почему?
- Печать двойная без потери точности
- Изменение режима округления с плавающей запятой
- Преобразование из строки научной нотации в float в C #
- Разделение целых чисел на Java
- round () для float в C ++
- Почему SSE скалярный sqrt (x) медленнее, чем rsqrt (x) * x?
- Безопасно ли проверять значения с плавающей запятой на равенство 0?
Что это за «намек» на время выполнения?
Как вы предполагаете, компилятор отслеживает, было ли преобразование в double или float фактически присутствующим в исходном коде, и если бы это было так, он всегда вставлял соответствующий код conv opcode.
Указывает ли спецификация C #, что явное приведение в float вызывает вложение conv.r4 в IL?
Нет, но я заверяю вас, что в тестах на компилятор есть единичные тесты, которые гарантируют, что это произойдет. Хотя спецификация не требует этого, вы можете положиться на это поведение.
Единственным комментарием спецификации является то, что любая операция с плавающей запятой может выполняться с большей точностью, чем требуется по прихоти среды выполнения, и это может сделать ваши результаты неожиданно более точными. См. Раздел 4.1.6.
Определяет ли спецификация CLR, что команда conv.r4 приводит к сужению значения до его собственного размера?
Да, в Разделе I, раздел 12.1.3, который, как я заметил, вы могли бы поискать себя, а не просить интернет сделать это за вас. Эти спецификации бесплатны в Интернете.
Вопрос, который вы не спрашивали, но, вероятно, должен иметь:
Есть ли какая-либо операция, кроме каста, которая усекает, выплывает из режима высокой точности?
Да. Присвоение статическому полю, поле экземпляра или элемент массива double[]
или float[]
обрезается.
Является ли постоянное усечение достаточным для обеспечения воспроизводимости на машинах?
Нет. Я призываю вас прочитать раздел 12.1.3, в котором есть много интересного, чтобы сказать о денормалах и NaNs.
И, наконец, еще один вопрос, который вы не спросили, но, вероятно, должен иметь:
Как я могу гарантировать воспроизводимую арифметику?
Используйте целые числа.
Конструкция чипа с плавающей точкой 8087 была ошибкой миллиарда долларов Intel. Идея выглядит хорошо на бумаге, дайте ей 8-разрядный стек, который хранит значения в расширенной точности, 80 бит. Чтобы вы могли писать вычисления, промежуточные значения которых с меньшей вероятностью потеряют значимые цифры.
Зверь, однако, невозможно оптимизировать. Хранение значения из стека FPU обратно в память дорого. Поэтому держать их внутри FPU является сильной целью оптимизации. Неизбежно, имея только 8 регистров, потребует обратной записи, если расчет достаточно глубок. Он также реализован в виде стека, а не свободно адресуемых регистров, что требует также гимнастики, которая может привести к обратному обращению. Неизбежно обратная запись урезает значение с 80 бит до 64 бит, теряя точность.
Таким образом, последствия того, что неоптимизированный код не дает того же результата, что и оптимизированный код. И небольшие изменения в расчете могут иметь большое влияние на результат, когда промежуточные значения заканчиваются, и их нужно записать обратно. Параметр / fp: strict – это взломать это, он заставляет генератор кода испускать обратную запись, чтобы поддерживать согласованность значений, но с неизбежной и значительной потерей perf.
Это полная скала и трудное место. Для джиттера x86 они просто не пытались решить проблему.
Intel не допустила такой же ошибки при разработке набора инструкций SSE. Регистры XMM свободно адресуются и не хранят лишние биты. Если вам нужны последовательные результаты, то компиляция с целью AnyCPU и 64-разрядной операционной системой является быстрым решением. Джиттер x64 использует SSE вместо инструкций FPU для математики с плавающей запятой. Хотя это добавило третий способ, что расчет может привести к другому результату. Если расчет неверен, потому что он теряет слишком много значимых цифр, то он будет постоянно ошибочным. На самом деле это немного бромид, но, как правило, только в том, что касается программиста.