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

Я много читал о детерминизме с плавающей запятой в .NET, то есть гарантируя, что один и тот же код с одинаковыми входами даст одинаковые результаты на разных машинах. Так как .NET не имеет таких параметров, как fpstrict Java и fp: strict, MSVC, консенсус, похоже, заключается в том, что в этой проблеме нет пути использования чистого управляемого кода. Игра C # AI Wars решила использовать математику с фиксированной точкой вместо этого, но это громоздкое решение.

Основная проблема заключается в том, что CLR позволяет промежуточным результатам жить в регистрах FPU, которые имеют более высокую точность, чем собственная точность типа, что приводит к непредсказуемо более высоким результатам точности. В статье MSDN инженера CLR Дэвида Нотарио объясняется следующее:

Обратите внимание, что с текущей спецификацией, это все еще выбор языка, чтобы дать «предсказуемость». Язык может вставлять команды conv.r4 или conv.r8 после каждой операции FP, чтобы получить «предсказуемое» поведение. Очевидно, что это действительно дорого, а разные языки имеют разные компромиссы. Например, C # ничего не делает, если вы хотите сузить, вам придется вручную вводить (float) и (double).

Это говорит о том, что можно достичь детерминизма с плавающей запятой, просто вставив явные приведения для каждого выражения и подвыражения, которое вычисляет float. Можно написать тип оболочки вокруг float для автоматизации этой задачи. Это было бы простое и идеальное решение!

Однако другие комментарии указывают на то, что это не так просто. Эрик Липперт недавно заявил (акцент мой):

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

Что это за «намек» на время выполнения? Указывает ли спецификация C #, что явное приведение в float вызывает вложение conv.r4 в IL? Определяет ли спецификация CLR, что команда conv.r4 приводит к сужению значения до его собственного размера? Только если оба они являются истинными, мы можем полагаться на явные приведения, чтобы обеспечить «предсказуемость» с плавающей запятой, как объяснил Дэвид Нотарио.

Наконец, даже если мы действительно можем принудить все промежуточные результаты к собственному размеру типа, достаточно ли этого, чтобы гарантировать воспроизводимость на разных машинах или существуют другие факторы, такие как настройки времени выполнения FPU / SSE?

Что это за «намек» на время выполнения?

Как вы предполагаете, компилятор отслеживает, было ли преобразование в 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 для математики с плавающей запятой. Хотя это добавило третий способ, что расчет может привести к другому результату. Если расчет неверен, потому что он теряет слишком много значимых цифр, то он будет постоянно ошибочным. На самом деле это немного бромид, но, как правило, только в том, что касается программиста.

  • разбиение числа на целые и десятичные числа
  • Суффикс «f» по плавающей стоимости?
  • Является ли fmod () точным, когда y является целым числом?
  • Сравнение чисел с плавающей запятой в C
  • C ++: как округлить двойной к int?
  • Почему числа с плавающей точкой неточны?
  • Почему преобразование из float в double изменяет значение?
  • плавать до неожиданного поведения
  • Спецификатор ширины печати для поддержания точности значения с плавающей запятой
  • Как представить 0,1 в арифметике с плавающей точкой и десятичной запятой
  • Как проверить, использует ли C ++-компилятор стандарт IEEE 754 с плавающей запятой
  • Давайте будем гением компьютера.