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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Следующая страница может быть полезна в случае, когда вам нужна абсолютная мобильность таких операций. В нем обсуждается программное обеспечение для тестирования реализаций стандарта IEEE 754, включая программное обеспечение для эмуляции операций с плавающей запятой. Однако большая часть информации, скорее всего, относится к C или 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]; 

    Это проблема для C #?

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

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

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

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

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

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

    Не то, что я знаю из.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    По просьбе ОП – относительно производительности:

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

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

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

    Чтобы уменьшить хит производительности, существует несколько страtagsй – например QNumbers (см. Ответ Джонатана Дикинсона). Является ли математика с плавающей запятой последовательной в C #? Может ли быть? ) И / или кэширование (например, триггерные вычисления …) и т. Д.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Interesting Posts

    Отображать символы Юникода при преобразовании Html в Pdf

    Может ли SFINAE обнаруживать нарушения частного доступа?

    Использование def, val и var в scala

    Как создать массив, когда размер является переменной, а не константой?

    Как начать работу в адаптере?

    Кнопка iOS UINavigationBar остается потушенной после повторной записи

    WPF: привязка ContextMenu к команде MVVM

    Поведение параметра «mapred.min.split.size» в HDFS

    Что делает postInvalidate ()?

    Преобразование числового представления столбца «переменная» в исходную строку после таяния с использованием шаблонов

    Где fn.toggle (обработчик (eventObject), обработчик (eventObject) …) ушел?

    Как очистить cookies и кеширование веб-обозревателя на Android, если вы не находитесь в веб-просмотре?

    Совместимы ли SATA II и SATA 3,0 Гбит / с?

    responsejs, дающий ошибку Uncaught TypeError: Супер выражение должно быть либо null, либо функцией, а не неопределенной

    Почему автоматическая установка sshfs с помощью autofs завершается с ошибкой?

    Давайте будем гением компьютера.