Я понимаю, что вычисления с плавающей запятой имеют проблемы с точностью, и есть много вопросов, объясняющих, почему. У меня вопрос: если я проведу один и тот же расчет дважды, могу ли я всегда полагаться на него для получения одного и того же результата? Какие факторы могут на это повлиять?
У меня есть простое физическое моделирование, и я хочу записывать сеансы, чтобы их можно было воспроизвести. Если на вычисления можно положиться, тогда мне нужно будет записать только начальное состояние плюс любые данные, введенные пользователем, и я всегда должен иметь возможность точно воспроизвести конечное состояние. Если расчеты не точны, ошибки в начале могут иметь огромные последствия к концу моделирования.
В настоящее время я работаю в Silverlight, хотя мне было бы интересно узнать, можно ли ответить на этот вопрос в целом.
Обновлять: Первоначальные ответы указывают на «да», но, видимо, это не совсем однозначно, как обсуждалось в комментариях к выбранному ответу. Похоже, мне придется сделать несколько тестов и посмотреть, что произойдет.
числа с плавающей запятой, упорядоченные по их точности: десятичное, двойное, плавающее.
Это зависит от фазы Луны.





Короткий ответ заключается в том, что вычисления FP полностью детерминированы в соответствии с Стандарт IEEE с плавающей запятой, но это не означает, что они полностью воспроизводимы на разных машинах, компиляторах, ОС и т. д.
Подробный ответ на эти и многие другие вопросы можно найти в, вероятно, лучшем справочнике по плавающей запятой, Что должен знать каждый компьютерный ученый об арифметике с плавающей запятой Дэвида Голдберга. Перейдите к разделу, посвященному стандарту IEEE, чтобы узнать о ключевых деталях.
Чтобы кратко ответить на ваши пункты:
Время между расчетами и состоянием процессора имеют мало общего с это.
Оборудование может влиять на вещи (например, некоторые графические процессоры не работают). Соответствует IEEE с плавающей запятой).
Язык, платформа и ОС также могут влияют на вещи. Для лучшего описания этого, чем я могу предложить, см. Ответ Джейсона Уоткинса. Если вы используете Java, взгляните на разглагольствовать о несоответствиях с плавающей запятой в Java Кахана.
Надеюсь, солнечные вспышки могут иметь значение не часто. Я бы не стал слишком волноваться, потому что если они имеют значение, тогда все остальное тоже напортачило. Я бы отнес это к той же категории, что и беспокойство о EMP.
Наконец, если вы выполняете тот же последовательность вычислений с плавающей запятой на одних и тех же начальных входных данных, тогда все должно быть воспроизведено точно. Точная последовательность может меняться в зависимости от вашего компилятора / ОС / стандартной библиотеки, поэтому таким образом вы можете получить небольшие ошибки.
Проблемы с плавающей запятой обычно возникают в том случае, если у вас численно нестабильный метод и вы начинаете с входов FP, которые равны примерно, но не совсем. Если ваш метод стабилен, вы сможете гарантировать воспроизводимость в пределах некоторого допуска. Если вам нужны более подробные сведения, то взгляните на статью Goldberg FP по ссылке выше или возьмите вводный текст по численному анализу.
См. Мой ответ на @JaredPar, есть много вещей, которые могут вызвать расхождения между расчетами в двух IEEE-совместимых реализациях. Утверждение, что вычисления являются детерминированными, не особенно полезно, поскольку детерминированность не обязательно означает воспроизводимость.
Я думаю, что ваше замешательство кроется в типе неточности в отношении чисел с плавающей запятой. Большинство языков реализуют Стандарт IEEE с плавающей запятой. Этот стандарт определяет, как отдельные биты в пределах float / double используются для создания числа. Обычно число с плавающей запятой состоит из четырех байтов и двойных восьми байтов.
Математическая операция между двумя числами с плавающей запятой будет каждый раз иметь одно и то же значение (как указано в стандарте).
Неточность заключается в точности. Рассмотрим int против float. Оба обычно занимают одинаковое количество байтов (4). Однако максимальное значение, которое может хранить каждое число, сильно различается.
Разница посередине. int, может представлять любое число от 0 до примерно 2 миллиардов. Однако плавать не может. Он может представлять 2 миллиарда значений от 0 до 3,40282347E38. Но остается целый ряд значений, которые невозможно представить. Если математическое уравнение попадает в одно из этих значений, оно должно быть округлено до представимого значения и, следовательно, считается «неточным». Ваше определение неточного может отличаться :).
Это приукрашивает аспект воспроизводимости, который не так очевиден. IEEE дает определенные гарантии, но они основаны на строгих предположениях и не распространяются на все операции или библиотечные функции. @ jason-watkins хорошо объяснил основные проблемы в своем ответе.
Суть в том, что если вы используете ограниченные операции в одной и той же реализации (компьютер / компилятор / среда выполнения), вы, вероятно, сможете точно воспроизвести результаты, но очень вероятно, что результаты будут немного отличаться в разных реализациях, даже тех, которые поддерживают IEEE. -754.
Я думаю, что Роберт, вдохновленный комментариями Джейсона, должен быть добавлен к этому ответу.
Прочтите параграф. «Воспроизводимость» в вики-статье, ссылка на которую имеется здесь. Резюме: IEEE 754-1985 действительно ли нет гарантирует воспроизводимость между реализациями. 754-2008 поощряет это, но все же не требует его. Если в вашем языке используется 754, это почти наверняка будет версия до 2008 года.
Это хорошее объяснение точности с плавающей запятой в целом, но оно вообще не отвечает на заданный вопрос, ответ Джейсона Уоткина делает и, по моему мнению, должен быть принятым ответом.
<pedant> каждый номер естественный </pedant>
Насколько я понимаю, вам гарантированы идентичные результаты только при условии, что вы имеете дело с одним и тем же набором инструкций и компилятором, и что все процессоры, на которых вы работаете, строго придерживаются соответствующих стандартов (например, IEEE754). Тем не менее, если вы не имеете дело с особенно хаотической системой, любой дрейф в вычислениях между прогонами вряд ли приведет к ошибочному поведению.
Конкретные ошибки, о которых я знаю:
некоторые операционные системы позволяют устанавливать режим процессора с плавающей запятой таким образом, чтобы нарушить совместимость.
Промежуточные результаты с плавающей запятой часто используют 80-битную точность в регистре, но только 64-битную в памяти. Если программа перекомпилирована таким образом, что изменяет разлив регистров внутри функции, она может возвращать другие результаты по сравнению с другими версиями. Большинство платформ позволяют принудительно усекать все результаты с точностью до памяти.
стандартные библиотечные функции могут изменяться от версии к версии. Я так понимаю, что в gcc 3 vs 4 есть несколько нередко встречающихся примеров этого.
Сам IEEE позволяет некоторым двоичным представлениям различаться ... в частности, значениям NaN, но я не могу вспомнить подробности.
@ Джейсон Уоткинс: Существует только два логических представления NaN, тихий и сигнальный, однако существует множество двоичных представлений NaN. В противном случае +1 хороший материал.
№1 особенно важен в Windows. Были версии DirectX, которые переводили процессор в режим пониженной точности, что приводило к неожиданным результатам.
Что касается реализаций ЦП, это зависит от используемого вами языка. C довольно неспецифично в том, какой FP вы получите. C# и Java определяют семантику IEEE754, и тогда задача реализации - скрыть то, на что на самом деле способен процессор. Если бы я запускал Java на старом VAX или сломанном Pentium, я ожидал бы увидеть поведение IEEE754, несмотря на то, что это не то, что реализует процессор, потому что определение языка требует этого. Если бы я этого не сделал, JVM была бы сломана по определению.
Здесь много дополнительной информации: gafferongames.com/networking-for-game-programmers/…
@ijw: На самом деле, JVM намеренно нарушена для double / float. Вам нужно ключевое слово strictfp, чтобы сделать его строгим IEEE754, но это может замедлить работу программы.
Стоит отметить, что 80-битное оборудование предназначалось для использования с языками, которые позволяли бы хранить 80-битные значения в памяти. Предполагалось, что значения самый, хранящиеся в памяти, будут преобразованы с понижением в 64 бита, но временные результаты из общих подвыражений будут сохранены как 80. К сожалению, стандарт C не смог предоставить средства, с помощью которых функции с переменным аргументом могли указывать хотят ли они 64-битных или 80-битных чисел с плавающей запятой, и поставщики компиляторов решили, что самый простой способ избежать проблем совместимости - это сделать так, чтобы их тип «long double» фактически сохранял 64 бита.
Хотя многие люди думают, что 80-битный тип является причудой x87, на самом деле он был разработан для более быстрой работы, чем 64-битный двойной на машинах без модулей с плавающей запятой. «Классический» Macintosh никогда не использовал 8x87, но он выполнял вычисления с плавающей запятой почти так же, как 8x87.
Кроме того, хотя Гольдберг является отличным справочником, исходный текст также неверен: 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.
Извините, но я не могу избавиться от мысли, что все упускают из виду суть.
Если неточность существенна для того, что вы делаете, вам следует поискать другой алгоритм.
Вы говорите, что если расчеты неточны, ошибки в начале могут иметь огромные последствия к концу моделирования.
Что мой друг не симуляция. Если вы получаете очень разные результаты из-за крошечных различий из-за округления и точности, то велика вероятность, что ни один из результатов не является достоверным. Тот факт, что вы можете повторить результат, не делает его более достоверным.
При решении любой нетривиальной реальной проблемы, которая включает измерения или нецелочисленные вычисления, всегда рекомендуется вносить незначительные ошибки, чтобы проверить, насколько стабилен ваш алгоритм.
Нет, я думаю, вы упустили суть. Вопрос действительно в повторяемости, а не в точности.
В данном конкретном случае Энтони прав, я ищу повторяемость, а не точность, поскольку я пытаюсь создать что-то интересное, а не настоящую «симуляцию». Может быть, там можно было использовать более подходящее слово ... детская площадка?
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:
(a + b) + c равен a + (b + c);Это означает, что вам не следует полагаться на то, что ваше приложение .NET будет производить одни и те же результаты вычислений с плавающей запятой при запуске в разных версиях .NET CLR.
Например, в вашем случае, если вы записываете начальное состояние и входные данные для моделирования, а затем устанавливаете пакет обновления, который обновляет CLR, ваше моделирование может не воспроизводиться идентично при следующем запуске.
См. Сообщение в блоге Шона Харгривза Является ли математика с плавающей запятой детерминированной? для дальнейшего обсуждения, относящегося к .NET.
Это не полный ответ на ваш вопрос, но вот пример, демонстрирующий, что двойные вычисления в C# недетерминированы. Я не знаю почему, но, казалось бы, несвязанный код, очевидно, может повлиять на результат двойного вычисления в нисходящем направлении.
Замените содержимое 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;
}
}
}
Установите для конфигурации сборки значение «Release» и выполните сборку, но не выполняйте ее в Visual Studio.
Теперь добавьте эту строку непосредственно перед «строковым результатом»:
string debug = teta.ToString();
Повторите шаги 3 и 4.
Такое поведение было подтверждено на машине коллеги. Обратите внимание, что в окне постоянно отображается «Правое изолированное соединение», если выполняется одно из следующих условий: исполняемый файл запускается из Visual Studio, исполняемый файл был создан с использованием конфигурации отладки или «Предпочитать 32-разрядный» не отмечен в свойствах проекта.
Разобраться в том, что происходит, довольно сложно, поскольку любая попытка наблюдать за процессом, похоже, меняет результат.
В Silverlight вы имеете дело с JIT-компилятором - это означает, что математические операции могут автоматически использовать преимущества SSE, MMX и других специальных инструкций, и те или иные изменения могут изменить точный порядок выполнения инструкций: A + B + C могут не давать того же результат как C + B + A при использовании значений с плавающей запятой. В результате вы получите детерминированные результаты при работе на одном компьютере, но можете получить другие результаты на другом процессоре или даже немного другую конфигурацию системы.