Точность и точность вычислений с плавающей запятой

Исходный номер базы знаний: 125056

Сводка

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

  1. При вычислении с одной и двойной точностью результат обычно не будет более точным, чем одиночная точность. Если требуется двойная точность, убедитесь, что все термины в вычислении, включая константы, указываются с двойной точностью.

  2. Никогда не предполагайте, что простое числовое значение точно представлено на компьютере. Большинство значений с плавающей запятой нельзя точно представить как конечное двоичное значение. Например, .1 является .0001100110011... двоичным (он повторяется навсегда), поэтому его нельзя представить с полной точностью на компьютере с помощью двоичной арифметики, которая включает все компьютеры.

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

  4. Никогда не сравнивайте два значения с плавающей запятой, чтобы узнать, равны ли они. Это является следствием правила 3. Почти всегда будут небольшие различия между числами, которые "должны" быть равными. Вместо этого всегда проверка, чтобы узнать, являются ли числа почти равными. Иными словами, проверка, чтобы увидеть, мала ли разница между ними или незначительна.

Дополнительные сведения

Как правило, описанные выше правила применяются ко всем языкам, включая C, C++ и ассемблер. В приведенных ниже примерах демонстрируются некоторые правила, использующие FORTRAN PowerStation. Все примеры были скомпилированы с помощью FORTRAN PowerStation 32 без каких-либо параметров, за исключением последнего, написанного на языке C.

Пример 1

В первом примере демонстрируются две вещи:

  • По умолчанию константы FORTRAN имеют одну точность (по умолчанию константы C имеют двойную точность).
  • Вычисления, содержащие какие-либо одноточные термины точности, не намного более точны, чем вычисления, в которых все термины имеют единую точность.

После инициализации с 1.1 (одной константой точности) значение y будет таким же неточным, как и одна переменная точности.

x = 1.100000000000000  y = 1.100000023841858

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

true = 1.320000000000000 (multiplying 2 double precision values)
y    = 1.320000052452087 (multiplying a double and a single)
z    = 1.320000081062318 (multiplying 2 single precision values)

Пример кода

C Compile options: none

       real*8 x,y,z
       x = 1.1D0
       y = 1.1
       print *, 'x =',x, 'y =', y
       y = 1.2 * x
       z = 1.2 * 1.1
       print *, x, y, z
       end

Пример 2

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

Root =   -1.1500000000

Вместо этого возникает следующая ошибка:

Ошибка времени выполнения M6201: MATH

  • sqrt: ошибка ДОМЕНА

Пример кода

C Compile options: none

       real*8 a,b,c,x,y
       a=1.0D0
       b=2.3D0
       c=1.322D0
       x = b**2
       y = 4*a*c
       print *,x,y,x-y
       print "(' Root =',F16.10)",(-b+dsqrt(x-y))/(2*a)
       end

Пример 3

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

В этом примере два значения равны и не равны. В первом случае значение Z по-прежнему находится в стеке сопроцессора и имеет ту же точность, что и Y. Таким образом, X не равен Y и выводится первое сообщение. Во время второго if, Z должен был быть загружен из памяти и, следовательно, имел ту же точность и значение, что и X, и второе сообщение также печатается.

Пример кода

C Compile options: none

       real*8 y
       y=27.1024D0
       x=27.1024
       z=y
       if (x.ne.z) then
         print *,'X does not equal Z'
       end if
       if (x.eq.z) then
         print *,'X equals Z'
       end if
       end

Пример 4

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

x   = 1.00000000000000000  (one bit more than 1.0)
y   = 1.00000000000000000  (exactly 1.0)
x-y =  .00000000000000022  (smallest possible difference)

Некоторые версии FORTRAN округляют числа при их отображении, чтобы присущая числовая точность не была столь очевидной. Поэтому при отображении x и y выглядят одинаково.

Вторая часть примера кода 4 вычисляет наименьшую возможную разницу между двумя числами, близкими к 10,0. Опять же, это делается путем добавления одного бита в двоичное представление 10.0. Обратите внимание, что разница между числами около 10 больше, чем разница рядом с 1. Это демонстрирует общий принцип, согласно которому чем больше абсолютное значение числа, тем менее точно оно может храниться в заданном количестве битов.

x   = 10.00000000000000000  (one bit more than 10.0)
y   = 10.00000000000000000  (exactly 10.0)
x-y =   .00000000000000178

Двоичное представление этих чисел также отображается, чтобы показать, что они отличаются только на 1 бит.

x = 4024000000000001 Hex
y = 4024000000000000 Hex

В последней части примера кода 4 показано, что простые неповторяющиеся десятичные значения часто могут быть представлены в двоичном формате только повторяющейся дроби. В этом случае x=1,05, для которого требуется повторяющийся коэффициент CCCCCCCC.... (Шестнадцатеричный) в мантиссе. В FORTRAN последняя цифра "C" округляется до "D" для поддержания максимально возможной точности:

x = 3FF0CCCCCCCCCCCD (Hex representation of 1.05D0)

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

x-1 = .05000000000000004

Пример кода

C Compile options: none

       IMPLICIT real*8 (A-Z)
       integer*4 i(2)
       real*8 x,y
       equivalence (i(1),x)

       x=1.
       y=x
       i(1)=i(1)+1
       print "(1x,'x  =',F20.17,'  y=',f20.17)", x,y
       print "(1x,'x-y=',F20.17)", x-y
       print *

       x=10.
       y=x
       i(1)=i(1)+1
       print "(1x,'x  =',F20.17,'  y=',f20.17)", x,y
       print "(1x,'x-y=',F20.17)", x-y
       print *
       print "(1x,'x  =',Z16,' Hex  y=',Z16,' Hex')", x,y
       print *

       x=1.05D0
       print "(1x,'x  =',F20.17)", x
       print "(1x,'x  =',Z16,' Hex')", x
       x=x-1
       print "(1x,'x-1=',F20.17)", x
       print *
       end

Пример 5

В C по умолчанию плавающие константы имеют значение doubles. Используйте "f", чтобы указать значение с плавающей точкой, как в "89,95f".

/* Compile options needed: none
*/ 

#include <stdio.h>

void main()
   {
      float floatvar;
      double doublevar;

/* Print double constant. */ 
      printf("89.95 = %f\n", 89.95);      // 89.95 = 89.950000

/* Printf float constant */ 
      printf("89.95 = %f\n", 89.95F);     // 89.95 = 89.949997

/*** Use double constant. ***/ 
      floatvar = 89.95;
      doublevar = 89.95;

printf("89.95 = %f\n", floatvar);   // 89.95 = 89.949997
      printf("89.95 = %lf\n", doublevar); // 89.95 = 89.950000

/*** Use float constant. ***/ 
      floatvar = 89.95f;
      doublevar = 89.95f;

printf("89.95 = %f\n", floatvar);   // 89.95 = 89.949997
      printf("89.95 = %lf\n", doublevar); // 89.95 = 89.949997
   }