Лучший алгоритм для предотвращения потери точности?

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

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

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

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

например, вот одна проблема:

f(x) = tan(x) − sin(x)  when x ~ 0

каков наилучший и худший алгоритм для оценки этого из этих трех вариантов:

(a) (1/ cos(x) − 1) sin(x),
(b) (x^3)/2
(c) tan(x)*(sin(x)^2)/(cos(x) + 1).

Я понимаю, что когда x близок к нулю, tan(x) и sin(x) почти одинаковы. Я не понимаю, как и почему любой из этих алгоритмов лучше или хуже подходит для решения проблемы.

4 ответов


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

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

a = 1,000,000;
do 100,000,000 time:
   a += 0.01;

если это 0.01 меньше, чем самая низкая цифра мантиссы, тогда цикл ничего не делает, и конечный результат a == 1,000,000 но если вы сделаете это так:

a = 0;
do 100,000,000 time:
   a += 0.01;
a += 1,000,000;

чем низкое число медленно растет, и вы, скорее всего, получите что-то близкое к a == 2,000,000, что является правильным ответом.
Это, конечно, экстремальный пример, но я надеюсь, что вы поймете эту идею.


IEEE 754 является стандартом с плавающей запятой, обычно реализуемым современными процессорами. Полезно понять основы этого, так как это дает вам много интуиции о том, что не делать. Упрощенное объяснение этого заключается в том, что компьютеры хранят числа с плавающей запятой в чем-то вроде научной нотации base-2 с фиксированным числом цифр (бит) для экспоненты и для мантисса. Это означает, что чем больше абсолютное значение числа, тем менее точно он может быть представлен. Для 32-битных поплавков в IEEE 754 половина возможных битовых шаблонов представляет от -1 до 1, хотя числа до 10^38 представимы с 32-битным поплавком. Для значений больше 2^24 (приблизительно 16.7 миллионов) 32-битный float не может точно представлять все целые числа.

что это означает для вас, что вы вообще хотите избежать следующий:

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

    for (float index = 17000000; index

этот цикл никогда не завершится, потому что 17,000,000 + 1 округляется до 17,000,000. Если у вас что-то например:

float foo = 10000000 - 10000000.0001

значение foo будет 0, а не -0.0001 из-за ошибки округления.


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

есть несколько способов, которыми вы можете иметь серьезную или даже катастрофическую потерю точности.

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

например (мы используем десятичные типы для демонстрации):

2.598765000000000000000000000100 -

2.598765000000000000000000000099

интересной частью является ответ 100-99 = 1. Как 2.598765 равна в обоих случаях не изменяет результат, но тратит 8 цифр. Гораздо хуже, потому что компьютер не делает знайте, что цифры бесполезны, он вынужден хранить его и запихивает 21 нуль после него, трата на все 29 цифр. К сожалению, нет способа обойти его для различий, но есть и другие случаи, например exp (x)-1, который является функцией, очень часто встречающейся в физике.

функция exp около 0 почти линейна, но она обеспечивает 1 в качестве ведущей цифры. Так с 12 значимых цифр exp(0.001)-1 = 1.00100050017 - 1 = 1.00050017 e-3

Если мы используем вместо функции expm1 (), используйте ряд Тейлора:

1 + x +x^2/2 +x^3/6 ... -1 =

x +x^2/2 +x^3/6 =: expm1 (x)

expm1 (0.001) = 1.00500166667 e-3

намного лучше.

вторая проблема-это функции с очень крутым наклоном, такие как касательная x вблизи pi/2. tan (11) имеет наклон 50000, что означает, что любое небольшое отклонение, вызванное ошибками округления раньше будет усилено фактором 50000 ! Или у вас есть особенности, если, например, результат приближается к 0/0, это означает, что он может иметь любое значение.

в обоих случаях вы создаете заменить функция, simplying исходной функции. Бесполезно выделять различные подходы к решению, потому что без обучения вы просто не "увидите" проблему в первую очередь.

очень хорошая книга для изучения и обучения: Forman S. Acton: Real Computing made real


еще одна вещь, чтобы избежать вычитания чисел, которые почти равны, так как это также может привести к повышенной чувствительности к ошибке округления. Для значений около 0 cos(x) будет близок к 1, поэтому 1/cos (x) - 1 является одним из тех вычитаний, которых вы хотели бы избежать, если это возможно, поэтому я бы сказал, что (a) следует избегать.