El malentendido de la aritmética de coma flotante y sus defectos es una de las principales causas de sorpresa y confusión en la programación (considere el número de preguntas sobre el desbordamiento de pila relacionadas con "números que no suman correctamente"). Teniendo en cuenta que muchos programadores aún no han entendido sus implicaciones, tiene el potencial de introducir muchos errores sutiles (especialmente en el software financiero). ¿Qué pueden hacer los lenguajes de programación para evitar sus dificultades para aquellos que no están familiarizados con los conceptos, mientras ofrecen su velocidad cuando la precisión no es crítica para aquellos que sí entienden los conceptos?
language-design
Adam Paynter
fuente
fuente
Respuestas:
Usted dice "especialmente para el software financiero", lo que saca a relucir una de mis manías: el dinero no es un flotador, es un int .
Claro, parece un flotador. Tiene un punto decimal allí. Pero eso es solo porque estás acostumbrado a unidades que confunden el problema. El dinero siempre viene en cantidades enteras. En Estados Unidos, son centavos. (En ciertos contextos, creo que pueden ser molinos , pero ignore eso por ahora).
Entonces, cuando dices $ 1.23, eso es realmente 123 centavos. Siempre, siempre, siempre haz tus cálculos en esos términos, y estarás bien. Para más información, ver:
Respondiendo la pregunta directamente, los lenguajes de programación deberían incluir un tipo de dinero como una primitiva razonable.
actualizar
Ok, debería haber dicho "siempre" dos veces, en lugar de tres veces. El dinero es siempre un int; los que piensan lo contrario pueden enviarme 0,3 centavos y mostrarme el resultado en su extracto bancario. Pero como señalan los comentaristas, hay raras excepciones cuando necesitas hacer cálculos de coma flotante en números similares a dinero. Por ejemplo, ciertos tipos de precios o cálculos de intereses. Incluso entonces, esos deben ser tratados como excepciones. El dinero entra y sale como cantidades enteras, por lo que cuanto más se acerque su sistema a eso, más seguro será.
fuente
Decimal
es el único sistema sensato para lidiar con esto, y su comentario "ignorar eso por ahora" es el heraldo de la fatalidad para los programadores de todas partes: PProporcionar soporte para un tipo decimal ayuda en muchos casos. Muchos idiomas tienen un tipo decimal, pero están infrautilizados.
Comprender la aproximación que ocurre cuando se trabaja con representación de números reales es importante. Usar ambos tipos de coma decimal y coma flotante
9 * (1/9) != 1
es una declaración correcta. Cuando las constantes, un optimizador puede optimizar el cálculo para que sea correcto.Proporcionar un operador aproximado ayudaría. Sin embargo, tales comparaciones son problemáticas. Tenga en cuenta que .9999 billones de dólares es aproximadamente igual a 1 billón de dólares. ¿Podría depositar la diferencia en mi cuenta bancaria?
fuente
0.9999...
billones de dólares es exactamente igual a 1 billón de dólares en realidad.0.99999...
. Todos se truncan en algún momento, lo que resulta en una desigualdad.0.9999
es lo suficientemente igual para la ingeniería. Para fines financieros no lo es.Cuando fui a la universidad, nos dijeron qué hacer en la clase de primer año (segundo año) de ciencias de la computación (este curso también era un requisito previo para la mayoría de los cursos de ciencias)
Recuerdo que el profesor dijo: "Los números en coma flotante son aproximaciones. Use tipos enteros para obtener dinero. Use FORTRAN u otro lenguaje con números BCD para un cálculo preciso". (y luego señaló la aproximación, usando ese ejemplo clásico de 0.2 imposible de representar con precisión en coma flotante binaria). Esto también apareció esa semana en los ejercicios de laboratorio.
Misma conferencia: "Si debe obtener más precisión del punto flotante, ordene sus términos. Agregue números pequeños, no números grandes". Eso se me quedó grabado.
Hace unos años tenía una geometría esférica que necesitaba ser muy precisa y rápida. El doble de 80 bits en las PC no era suficiente, así que agregué algunos tipos al programa que clasificaron los términos antes de realizar operaciones conmutativas. Problema resuelto.
Antes de quejarse de la calidad de la guitarra, aprenda a tocar.
Hace cuatro años, tuve un compañero de trabajo que había trabajado para JPL. Expresó su incredulidad porque usamos FORTRAN para algunas cosas. (Necesitábamos simulaciones numéricas súper precisas calculadas fuera de línea). "Reemplazamos todo ese FORTRAN con C ++", dijo con orgullo. Dejé de preguntarme por qué se perdieron un planeta.
fuente
1.0 + 0.1 + ... + 0.1
(repetido 10 veces) regresa a1.0
medida que se redondea cada resultado intermedio. Hacerlo al revés, se obtiene resultados intermedios de0.2
,0.3
, ...,1.0
y finalmente2.0
. Este es un ejemplo extremo, pero con números realistas de coma flotante, ocurren problemas similares. La idea base es que agregar números de tamaño similar conduce al error más pequeño. Comience con los números más pequeños ya que su suma es mayor y, por lo tanto, es más adecuada para sumar a los más grandes.No creo que se pueda o deba hacer nada a nivel de idioma.
fuente
Decimal
cuando se trata de pruebas de igualdad. La diferencia entre1.0m/7.0m*7.0m
y1.0m
puede ser muchos órdenes de magnitud menor que la diferencia entre1.0/7.0*7.0
, pero no es cero.De forma predeterminada, los idiomas deben usar razones de precisión arbitraria para números no enteros.
Aquellos que necesitan optimizar siempre pueden pedir flotadores. Usarlos por defecto tiene sentido en C y otros lenguajes de programación de sistemas, pero no en la mayoría de los lenguajes populares hoy en día.
fuente
double
. Si un cálculo necesita ser exacto a una parte por millón, es mejor gastar un microsegundo informándolo dentro de unas pocas partes por billón, que gastar un segundo informándolo de manera absolutamente precisa.Los dos mayores problemas relacionados con los números de coma flotante son:
El primer tipo de falla solo puede remediarse proporcionando un tipo compuesto que incluya información sobre el valor y la unidad. Por ejemplo, un valor
length
oarea
que incorpora la unidad (metros o metros cuadrados o pies y pies cuadrados respectivamente). De lo contrario, debe ser diligente para trabajar siempre con un tipo de unidad de medida y solo para convertir a otro cuando compartimos la respuesta con un humano.El segundo tipo de falla es una falla conceptual. Las fallas se manifiestan cuando la gente piensa en ellas como números absolutos . Afecta las operaciones de igualdad, los errores de redondeo acumulativo, etc. Por ejemplo, puede ser correcto que para un sistema dos mediciones sean equivalentes dentro de un cierto margen de error. Es decir .999 y 1.001 son más o menos lo mismo que 1.0 cuando no le importan las diferencias que son más pequeñas que +/- .1. Sin embargo, no todos los sistemas son tan indulgentes.
Si se necesita alguna facilidad de nivel de idioma, entonces lo llamaría precisión de igualdad . En NUnit, JUnit y marcos de prueba construidos de manera similar, puede controlar la precisión que se considera correcta. Por ejemplo:
Si, por ejemplo, C # o Java se modificaron para incluir un operador de precisión, podría verse así:
Sin embargo, si proporciona una característica como esa, también debe considerar el caso en que la igualdad es buena si los lados +/- no son iguales. Por ejemplo, + 1 / -10 consideraría dos números equivalentes si uno de ellos estuviera dentro de 1 más, o 10 menos que el primer número. Para manejar este caso, es posible que también deba agregar una
range
palabra clave:fuente
¿Qué pueden hacer los lenguajes de programación? No sé si hay una respuesta a esa pregunta, porque cualquier cosa que el compilador / intérprete haga en nombre del programador para facilitarle la vida generalmente va en contra del rendimiento, la claridad y la legibilidad. Creo que tanto la forma C ++ (pague solo por lo que necesita) como la forma Perl (principio de menor sorpresa) son válidas, pero depende de la aplicación.
Los programadores aún necesitan trabajar con el lenguaje y comprender cómo maneja los puntos flotantes, porque si no lo hacen, harán suposiciones, y un día el comportamiento perscrito no coincidirá con sus suposiciones.
Mi opinión sobre lo que el programador necesita saber:
fuente
Utilice valores predeterminados razonables, por ejemplo, soporte integrado para decimales.
Groovy hace esto bastante bien, aunque con un poco de esfuerzo aún puede escribir código para introducir imprecisión de coma flotante.
fuente
Estoy de acuerdo en que no hay nada que hacer a nivel de idioma. Los programadores deben comprender que las computadoras son discretas y limitadas, y que muchos de los conceptos matemáticos representados en ellas son solo aproximaciones.
No importa el punto flotante. Hay que entender que la mitad de los patrones de bits se usan para números negativos y que 2 ^ 64 es en realidad bastante pequeño para evitar problemas típicos con la aritmética de enteros.
fuente
x
==y
no implica que realizar un cálculo enx
producirá el mismo resultado que realizar el mismo cálculo eny
).Una cosa que podrían hacer los idiomas: eliminar la comparación de igualdad de los tipos de punto flotante que no sea una comparación directa con los valores NAN.
La prueba de igualdad solo existiría es como una llamada de función que tomó los dos valores y un delta, o para lenguajes como C # que permiten que los tipos tengan métodos un EqualsTo que toma el otro valor y el delta.
fuente
Me resulta extraño que nadie haya señalado el truco racional de la familia Lisp.
En serio, abre sbcl y haz esto:
(+ 1 3)
y obtienes 4. Si*( 3 2)
obtienes 6. Ahora intenta(/ 5 3)
y obtienes 5/3, o 5 tercios.Eso debería ayudar un poco en algunas situaciones, ¿no?
fuente
Una cosa que me gustaría ver sería un reconocimiento de que
double
afloat
debe ser considerada como una conversión de ampliación, mientrasfloat
quedouble
se está estrechando (*). Eso puede parecer contrario a la intuición, pero considere lo que realmente significan los tipos:Si uno tiene una
double
que tiene la mejor representación de la cantidad "una décima" y la conviertefloat
, el resultado será "13,421,773.5 / 134,217,728, más o menos 1 / 268,435,456 más o menos", que es una descripción correcta del valor.Por el contrario, si uno tiene una
float
que tiene la mejor representación de la cantidad "una décima" y la conviertedouble
, el resultado será "13,421,773.5 / 134,217,728, más o menos 1 / 72,057,594,037,927,936 más o menos" - un nivel de precisión implícita lo cual está mal por un factor de más de 53 millones.Aunque el estándar IEEE-744 requiere que las matemáticas de punto flotante se realicen como si cada número de punto flotante representara la cantidad numérica exacta precisamente en el centro de su rango, eso no debe suponerse que los valores de punto flotante realmente representan esos números exactos cantidades numéricas Más bien, el requisito de que se suponga que los valores están en el centro de sus rangos se deriva de tres hechos: (1) los cálculos deben realizarse como si los operandos tuvieran algunos valores precisos particulares; (2) los supuestos consistentes y documentados son más útiles que los inconsistentes o indocumentados; (3) si uno va a hacer una suposición consistente, ninguna otra suposición consistente es mejor que asumir que una cantidad representa el centro de su rango.
Por cierto, recuerdo que hace unos 25 años, a alguien se le ocurrió un paquete numérico para C que usaba "tipos de rango", cada uno de los cuales constaba de un par de flotadores de 128 bits; todos los cálculos se realizarían de tal manera que se calcule el valor mínimo y máximo posible para cada resultado. Si se realiza un cálculo iterativo largo y grande y se obtiene un valor de [12.53401391134 12.53902812673], se puede estar seguro de que si bien se perdieron muchos dígitos de precisión debido a errores de redondeo, el resultado aún podría expresarse razonablemente como 12.54 (y no fue así) t realmente 12.9 o 53.2). Me sorprende que no haya visto ningún soporte para estos tipos en ningún lenguaje convencional, especialmente porque parecería encajar bien con unidades matemáticas que pueden operar en múltiples valores en paralelo.
(*) En la práctica, a menudo es útil usar valores de doble precisión para mantener cálculos intermedios cuando se trabaja con números de precisión simple, por lo que tener que usar un tipo de letra para todas esas operaciones podría ser molesto. Los idiomas podrían ayudar al tener un tipo de "doble difuso", que realizaría cálculos como dobles, y podría emitirse libremente desde y hacia el sencillo; Esto sería especialmente útil si las funciones que toman parámetros de tipo
double
y retornodouble
pudieran marcarse de modo que generen automáticamente una sobrecarga que acepte y devuelva "doble difuso".fuente
Si más lenguajes de programación tomaran una página de las bases de datos y permitieran a los desarrolladores especificar la longitud y precisión de sus tipos de datos numéricos, podrían reducir sustancialmente la probabilidad de errores relacionados con el punto flotante. Si un lenguaje permitiera a un desarrollador declarar una variable como Float (2), lo que indica que necesita un número de coma flotante con dos dígitos decimales de precisión, podría realizar operaciones matemáticas con mucha más seguridad. Si lo hiciera representando la variable como un número entero internamente y dividiéndola por 100 antes de exponer el valor, podría mejorar la velocidad al usar las rutas aritméticas de números enteros más rápidos. La semántica de un Float (2) también permitiría a los desarrolladores evitar la necesidad constante de redondear los datos antes de generarlos, ya que un Float (2) redondearía los datos inherentemente a dos puntos decimales.
Por supuesto, necesitaría permitir que un desarrollador solicite un valor de punto flotante de máxima precisión cuando el desarrollador necesita tener esa precisión. Y presentaría problemas en los que expresiones ligeramente diferentes de la misma operación matemática producen resultados potencialmente diferentes debido a operaciones de redondeo intermedias cuando los desarrolladores no tienen suficiente precisión en sus variables. Pero al menos en el mundo de las bases de datos, eso no parece ser un gran problema. La mayoría de las personas no están haciendo el tipo de cálculos científicos que requieren mucha precisión en los resultados intermedios.
fuente
Float(2)
No debería llamarse un " like" que usted proponeFloat
, ya que no hay nada flotando aquí, ciertamente no es el "punto decimal".Estos anteriores son aplicables en algunos casos, pero no son realmente una solución general para tratar con valores flotantes. La solución real es comprender el problema y aprender a lidiar con él. Si está utilizando cálculos de coma flotante, siempre debe verificar si sus algoritmos son numéricamente estables . Hay un campo enorme de matemática / informática que se relaciona con el problema. Se llama análisis numérico .
fuente
Como han señalado otras respuestas, la única forma real de evitar las trampas de coma flotante en el software financiero es no usarlo allí. Esto puede ser factible si proporciona una biblioteca bien diseñada dedicada a las matemáticas financieras .
Las funciones diseñadas para importar estimaciones de punto flotante deben etiquetarse claramente como tales y proporcionarse con parámetros apropiados para esa operación, por ejemplo:
La única forma real de evitar dificultades de coma flotante en general es la educación: los programadores necesitan leer y comprender algo como Lo que todo programador debe saber sobre la aritmética de coma flotante .
Sin embargo, algunas cosas que podrían ayudar:
isNear()
función.fuente
La mayoría de los programadores se sorprenderían de que COBOL acertara ... en la primera versión de COBOL no había coma flotante, solo decimal, y la tradición en COBOL continuó hasta hoy que lo primero que piensas al declarar un número es decimal. .. el punto flotante solo se usaría si realmente lo necesitaras. Cuando apareció C, por alguna razón, no había un tipo decimal primitivo, así que, en mi opinión, ahí es donde comenzaron todos los problemas.
fuente