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


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

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

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("%.16fn", f1);

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

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

0.0000050000057854
0.0000050000062402

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

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

Единственное решение, которое я пока смог придумать, - это используйте некоторое значение epsilon, например:

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

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

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


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

5 11

5 ответов:

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

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

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

Одним из способов сделать это может быть определение вашего базового Эпсилона в случае, когда показатель степени равен 0, то есть сравниваемое значение находится в диапазоне 1.0

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

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

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

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

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

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

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

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