Я работаю над проектом научных расчетов и визуализации в C#/.NET, и мы используем double
s для представления всех физических количеств. Так как числа с плавающей запятой всегда включают немного округления, у нас есть простые методы сделать сравнения равенства, такие как:
static double EPSILON = 1e-6;
bool ApproxEquals(double d1, double d2) {
return Math.Abs(d1 - d2) < EPSILON;
}
Довольно стандартный.
Однако мы постоянно имеем необходимость для корректировки величины EPSILON
поскольку мы встречаемся с ситуациями, в которых ошибка "равных" количеств больше, чем мы ожидали. Например, если Вы умножаете 5 больших double
s вместе и затем делятся 5 раз, Вы теряете большую точность. Это перешло к сути дела, где мы не можем сделать ЭПСИЛОН слишком много больше, или иначе это собирается дать нам ложные положительные стороны, но мы все еще получаем ложные отрицательные стороны также.
В целом наш подход должен был искать более численно стабильные алгоритмы для работы с, но программа очень вычислительна и существует только так, мы смогли сделать.
У кого-либо есть какие-либо хорошие стратегии контакта с этой проблемой? Я изучил Decimal
введите немного, но касаюсь производительности, и я действительно не знаю достаточно об этом, чтобы знать, решило ли это проблему или только затенило бы его. Я был бы готов принять умеренный хит производительности (скажите, 2x) путем движения в Decimal
если это решило бы эти проблемы, но производительность является определенно беспокойством и так как код главным образом ограничен арифметикой с плавающей точкой, я не думаю, что это - неблагоразумное беспокойство. Я видел, что люди заключают в кавычки 100x различие, которое определенно было бы недопустимо.
Кроме того, переключение на Decimal
имеет другие сложности, такие как общее отсутствие поддержки в Math
библиотека, таким образом, мы должны были бы записать нашу собственную функцию квадратного корня, например.
Совет?
Править: между прочим, то, что я использую постоянный эпсилон (вместо относительного сравнения) не является точкой моего вопроса. Я просто поместил это там как пример, это не на самом деле отрывок моего кода. Изменение на относительное сравнение не имело бы значения для вопроса, потому что проблема является результатом проигрывающей точности, когда числа становятся очень большими и затем маленькими снова. Например, у меня могло бы быть значение 1000, и затем я делаю ряд вычислений на нем, которые должны привести к точно тому же числу, но из-за потери точности я на самом деле имею 1001. Если я затем иду для сравнения тех чисел, не имеет значения очень, если я использую относительное или абсолютное сравнение (как долго, поскольку я определил сравнения способом, которые значимы для проблемы и масштаба).
Так или иначе, как предложенный Mitch Wheat, переупорядочение алгоритмов действительно помогало с проблемами.
Это проблема не только .NET. Стратегия уменьшения потери точности состоит в том, чтобы изменить порядок вычислений, чтобы вы умножили большие количества на малые и добавляли / вычитали количества аналогичного размера (очевидно, без изменения характера вычислений).
В вашем примере, вместо того, чтобы умножать 5 больших количеств вместе и затем делить на 5 больших количеств, измените порядок, чтобы разделить каждое большое количество на один из делителей, а затем умножьте эти 5 частичных результатов.
Интересно? (если вы еще не читали): Что должен знать каждый компьютерный ученый об арифметике с плавающей запятой
Конечно, лучший ответ - это всегда лучшие алгоритмы. Но мне кажется, что если ваши значения не все находятся в пределах нескольких порядков величины от 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++, то вы могли бы также использовать некоторые трюки для раздельного сравнения мантиссы и экспоненты, но я не могу придумать никакого способа сделать это безопасно в неизменяемом коде.
Из-за того, как обычно представляются вещественные числа, вы можете сделать это в C (и, вероятно, в небезопасном C#):
if (llabs(*(long long)&x - *(long long)&y) <= EPSILON) {
// Close enough
}
Это, очевидно, не переносимо и, вероятно, плохая идея, но у этого есть существенное преимущество - независимость от масштаба. То есть, EPSILON может быть какой-то небольшой константой, например, 1, 10 или 100 (в зависимости от желаемого допуска), и он будет корректно обрабатывать ошибки пропорционального округления независимо от экспоненты.
Отказ от ответственности: Это мое личное изобретение, которое не было проверено кем-либо, кто имеет представление об этом (например, математиком с опытом работы в дискретной арифметике).