Является ли математика с плавающей запятой в С#? Может ли так быть?

Нет, это не другое. "Почему вопрос (1/3.0) * 3!= 1".

В последнее время я много читал о плавающих очках; в частности, как тот же расчет может дать разные результаты для разных архитектур или параметров оптимизации.

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

Это происходит даже среди процессоров, которые "следуют" IEEE-754, прежде всего потому, что некоторые процессоры (а именно x86) используют двойная расширенная точность. То есть они используют 80-битные регистры для выполнения всех вычислений, затем обрезают до 64 или 32 бит, что приводит к разным результатам округления, чем к машинам, использующим для вычислений 64- или 32-бит.

Я видел несколько решений этой проблемы в Интернете, но все для С++, а не С#:

  • Отключить режим двойной расширенной точности (так что все вычисления double используют 64-разрядные IEEE-754) с помощью _controlfp_s (Windows), _FPU_SETCW (Linux?) или fpsetprec (BSD).
  • Всегда запускайте один и тот же компилятор с одинаковыми настройками оптимизации и требуйте, чтобы все пользователи имели одинаковую архитектуру процессора (без кросс-платформенной игры). Поскольку мой "компилятор" на самом деле является JIT, который может оптимизироваться по-разному каждый раз, когда программа запускается, я не думаю, что это возможно.
  • Используйте арифметику с фиксированной точкой и избегайте float и double вообще. decimal будет работать с этой целью, но будет намного медленнее, и ни одна из функций библиотеки System.Math не поддерживает его.

Итак, - это даже проблема в С#? Что делать, если я только намерен поддерживать Windows (не Mono)?

Если это так, есть ли способ заставить мою программу работать при нормальной двойной точности?

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

Ответы

Ответ 1

Я не знаю пути, чтобы сделать нормальные с плавающей запятой детерминированными в .net. JITter разрешено создавать код, который ведет себя по-разному на разных платформах (или между различными версиями .net). Поэтому использование нормального float в детерминированном .net-коде не представляется возможным.

Обходные решения, которые я рассмотрел:

  • Внедрить FixedPoint32 в С#. Хотя это не слишком сложно (у меня есть половина готовой реализации), очень маленький диапазон значений делает его раздражающим для использования. Вы должны быть осторожны во все времена, чтобы вы не переполняли и не теряли слишком много точности. В конце концов я нашел это не проще, чем использовать целые числа.
  • Внедрить FixedPoint64 в С#. Я счел это довольно трудным делом. Для некоторых операций были бы полезны промежуточные целые числа в 128 бит. Но .net не предлагает такого типа.
  • Внедрение настраиваемой 32-разрядной плавающей запятой. Отсутствие встроенного BitScanReverse вызывает некоторые неприятности при реализации этого. Но в настоящее время я думаю, что это самый перспективный путь.
  • Используйте собственный код для математических операций. Включает накладные расходы на вызов делегата при каждой математической операции.

Я только что начал реализацию программного обеспечения с 32-битной математикой с плавающей запятой. Он может делать около 70 миллионов дополнений/умножений в секунду на моем 2,66 ГГц i3. https://github.com/CodesInChaos/SoftFloat. Очевидно, он все еще очень неполный и багги.

Ответ 2

Спецификация С# (§4.1.6 Типы с плавающей точкой) специально позволяет выполнять вычисления с плавающей запятой, используя точность, превышающую точность результата. Итак, нет, я не думаю, что вы можете сделать эти вычисления детерминированными непосредственно в .Net. Другие предложили различные обходные пути, чтобы вы могли попробовать их.

Ответ 3

Следующая страница может быть полезна в случае, когда вам нужна абсолютная мобильность таких операций. В нем обсуждается программное обеспечение для тестирования реализаций стандарта IEEE 754, включая программное обеспечение для эмуляции операций с плавающей запятой. Однако большая часть информации, вероятно, относится к C или С++.

http://www.math.utah.edu/~beebe/software/ieee/

Заметка о неподвижной точке

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

  • Сложение и вычитание тривиальны. Они работают так же, как целые. Просто добавьте или вычтите!
  • Чтобы умножить два числа с фиксированной точкой, умножьте два числа, затем сдвиньте вправо определенное количество дробных бит.
  • Чтобы разделить два номера фиксированной точки, сдвиньте дивиденд, оставив определенное количество дробных битов, затем разделите его на делитель.
  • Глава четвертая в этом документе содержит дополнительные указания по реализации двоичных чисел с фиксированной точкой.

Двоичные числа фиксированной точки могут быть реализованы на любом целочисленном типе данных, таком как int, long и BigInteger, а также не-CLS-совместимые типы uint и ulong.

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

// Assume each number has a 12 bit fractional part. (1/4096)
// Each entry in the lookup table corresponds to a fixed point number
//  with an 8-bit fractional part (1/256)
input+=(1<<3); // Add 2^3 for rounding purposes
input>>=4; // Shift right by 4 (to get 8-bit fractional part)
// --- clamp or restrict input here --
// Look up value.
return lookupTable[input];

Ответ 4

Является ли это проблемой для С#?

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

Могу ли я использовать System.Decimal?

Нет причин, по которым вы не можете, однако собака медленно.

Есть ли способ заставить мою программу работать с двойной точностью?

Да. сам запустить среду CLR; и скомпилировать во всех неотложных вызовах/флажках (которые изменяют поведение арифметики с плавающей запятой) в приложение С++ до вызова CorBindToRuntimeEx.

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

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

Есть ли другой способ решить эту проблему?

Я решил эту проблему раньше, идея заключается в использовании QNumbers. Они представляют собой форму реалов, которые являются фиксированными; но не фиксированная точка в базе-10 (десятичная) - скорее база-2 (двоичная); из-за этого математические примитивы на них (add, sub, mul, div) намного быстрее, чем наивные базовые 10 неподвижных точек; особенно если n одинаково для обоих значений (что в вашем случае было бы). Кроме того, поскольку они являются неотъемлемыми, они имеют четко определенные результаты на каждой платформе.

Имейте в виду, что частота кадров может все же повлиять на них, но это не так плохо и легко исправляется с использованием точек синхронизации.

Могу ли я использовать более математические функции с QNumbers?

Да, с округлением до запятой, чтобы сделать это. Кроме того, вы действительно должны использовать таблицы поиска для функций trig (sin, cos); поскольку они могут действительно давать разные результаты на разных платформах - и если вы правильно их кодируете, они могут напрямую использовать QNumbers.

Ответ 5

В соответствии с этой слегка старой записью в блоге MSDN JIT не будет использовать SSE/SSE2 для плавающей запятой, все x87. Из-за этого, как вы упомянули, вам нужно беспокоиться о режимах и флагах, а также в С#, которые невозможно контролировать. Поэтому использование обычных операций с плавающей запятой не гарантирует точно такой же результат на каждом компьютере для вашей программы.

Чтобы получить точную воспроизводимость двойной точности, вам придется делать эмуляцию программного обеспечения с плавающей запятой (или с фиксированной точкой). Я не знаю библиотеки С# для этого.

В зависимости от операций, которые вам нужны, вы можете уйти с единой точностью. Здесь идея:

  • хранить все значения, о которых вы заботитесь в одинарной точности.
  • для выполнения операции:
    • расширить вводы для двойной точности
    • выполнить операцию с двойной точностью
    • преобразовать результат обратно в одинарную точность

Большая проблема с x87 заключается в том, что вычисления могут выполняться с 53-битной или 64-разрядной точностью в зависимости от флага точности и от того, пролистал ли регистр память. Но для многих операций выполнение операции с высокой точностью и округление до более низкой точности гарантирует правильный ответ, что подразумевает, что ответ будет гарантированно одинаковым для всех систем. Получаете ли вы дополнительную точность, это не имеет значения, поскольку у вас достаточно точности, чтобы гарантировать правильный ответ в любом случае.

Операции, которые должны работать в этой схеме: сложение, вычитание, умножение, деление, sqrt. Такие вещи, как sin, exp и т.д., Не будут работать (результаты обычно совпадают, но нет гарантии). "Когда двойное округление безобидно?" Ссылка ACM (платный рег.)

Надеюсь, это поможет!

Ответ 6

Как уже было сказано другими ответами: Да, это проблема на С# - даже при сохранении чистой Windows.

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

В соответствии с запросом OP - относительно производительности:

