Precisión y precisión en los cálculos de punto flotante

Número de KB original: 125056

Resumen

Hay muchas situaciones en las que la precisión, el redondeo y la precisión en los cálculos de punto flotante pueden funcionar para generar resultados sorprendentes para el programador. Deben seguir las cuatro reglas generales:

  1. En un cálculo que implica una precisión única y doble, el resultado no suele ser más preciso que una precisión única. Si se requiere precisión doble, tenga en cuenta que todos los términos del cálculo, incluidas las constantes, se especifican con doble precisión.

  2. Nunca suponga que un valor numérico simple se representa con precisión en el equipo. La mayoría de los valores de punto flotante no se pueden representar con precisión como un valor binario finito. Por ejemplo, .1 está .0001100110011... en binario (se repite para siempre), por lo que no se puede representar con precisión completa en un equipo mediante aritmética binaria, que incluye todos los equipos.

  3. Nunca se supone que el resultado sea preciso para la última posición decimal. Siempre hay pequeñas diferencias entre la respuesta "verdadera" y lo que se puede calcular con la precisión finita de cualquier unidad de procesamiento de punto flotante.

  4. Nunca compare dos valores de punto flotante para ver si son iguales o no. Se trata de un corolario de la regla 3. Casi siempre va a haber pequeñas diferencias entre números que "deberían" ser iguales. En su lugar, compruebe siempre si los números son casi iguales. En otras palabras, compruebe si la diferencia entre ellos es pequeña o insignificante.

Más información

En general, las reglas descritas anteriormente se aplican a todos los lenguajes, incluidos C, C++ y ensamblador. En los ejemplos siguientes se muestran algunas de las reglas que usan FORTRAN PowerStation. Todos los ejemplos se compilaron con FORTRAN PowerStation 32 sin ninguna opción, excepto la última, que está escrita en C.

Ejemplo 1

El primer ejemplo muestra dos cosas:

  • Las constantes FORTRAN son de precisión única de forma predeterminada (las constantes C son de doble precisión de forma predeterminada).
  • Los cálculos que contienen términos de precisión únicos no son mucho más precisos que los cálculos en los que todos los términos son de precisión única.

Después de inicializarse con 1.1 (una sola constante de precisión), y es tan inexacto como una única variable de precisión.

x = 1.100000000000000  y = 1.100000023841858

El resultado de multiplicar un valor de precisión único por un valor de precisión doble precisa es casi tan malo como multiplicar dos valores de precisión únicos. Ambos cálculos tienen miles de veces más errores que multiplicar dos valores de precisión doble.

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ódigo de ejemplo

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

Ejemplo 2

El ejemplo 2 usa la ecuación cuadrática. Demuestra que incluso los cálculos de precisión doble no son perfectos y que el resultado de un cálculo debe probarse antes de que dependa de si los errores pequeños pueden tener resultados drásticos. La entrada a la función raíz cuadrada de la muestra 2 solo es ligeramente negativa, pero sigue siendo no válida. Si los cálculos de precisión doble no tuvieran errores leves, el resultado sería:

Root =   -1.1500000000

En su lugar, genera el siguiente error:

Error en tiempo de ejecución M6201: MATH

  • sqrt: error DE DOMINIO

Código de ejemplo

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

Ejemplo 3

El ejemplo 3 muestra que, debido a optimizaciones que se producen incluso si la optimización no está activada, los valores pueden conservar temporalmente una precisión mayor de la esperada y que no es prudente probar dos valores de punto flotante para la igualdad.

En este ejemplo, dos valores son iguales y no iguales. En el primer IF, el valor de Z sigue en la pila del coprocesador y tiene la misma precisión que Y. Por lo tanto, X no es igual a Y y se imprime el primer mensaje. En el momento del segundo IF, Z tenía que cargarse desde la memoria y, por lo tanto, tenía la misma precisión y valor que X, y el segundo mensaje también se imprime.

Código de ejemplo

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

Ejemplo 4

La primera parte del código de ejemplo 4 calcula la menor diferencia posible entre dos números cercanos a 1,0. Para ello, agrega un solo bit a la representación binaria de 1.0.

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

Algunas versiones de FORTRAN redondean los números al mostrarlos para que la imprecisión numérica inherente no sea tan obvia. Esta es la razón por la que x e y tienen el mismo aspecto cuando se muestran.

La segunda parte del código de ejemplo 4 calcula la menor diferencia posible entre dos números cercanos a 10,0. De nuevo, lo hace agregando un solo bit a la representación binaria de 10.0. Observe que la diferencia entre números cercanos a 10 es mayor que la diferencia cerca de 1. Esto demuestra el principio general de que cuanto mayor sea el valor absoluto de un número, menos preciso se puede almacenar en un número determinado de bits.

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

La representación binaria de estos números también se muestra para mostrar que difieren en solo 1 bit.

x = 4024000000000001 Hex
y = 4024000000000000 Hex

La última parte del código de ejemplo 4 muestra que los valores decimales simples no repetitivos a menudo solo se pueden representar en binario mediante una fracción repetible. En este caso x=1.05, que requiere un factor de repetición CCCCCCCC.... (Hexadecimal) en la mantisa. En FORTRAN, el último dígito "C" se redondea hasta "D" para mantener la mayor precisión posible:

x = 3FF0CCCCCCCCCCCD (Hex representation of 1.05D0)

Incluso después del redondeo, el resultado no es perfectamente preciso. Hay algún error después del dígito menos significativo, que podemos ver quitando el primer dígito.

x-1 = .05000000000000004

Código de ejemplo

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

Ejemplo 5

En C, las constantes flotantes son dobles de forma predeterminada. Use una "f" para indicar un valor float, como en "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
   }