Насколько детерминирована неточность с плавающей запятой?

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

  • Время между расчетами?
  • Текущее состояние процессора?
  • Другое оборудование?
  • Язык / платформа / ОС?
  • Солнечные вспышки?

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

В настоящее время я работаю в Silverlight, хотя мне было бы интересно узнать, можно ли ответить на этот вопрос в целом.

Обновлять: Первоначальные ответы указывают на «да», но, видимо, это не совсем однозначно, как обсуждалось в комментариях к выбранному ответу. Похоже, мне придется сделать несколько тестов и посмотреть, что произойдет.

В Silverlight вы имеете дело с JIT-компилятором - это означает, что математические операции могут автоматически использовать преимущества SSE, MMX и других специальных инструкций, и те или иные изменения могут изменить точный порядок выполнения инструкций: A + B + C могут не давать того же результат как C + B + A при использовании значений с плавающей запятой. В результате вы получите детерминированные результаты при работе на одном компьютере, но можете получить другие результаты на другом процессоре или даже немного другую конфигурацию системы.

David 17.06.2009 20:53

числа с плавающей запятой, упорядоченные по их точности: десятичное, двойное, плавающее.

Uğur Gümüşhan 31.12.2012 13:40

Это зависит от фазы Луны.

Vinicius Brasil 14.02.2018 14:49
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
26
3
4 954
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

Короткий ответ заключается в том, что вычисления FP полностью детерминированы в соответствии с Стандарт IEEE с плавающей запятой, но это не означает, что они полностью воспроизводимы на разных машинах, компиляторах, ОС и т. д.

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

Чтобы кратко ответить на ваши пункты:

  • Время между расчетами и состоянием процессора имеют мало общего с это.

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

  • Язык, платформа и ОС также могут влияют на вещи. Для лучшего описания этого, чем я могу предложить, см. Ответ Джейсона Уоткинса. Если вы используете Java, взгляните на разглагольствовать о несоответствиях с плавающей запятой в Java Кахана.

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

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

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

См. Мой ответ на @JaredPar, есть много вещей, которые могут вызвать расхождения между расчетами в двух IEEE-совместимых реализациях. Утверждение, что вычисления являются детерминированными, не особенно полезно, поскольку детерминированность не обязательно означает воспроизводимость.

Robert Gamble 30.11.2008 12:22

Я думаю, что ваше замешательство кроется в типе неточности в отношении чисел с плавающей запятой. Большинство языков реализуют Стандарт IEEE с плавающей запятой. Этот стандарт определяет, как отдельные биты в пределах float / double используются для создания числа. Обычно число с плавающей запятой состоит из четырех байтов и двойных восьми байтов.

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

Неточность заключается в точности. Рассмотрим int против float. Оба обычно занимают одинаковое количество байтов (4). Однако максимальное значение, которое может хранить каждое число, сильно различается.

  • int: примерно 2 миллиарда
  • float: 3.40282347E38 (немного больше)

Разница посередине. int, может представлять любое число от 0 до примерно 2 миллиардов. Однако плавать не может. Он может представлять 2 миллиарда значений от 0 до 3,40282347E38. Но остается целый ряд значений, которые невозможно представить. Если математическое уравнение попадает в одно из этих значений, оно должно быть округлено до представимого значения и, следовательно, считается «неточным». Ваше определение неточного может отличаться :).

Это приукрашивает аспект воспроизводимости, который не так очевиден. IEEE дает определенные гарантии, но они основаны на строгих предположениях и не распространяются на все операции или библиотечные функции. @ jason-watkins хорошо объяснил основные проблемы в своем ответе.

Robert Gamble 30.11.2008 12:06

Суть в том, что если вы используете ограниченные операции в одной и той же реализации (компьютер / компилятор / среда выполнения), вы, вероятно, сможете точно воспроизвести результаты, но очень вероятно, что результаты будут немного отличаться в разных реализациях, даже тех, которые поддерживают IEEE. -754.

Robert Gamble 30.11.2008 12:08

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

Vinko Vrsalovic 30.11.2008 12:26

Прочтите параграф. «Воспроизводимость» в вики-статье, ссылка на которую имеется здесь. Резюме: IEEE 754-1985 действительно ли нет гарантирует воспроизводимость между реализациями. 754-2008 поощряет это, но все же не требует его. Если в вашем языке используется 754, это почти наверняка будет версия до 2008 года.

Steve Jessop 30.11.2008 15:14

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

Robert Gamble 30.11.2008 20:39

<pedant> каждый номер естественный </pedant>

ijw 02.01.2011 00:54
Ответ принят как подходящий

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

