Работа с ошибками с плавающей запятой в .NET - PullRequest
8 голосов
/ 12 февраля 2010

Я работаю над научным проектом по вычислениям и визуализации в C # /. NET, и мы используем double s для представления всех физических величин. Так как числа с плавающей точкой всегда включают в себя немного округления, у нас есть простые методы для сравнения на равенство, такие как:

static double EPSILON = 1e-6;

bool ApproxEquals(double d1, double d2) {
    return Math.Abs(d1 - d2) < EPSILON;
}

Довольно стандартно.

Однако нам постоянно приходится корректировать величину EPSILON, когда мы сталкиваемся с ситуациями, в которых погрешность «равных» величин больше, чем мы ожидали. Например, если вы умножите 5 больших double с вместе, а затем разделите 5 раз, вы потеряете много точности. Дошло до того, что мы не можем сделать EPSILON слишком большим, иначе это даст нам ложные срабатывания, но мы все равно получим ложные отрицания.

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

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

Кроме того, переключение на Decimal имеет другие сложности, такие как общее отсутствие поддержки в библиотеке Math, поэтому мы должны написать нашу собственную функцию квадратного корня, например.

Любой совет?

РЕДАКТИРОВАТЬ: кстати, тот факт, что я использую постоянный эпсилон (вместо относительного сравнения) не является вопросом моего вопроса. Я просто привожу это в качестве примера, на самом деле это не фрагмент моего кода. Изменение относительного сравнения не будет иметь значения для вопроса, потому что проблема возникает из-за потери точности, когда числа становятся очень большими, а затем снова маленькими. Например, у меня могло бы быть значение 1000, и затем я делаю серию вычислений, которые должны привести к тому же самому числу, но из-за потери точности у меня фактически есть 1001. Если я тогда иду, чтобы сравнить те числа, это не Не имеет большого значения, использую ли я относительное или абсолютное сравнение (при условии, что я определил сравнения таким образом, чтобы они имели значение для проблемы и масштаба).

В любом случае, как предложил Митч Уит, изменение порядка алгоритмов помогло с проблемами.

Ответы [ 3 ]

5 голосов
/ 12 февраля 2010

Это не проблема, уникальная для .NET. Стратегия уменьшения потери точности состоит в том, чтобы переупорядочить вычисления так, чтобы вы умножали большие количества на малые и добавляли / вычитали аналогичные по размеру величины (без изменения характера расчета, очевидно).

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

Интересно? (если вы еще не читали): Что должен знать каждый учёный об арифметике с плавающей точкой

2 голосов
/ 12 февраля 2010

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

// are values equal to within 12 (or so) digits of precision?
//
bool ApproxEquals(double d1, double d2) {
    return Math.Abs(d1 - d2) < (Math.Abs(d1) * 1e-12);
}

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

1 голос
/ 12 февраля 2010

Из-за того, как обычно представлены реальные числа, Вы можете сделать это в C (и, вероятно, в небезопасном C #):

if (llabs(*(long long)&x - *(long long)&y) <= EPSILON) {
    // Close enough
}

Это явно непереносимая и, вероятно, плохая идея, но она обладает значительным преимуществом независимости от масштаба. То есть EPSILON может быть некоторой небольшой константой, такой как 1, 10 или 100 (в зависимости от вашего желаемого допуска), и он будет правильно обрабатывать ошибки пропорционального округления независимо от показателя степени.

Отказ от ответственности: это мое собственное частное изобретение, и никто его не проверял (например, математик с фоном в дискретной арифметике).

...