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


нет, это не еще один "почему (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 (не моно)?

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

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

10 145

10 ответов:

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

обходные пути я считал:

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

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

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

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

http://www.math.utah.edu / ~beebe / программное обеспечение / 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#?

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

могу ли я использовать систему.Десятичное число?

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

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

да. размещайте среду выполнения CLR самостоятельно; и скомпилировать во всех вызовах/флагах nessecary (которые изменяют поведение арифметики с плавающей запятой) в приложение 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-разрядной точностью в зависимости от флага точности и того, разлит ли регистр в память. Но для многих операций выполнение операции с высокой точностью и округление до более низкой точности гарантирует правильный ответ, что означает, что ответ будет гарантированно одинаковым на всех системах. Получите ли вы дополнительную точность не имеет значения, так как у вас достаточно точности, чтобы гарантировать правильный ответ в любом случае.

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

надеюсь, что это помогает!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

на ваш вопрос достаточно сложно-технические вещи О_о. Однако у меня есть идея.

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

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

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

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

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

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

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

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