logo
Языки программирования

9.3. Три смертных греха

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

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

0.1234 х 103 + 0.1234 х 10-4 = 0.1234 х 103

Маловероятно, что преподаватель средней школы учил вас, что х + у = х для ненулевого у, но именно это здесь и произошло!

Умножение ошибки — это большая абсолютная ошибка, которая может появиться при использовании арифметики с плавающей точкой, даже если относительная ошибка мала. Обычно это является результатом умножения деления. Рассмотрим вычисление х • х:

0.1234 х103 • 0.1234 х 103 = 0.1522 х 105

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

0.1235 х 103 • 0.1235 х 103 = 0.1525 х 105

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

Наиболее грубая ошибка — полная потеря значимости, вызванная вычита­нием почти равных чисел:

C


float f1= 0.12342;

float f2 = 0.12346;

B математике f2 -f1 = 0.00004, что, конечно, вполне представимо как четы­рехразрядное число с плавающей точкой: 0.4000 х 10-4. Однако программа, вы-числяющая f2 - f 1 в четырехразрядном представлении с плавающей точкой, даст ответ:

0.1235 10°-0.1234x10° = 0.1000 х 10-3

что даже приблизительно не является приемлемым ответом.

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

C

f2=...;

f2=…;

if (f1 ==f2)...

Самая невинная перестройка выражений для f 1 и f2, независимо от того, сде­лана она программистом или оптимизатором, может вызвать переход в услов­ном операторе по другой ветке. Правильный способ проверки равенства с плавающей точкой состоит в том, чтобы ввести малую величину:

C

#define Epsilon10e-20

if ((fabs(f2-f1))<Epsilon)...

и затем сравнить абсолютное значение разности с малой величиной. По той же самой причине нет существенного различия между < = и < при вычислени­ях с плавающей точкой.

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

1234.0 + 0.5678 + 0.5678 = 1234.0

лучше делать как:

0.5678 + 0.5678 + 1234.0 = 1235.0

чтобы не было исчезновения слагаемых.

В качестве другого примера рассмотрим арифметическое тождество:

(х+у)(х-у)=х22

и используем его для улучшения точности вычисления:

X, Y: Float_4;

Z: Float_7;

Ada

Z := Float_7((X + Y)*(X - Y)); -- Так считать?

Z := Float_7(X*X - Y*Y); -- или так?

Если мы положим х = 1234.0 и у = 0.6, правильное значение этого выражения будет равно 1522755.64. Результаты, вычисленные с точностью до восьми цифр, таковы:

(1234.0 + 0.6) • (1234.0-0.6) =1235.0 • 1233.0=1522755.0

и

(1234.0 • 1234.0)-(0.6 • 0.6) = 1522756.0-0.36 =1522756.0

При вычислении (х + у) (х- у) небольшая ошибка, являющаяся результа­том сложения и вычитания, значительно возрастает при умножении. При вычислении по формуле х2 - у2 уменьшается ошибка от исчезновения слагаемого и результат получается более точным.