System.Decimal представляет число с 1 бит для знака и 96-битное целое число и "масштаб" (представляющий, где находится десятичная точка). Для всех вычислений, которые вы делаете, он должен работать с этой структурой данных и не может использовать инструкции с плавающей запятой, встроенные в ЦП.

BigInteger "решение" делает что-то похожее - только вы можете определить, сколько цифр вам нужно/нужно... возможно, вы хотите только 80 бит или 240 бит точности.

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

Чтобы уменьшить хит производительности, существует несколько стратегий - например, QNumbers (см. ответ от Jonathan Dickinson - Является ли математика с плавающей запятой последовательной в С#? Может ли это быть?) и/или кеширование (например, триггерные вычисления...) и т.д.

Ответ 7

Ну, вот моя первая попытка на как это сделать:

  • Создайте проект ATL.dll, в котором есть простой объект, который будет использоваться для ваших критических операций с плавающей запятой. не забудьте скомпилировать его с флагами, которые запрещают использование любого оборудования, отличного от xx87, для выполнения плавающей запятой.
  • Создавать функции, которые вызывают операции с плавающей запятой и возвращают результаты; начните просто, а затем, если он будет работать на вас, вы всегда сможете увеличить сложность для удовлетворения ваших потребностей в производительности позже, если это необходимо.
  • Поместите вызовы control_fp вокруг фактической математики, чтобы убедиться, что они сделали то же самое на всех машинах.
  • Ссылка на новую библиотеку и тест, чтобы убедиться, что она работает так, как ожидалось.

(Я считаю, что вы можете просто скомпилировать 32-битную DLL, а затем использовать ее либо с x86, либо с AnyCpu [или, скорее всего, только с таргетингом на x86 в 64-битной системе, см. комментарий ниже).

Затем, полагая, что это работает, если вы хотите использовать Mono, я предполагаю, что вы должны иметь возможность реплицировать библиотеку на других платформах x86 аналогичным образом (не COM, конечно, хотя, возможно, с вином? немного из мой район, как только мы туда поедем...).

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

Ответ 8

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

Стратегия, которую я бы принял, в основном такова:

  • Используйте медленнее (если необходимо, если есть более быстрый способ, отличный!), но предсказуемый метод получения воспроизводимых результатов
  • Используйте double для всего остального (например, рендеринга)

Короче говоря, вам нужно найти баланс. Если вы тратите 30 мс рендеринга (~ 33 кадра в секунду) и только 1 мс на обнаружение столкновений (или вставляете какую-то другую высокочувствительную операцию) - даже если вы утроите время, необходимое для выполнения критической арифметики, влияние, которое оно оказывает на вашу частоту кадров вы снижаетесь с 33,3 кадра в секунду до 30,3 кадра в секунду.

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

Ответ 9

Проверка ссылок в других ответах дает понять, что у вас никогда не будет гарантии, правильно ли реализована плавающая точка, или вы всегда будете получать определенную точность для данного расчета, но, возможно, вы можете сделать Наилучшее усилие: (1) усечение всех вычислений до общего минимума (например, если разные реализации дадут вам от 32 до 80 бит точности, всегда усекая каждую операцию до 30 или 31 бит), (2) имеют таблицу нескольких тестов случаи при запуске (пограничные случаи добавления, вычитания, умножения, деления, sqrt, косинуса и т.д.), и если реализация вычисляет значения, соответствующие таблице, не беспокоиться о каких-либо корректировках.

Ответ 10

Ваш вопрос в довольно сложных и технических материалах O_o. Однако у меня может быть идея.

Вы точно знаете, что CPU выполняет некоторую настройку после любых плавающих операций. И CPU предлагают несколько разных инструкций, которые выполняют различные операции округления.

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

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

В качестве примера можно сказать, что на уровне сборки: a * b * c не эквивалентен a * c * b.

Я не совсем уверен в этом, вам нужно будет попросить кого-то, кто знает архитектуру процессора, намного больше меня: p

Однако, чтобы ответить на ваш вопрос: на C или С++ вы можете решить свою проблему, потому что у вас есть некоторый контроль над машинным кодом, генерируемым вашим компилятором, однако в .NET у вас его нет. Так что, пока ваш машинный код может быть другим, вы никогда не будете уверены в точном результате.

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

Надеюсь, я был достаточно ясен, я не совершенен на английском (... вообще: s)