Конкретные ошибки, о которых я знаю:

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

  2. Промежуточные результаты с плавающей запятой часто используют 80-битную точность в регистре, но только 64-битную в памяти. Если программа перекомпилирована таким образом, что изменяет разлив регистров внутри функции, она может возвращать другие результаты по сравнению с другими версиями. Большинство платформ позволяют принудительно усекать все результаты с точностью до памяти.

  3. стандартные библиотечные функции могут изменяться от версии к версии. Я так понимаю, что в gcc 3 vs 4 есть несколько нередко встречающихся примеров этого.

  4. Сам IEEE позволяет некоторым двоичным представлениям различаться ... в частности, значениям NaN, но я не могу вспомнить подробности.

@ Джейсон Уоткинс: Существует только два логических представления NaN, тихий и сигнальный, однако существует множество двоичных представлений NaN. В противном случае +1 хороший материал.

user7116 30.11.2008 19:14

№1 особенно важен в Windows. Были версии DirectX, которые переводили процессор в режим пониженной точности, что приводило к неожиданным результатам.

nobody 30.11.2008 19:46

Что касается реализаций ЦП, это зависит от используемого вами языка. C довольно неспецифично в том, какой FP вы получите. C# и Java определяют семантику IEEE754, и тогда задача реализации - скрыть то, на что на самом деле способен процессор. Если бы я запускал Java на старом VAX или сломанном Pentium, я ожидал бы увидеть поведение IEEE754, несмотря на то, что это не то, что реализует процессор, потому что определение языка требует этого. Если бы я этого не сделал, JVM была бы сломана по определению.

ijw 02.01.2011 00:51

Здесь много дополнительной информации: gafferongames.com/networking-for-game-programmers/…

Generic Error 11.07.2011 04:43

@ijw: На самом деле, JVM намеренно нарушена для double / float. Вам нужно ключевое слово strictfp, чтобы сделать его строгим IEEE754, но это может замедлить работу программы.

Konrad Borowski 21.05.2014 23:48

Стоит отметить, что 80-битное оборудование предназначалось для использования с языками, которые позволяли бы хранить 80-битные значения в памяти. Предполагалось, что значения самый, хранящиеся в памяти, будут преобразованы с понижением в 64 бита, но временные результаты из общих подвыражений будут сохранены как 80. К сожалению, стандарт C не смог предоставить средства, с помощью которых функции с переменным аргументом могли указывать хотят ли они 64-битных или 80-битных чисел с плавающей запятой, и поставщики компиляторов решили, что самый простой способ избежать проблем совместимости - это сделать так, чтобы их тип «long double» фактически сохранял 64 бита.

supercat 07.04.2015 20:13

Хотя многие люди думают, что 80-битный тип является причудой x87, на самом деле он был разработан для более быстрой работы, чем 64-битный двойной на машинах без модулей с плавающей запятой. «Классический» Macintosh никогда не использовал 8x87, но он выполнял вычисления с плавающей запятой почти так же, как 8x87.

supercat 07.04.2015 20:16

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

Many programmers may not realize that even a program that uses only the numeric formats and operations prescribed by the IEEE standard can compute different results on different systems. In fact, the authors of the standard intended to allow different implementations to obtain different results.

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

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

Вы говорите, что если расчеты неточны, ошибки в начале могут иметь огромные последствия к концу моделирования.

Что мой друг не симуляция. Если вы получаете очень разные результаты из-за крошечных различий из-за округления и точности, то велика вероятность, что ни один из результатов не является достоверным. Тот факт, что вы можете повторить результат, не делает его более достоверным.

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

Нет, я думаю, вы упустили суть. Вопрос действительно в повторяемости, а не в точности.

AnthonyWJones 30.11.2008 12:21

В данном конкретном случае Энтони прав, я ищу повторяемость, а не точность, поскольку я пытаюсь создать что-то интересное, а не настоящую «симуляцию». Может быть, там можно было использовать более подходящее слово ... детская площадка?

Generic Error 30.11.2008 12:51

HM. Поскольку OP запросил C#:

Является ли JIT-код байт-кода C# детерминированным или он генерирует разный код между разными запусками? Я не знаю, но я бы не стал доверять Джиту.

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

Кроме того, если сам JIT будет улучшен (возможно, как часть пакета обновления), сгенерированный код обязательно изменится. Проблема внутренней 80-битной точности уже упоминалась.

Очень немногие FPU соответствуют стандарту IEEE (несмотря на их заявления). Таким образом, запуск одной и той же программы на другом оборудовании действительно даст вам разные результаты. Результаты, скорее всего, будут в критических случаях, которых вы уже должны избегать как часть использования FPU в своем программном обеспечении.

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

