Контакт с ошибками с плавающей точкой в.NET

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

static double EPSILON = 1e-6;

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

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

Однако мы постоянно имеем необходимость для корректировки величины EPSILON поскольку мы встречаемся с ситуациями, в которых ошибка "равных" количеств больше, чем мы ожидали. Например, если Вы умножаете 5 больших doubles вместе и затем делятся 5 раз, Вы теряете большую точность. Это перешло к сути дела, где мы не можем сделать ЭПСИЛОН слишком много больше, или иначе это собирается дать нам ложные положительные стороны, но мы все еще получаем ложные отрицательные стороны также.

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

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

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

Совет?

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

Так или иначе, как предложенный Mitch Wheat, переупорядочение алгоритмов действительно помогало с проблемами.

8
задан Hank 15 February 2010 в 15:52
поделиться

3 ответа

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

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

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

5
ответ дан 5 December 2019 в 20:16
поделиться

Конечно, лучший ответ - это всегда лучшие алгоритмы. Но мне кажется, что если ваши значения не все находятся в пределах нескольких порядков величины от 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++, то вы могли бы также использовать некоторые трюки для раздельного сравнения мантиссы и экспоненты, но я не могу придумать никакого способа сделать это безопасно в неизменяемом коде.

2
ответ дан 5 December 2019 в 20:16
поделиться

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

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

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

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

1
ответ дан 5 December 2019 в 20:16
поделиться
Другие вопросы по тегам:

Похожие вопросы: