Стратегия борьбы с неточностями с плавающей запятой - PullRequest
3 голосов
/ 23 сентября 2011

Существует ли общая стратегия наилучшей практики для устранения неточностей с плавающей запятой?

Проект, над которым я работаю, пытался решить их, обернув все в класс Unit, который содержит значение с плавающей запятой и перегружает операторы. Числа считаются равными, если они "достаточно близки", сравнения типа> или <выполняются путем сравнения с немного более низким или более высоким значением. </p>

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

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

Ответы [ 4 ]

2 голосов
/ 23 сентября 2011

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

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

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

struct RatioCompare {
  bool operator()(float lhs, float rhs) const;
};

struct EpsilonCompare {
  bool operator()(float lhs, float rhs) const;
};

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

std::sort(prices.begin(), prices.end(), EpsilonCompare());
std::sort(prices.begin(), prices.end(), RatioCompare());

Обычно люди, пытающиеся перегрузить операторы, чтобы избежать этих вещей, будут жаловаться на «хорошие значения по умолчанию» и т. Д. Если компилятор сразу скажет вам, что по умолчанию нет, это легко исправить. Если клиент говорит вам, что что-то не так в ваших миллионных строках расчета цены, это будет немного сложнее отследить. Это может быть особенно опасно, если в какой-то момент кто-то изменил поведение по умолчанию.

2 голосов
/ 23 сентября 2011
1 голос
/ 24 октября 2011

Для меня ошибки с плавающей запятой - это, по сути, ошибки, которые в x86 приводят к исключению с плавающей запятой (при условии, что сопроцессор имеет это прерывание включенным).Особый случай - «неточное» исключение, т. Е. Когда результат не был точно представлен в формате с плавающей точкой (например, при делении 1 на 3).Новички, которых еще нет дома в мире с плавающей запятой, будут ожидать точных результатов и будут считать этот случай ошибкой.

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

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

, если назвать только несколько.

Когда я написал собственный движок баз данных более двадцатиНесколько лет назад, используя 80286 с сопроцессором 80287, я выбрал форму поздней проверки данных и использования примитивных операций x87.Поскольку операции с плавающей запятой были относительно медленными, я хотел избегать сравнения с плавающей запятой каждый раз, когда загружал значение (некоторые из которых могли вызвать исключения).Чтобы достичь этого, мои значения с плавающей запятой (двойной точности) представляли собой объединения с целыми числами без знака, так что я проверял бы значения с плавающей запятой, используя операции x86, прежде чем были вызваны операции x87.Это было громоздко, но целочисленные операции были быстрыми, и когда операции с плавающей запятой вступили в действие, рассматриваемое значение с плавающей запятой было бы готово в кеше.

Типичная последовательность C (деление двух матриц с плавающей запятой) выгляделачто-то вроде этого:

// calculate source and destination pointers

type1=npx_load(src1pointer);
if (type1!=UNKNOWN)    /* x87 stack contains negative, zero or positive value */
{
  type2=npx_load(src2pointer);
  if (!(type2==POSITIVE_NOT_0 || type2==NEGATIVE))
  {
    if (type2==ZERO) npx_pop();
    npx_pop();    /* remove src1 value from stack since there won't be a division */
    type1=UNKNOWN;
  }
  else npx_divide();
}
if (type1==UNKNOWN) npx_load_0();   /* x86 stack is empty so load zero */
npx_store(dstpointer);    /* store either zero (from prev statement) or quotient as result */

npx_load будет загружать значение в верхнюю часть стека x87, если оно допустимо.В противном случае вершина стека будет пустой.npx_pop просто удаляет значение, которое в данный момент находится на вершине x87.Кстати, «npx» является аббревиатурой от «Расширения числового процессора», как его иногда называли.

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

Конечно, это решение приводило к накладным расходам, но о чистом

*dstpointer = *src1pointer / *src2pointer;

не могло быть и речи, поскольку оно не содержало обработки ошибок.Дополнительные затраты на обработку ошибок были более чем компенсированы тем, как были подготовлены указатели на значения.Кроме того, случай 99% (оба значения действительны) довольно быстрый, так что если дополнительная обработка для других случаев медленнее, ну и что?

1 голос
/ 23 сентября 2011

Обе техники не хороши. См. эту статью.

Google Test - это фреймворк для написания тестов C ++ на различных платформах.

gtest.h содержит функцию NearEquals .

  // Returns true iff this number is at most kMaxUlps ULP's away from
  // rhs.  In particular, this function:
  //
  //   - returns false if either number is (or both are) NAN.
  //   - treats really large numbers as almost equal to infinity.
  //   - thinks +0.0 and -0.0 are 0 DLP's apart.
  bool AlmostEquals(const FloatingPoint& rhs) const {
    // The IEEE standard says that any comparison operation involving
    // a NAN must return false.
    if (is_nan() || rhs.is_nan()) return false;

    return DistanceBetweenSignAndMagnitudeNumbers(u_.bits_, rhs.u_.bits_)
        <= kMaxUlps;
  }

Реализация Google хороша, быстра и независима от платформы.

Небольшая документация здесь .

...