Тестирование этого может оказаться бесполезным. Вы не увидите проблемы, пока не доставите товар.

Соблюдайте правило FP № 1: никогда не используйте сравнение if (something == something). И правило номер два IMO будет иметь отношение к ascii в fp или fp в ascii (printf, scanf и т. д.). Там больше проблем с точностью и ошибками, чем в железе.

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

Потребляя огромное количество логики, fpu, вероятно, будет очень быстрым (один тактовый цикл). Не медленнее, чем целое число alu. Не путайте это с тем, что современные fpus такие же простые, как alus, fpus дорогие. (alus также потребляет больше логики для умножения и деления, чтобы сократить это до одного такта, но это не так много, как fpu).

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

Этот ответ в FAQ по C++, вероятно, описывает его лучше всего:

http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.18

Дело не только в том, что разные архитектуры или компиляторы могут доставить вам проблемы, числа с плавающей запятой уже ведут себя странным образом в одной и той же программе. Как указано в FAQ, если y == x верен, это все еще может означать, что cos(y) == cos(x) будет ложным. Это связано с тем, что процессор x86 вычисляет значение с помощью 80 бит, в то время как значение хранится как 64 бит в памяти, поэтому вы в конечном итоге сравниваете усеченное 64-битное значение с полным 80-битным значением.

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

С практической точки зрения, это не так уж плохо, я мог воспроизвести простую математику с плавающей точкой с другой версией GCC на 32-битной версии Linux для бит, но в тот момент, когда я переключился на 64-битную версию Linux, результат был уже не таким. Демонстрационные записи, созданные на 32-битной версии, не будут работать на 64-битной и наоборот, но будут работать нормально при запуске на той же арке.

Поскольку ваш вопрос помечен как C#, стоит выделить проблемы, с которыми сталкивается .NET:

  1. Математика с плавающей запятой не ассоциативна - то есть не гарантируется, что (a + b) + c равен a + (b + c);
  2. Различные компиляторы оптимизируют ваш код по-разному, и это может потребовать изменения порядка арифметических операций.
  3. В .NET JIT-компилятор CLR будет компилировать ваш код на лету, поэтому компиляция зависит от версии .NET на машине во время выполнения.

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

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

См. Сообщение в блоге Шона Харгривза Является ли математика с плавающей запятой детерминированной? для дальнейшего обсуждения, относящегося к .NET.

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

  1. Создайте новое приложение WPF в Visual Studio версии 12.0.40629.00 с обновлением 5 и примите все параметры по умолчанию.
  2. Замените содержимое MainWindow.xaml.cs следующим образом:

    using System;
    using System.Windows;
    
    namespace WpfApplication1
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                Content = FooConverter.Convert(new Point(950, 500), new Point(850, 500));
            }
        }
    
        public static class FooConverter
        {
            public static string Convert(Point curIPJos, Point oppIJPos)
            {
                var ij = " Insulated Joint";
                var deltaX = oppIJPos.X - curIPJos.X;
                var deltaY = oppIJPos.Y - curIPJos.Y;
                var teta = Math.Atan2(deltaY, deltaX);
                string result;
                if (-Math.PI / 4 <= teta && teta <= Math.PI / 4)
                    result = "Left" + ij;
                else if (Math.PI / 4 < teta && teta <= Math.PI * 3 / 4)
                    result = "Top" + ij;
                else if (Math.PI * 3 / 4 < teta && teta <= Math.PI || -Math.PI <= teta && teta <= -Math.PI * 3 / 4)
                    result = "Right" + ij;
                else
                    result = "Bottom" + ij;
                return result;
            }
        }
    }
    
  3. Установите для конфигурации сборки значение «Release» и выполните сборку, но не выполняйте ее в Visual Studio.

  4. Дважды щелкните созданный exe, чтобы запустить его.
  5. Обратите внимание, что в окне отображается «Нижний изолированный стык».
  6. Теперь добавьте эту строку непосредственно перед «строковым результатом»:

    string debug = teta.ToString();
    
  7. Повторите шаги 3 и 4.

  8. Обратите внимание, что в окне отображается «Правое изолированное соединение».

Такое поведение было подтверждено на машине коллеги. Обратите внимание, что в окне постоянно отображается «Правое изолированное соединение», если выполняется одно из следующих условий: исполняемый файл запускается из Visual Studio, исполняемый файл был создан с использованием конфигурации отладки или «Предпочитать 32-разрядный» не отмечен в свойствах проекта.

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

Другие вопросы по теме