Как я могу получить согласованное поведение программы при использовании float?

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

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

if (value <= 0.0f) something_happens();
Тем не менее, проблема возникла после некоторых недавних изменений, внесенных мной в программу, в которой я повторно упорядочил порядок выполнения определенных вычислений. В идеальном мире значения будут по-прежнему выходить одинаково после этой реорганизации, но из-за неточности представления с плавающей запятой они выходят очень немного иначе. Поскольку расчеты для каждого шага зависят от результатов предыдущего шага, эти незначительные изменения в результатах могут накапливаться до больших изменений по мере продолжения моделирования.

Вот простая примерная программа, которая демонстрирует описываемые мной явления:

float f1 = 0.000001f, f2 = 0.000002f;
f1 += 0.000004f; // This part happens first here
f1 += (f2 * 0.000003f);
printf("%.16f\n", f1);

f1 = 0.000001f, f2 = 0.000002f;
f1 += (f2 * 0.000003f);
f1 += 0.000004f; // This time this happens second
printf("%.16f\n", f1);

Выход этой программы

0.0000050000057854
0.0000050000062402

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

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

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

if (value < epsilon) something_happens();

но поскольку крошечные вариации в результатах накапливаются со временем, мне нужно сделать epsilon довольно большим (относительно говоря), чтобы гарантировать, что изменения не приведут к срабатыванию something_happens() на другом шаге. Есть ли лучший способ?

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

Примечание. Вместо этого использование целочисленных значений не является опцией.


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

Ответ 1

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

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

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

Один из способов сделать это может состоять в том, чтобы определить ваш базовый epsilon в случае, когда показатель степени равен 0 я e, значение, которое нужно сравнить, находится в диапазоне 1.0 <= x < 2,0. Предпочтительно, чтобы эпсилон выбирался таким образом, чтобы быть адаптированным к основанию 2, то есть значение, которое может быть точно представлено в формате с плавающей точкой с одной точностью - таким образом вы точно знаете, с чем вы тестируете, и не должны думать о проблемах округления в epsilon как Что ж. Для экспоненты -1 вы использовали бы ваш базовый эпсилон, разделенный на два, для -2, разделенных на 4 и так далее. Когда вы приближаетесь к самой низкой и самой высокой части диапазона экспонентов, вы постепенно теряете точность - побитно, поэтому вам нужно знать, что экстремальные значения могут привести к сбою метода epsilon.

Ответ 2

Я работал с имитационными моделями в течение 2 лет, а подход epsilon - самый надежный способ сравнить ваши поплавки.

Ответ 3

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

  • Если ваши значения находятся в известном диапазоне, вам и вам не нужны деления, вы можете масштабировать проблему и использовать точные операции над целыми числами. Как правило, условия не применяются.
  • Вариант заключается в использовании рациональных чисел для точных вычислений. Это все еще имеет ограничения на доступные операции и, как правило, имеет серьезные последствия для производительности: вы торгуете эффективностью для точности.
  • Режим округления можно изменить. Это может использоваться для вычисления интервала, а не отдельного значения (возможно, с тремя значениями, полученными в результате округления, округления и округления ближе всего). Опять же, это не сработает для всего, но вы можете получить оценку ошибки из этого.
  • Отслеживание значения и количества операций (возможных нескольких счетчиков) также может использоваться для оценки текущего размера ошибки.
  • Чтобы поэкспериментировать с различными числовыми представлениями (float, double, интервал и т.д.), вы можете реализовать свое моделирование в качестве шаблонов, параметризованных для числового типа.
  • Существует множество книг, написанных по оценке и минимизации ошибок при использовании арифметики с плавающей запятой. Это тема численной математики.

В большинстве случаев я кратко знаю эксперимент с некоторыми из вышеперечисленных методов и делаю вывод о том, что модель все равно неточна и не беспокоится об этом. Кроме того, выполнение чего-то другого, кроме использования float, может дать лучший результат, но слишком медленное, даже используя double из-за дублирования памяти и меньшей возможности использования операций SIMD.

Ответ 4

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

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

Ответ 5

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

Как правило, использование порога epsilon полезно только тогда, когда вы сравниваете два числа с плавающей запятой для равенства, а не когда вы сравниваете их, чтобы увидеть, что больше. Поэтому (для большинства моделей, по крайней мере), используя epsilon, вы ничего не получите вообще - он просто изменит набор перевернутых узлов, он не сделает это меньше. Если ваша модель сама хаотична, то она хаотична.