CPI (Curioso pero inútil)

En directo en la Red desde 11111010100

Google calculadora ¿no sabía restar?

Para el que no lo sepa, Google también funciona como calculadora. Si en la casilla de búsqueda introducis “5*9*7” os responderá 315. Además funciona para unidades de medida “100 lb in kg” (45.35 kilo) e incluso da un cambio de divisas “25 USD in EUR”(unos 20 €, dependiendo del día). Yo lo encuentro muy útil.

A todo esto, recibí un email de Carlos con una referencia de Blackonion en el que me comentaba que Google no sabe restar y me adjuntaba la explicación. Sí queréis hacer la prueba, buscad la siguiente operación 1-0.9-0.1, que en principio debería dar cero, y la respuesta que os dará es:

1-0.9-0.1 = 0*

Cero! En el mes desde que me lo enviaron (he andado muy liado, no he podido publicarlo antes), lo han corregido, pero antes daba un resultado mucho más interesante. Antes respondía -2.77555756*10-17 (el documento de prueba) Obviamente ese resultado no era cero, y se llenaron los blogs de gente diciendo ¡Google no sabe restar! ¡Google falla más que un pentium original! y cosas similares.

Pues bien, resulta que el problema no lo tiene Google, sino muchos otros lenguajes:

# Ruby: 1 – 0.9 – 0.1 = -2.77555756156289e-17
# PHP(4.4): 1 – 0.9 – 0.1 = -2.7755575615629e-17
# JavaScript:1 – 0.9 – 0.1 = -2.7755575615628914e-17
# VbScript: 1 – 0.9 – 0.1 = -2,77555756156289E-17
# Java2: 1 – 0.9 – 0.1 = -2.7755575615628914E-17
# Perl 5: 1 – 0.9 – 0.1 = -2.77555756156289e-017

y todos hacen la operación igual. ¿Porqué?

El standard IEEE 754 (IEEE = Institute of Electrical and Electronics Engineers, los de las Navidades según Remo) es un método de almacenar números en coma flotante compacto y fácil de usar. Lo utiliza Intel y la mayoría de programas para PC. Se guardan los números en formato binario. Y aquí está la clave de la cuestión.

El 1 se almacena exactamente. Pero algunos números que son sencillos en clave decimal como 0.1 no se pueden grabar de manera exacta en binario. Del mismo modo que 1/3 en decimal se convierte en 0.33333…, el número 1/10 (10 en notación decimal, no 10 binario) se convierte en periodico al ser expresado en binario: 0.1 (decimal) = .0001100011000111000111… (binario).

Como pasa con los números periodicos, el ordenador lo recorta al número de bits máximos que usa, y el número se redondea por 2.78E-17 al ser almacenado (yo aquí también he redondeado 😉 ).

Así que el ordenador hace el siguiente cálculo en realidad

1 – 0.9 – (0.1 + 2.78E-17) = -2.78E-17

Y ese resultado tiene mucho más sentido. En Ingeniería computacional esto es muy conocido, y por eso se intenta huir al máximo de operaciones de división del estilo A/(B-C), donde B y C pueden ser muy próximos. Si B-C se acerca a cero, surgen problemas de precisión graves en el resultado y el resultado de la división es aún más sensible a estos errores.

Al fin y al cabo, parece un error muy pequeño, ya que 2.78E-17 =0.0000000000000000278 y en la calculadora (o el bolsillo) eso no tiene mayor importancia, y si se redondean los resultados (como hace Google ahora) da un cero perfecto. Pero, de momento, ya ha dado para un post sobre .

* He descubierto que se puede reproducir el error cambiando los numeros:

100 – 99.9 – 0.1 = -5.689893 × 10-15

19 comentarios en “Google calculadora ¿no sabía restar?

  • Nixz dice:

    También sucede con:

    1000 – 999.9 – 0.1 = 2.27318164 × 10^(-14)

    y de ahí en adelante, se le pueden poner todas las cifras que queráis.

    Como curiosidad, la calculadora que viene con Ubuntu, ‘gcalctool’ (la que viene en el menú), da como resultado -0 (cero negativo). Y la otra calculadora que viene con Ubuntu, ‘xcalc’ (un poco más feílla) comete el mismo error que google… :S da exactamente:

    10 – 9.9 – 0.1 = -3,60822e-16

    En fin, ahí queda eso xD

    Saludos!

  • ^DiAmOnD^ dice:

    Yo escribi hace tiempo sobre el tema, pero los ejemplos que puse en mi post ya están arreglados.

    La verdad es que es muy curioso 😛

  • cedila dice:

    Es por esto que en un programa de computadora, uno no debe incluir comprobaciones de igualdad entre valores en coma flotante, como en este trocito de programa en C:

    float a = 0.1, b = 1.0, c = 0.9;

    if (a == b – c)
    puts(“¡Funciona! Qué raro”);
    else
    puts(“Ya me lo temía yo.”);

    la comprobación de igualdad

    if (a == b – c)

    fallará seguramente. Lo que se debe hacer es ponerlo de esta otra forma:

    if (fabs(a – (b – c)) < epsilon) donde fabs() es una función que devuelve el valor absoluto de un valor en coma flotante y epsilon es “un valor suficientemente pequeño”. Es una pena, pero es así. Los números que los computadores pueden manejar normalmente son sólo cierto subconjunto de los enteros y unos cuantos valores racionales, entre los que está el cero. Pero no se pueden representar cantidades con valor absoluto arbitrariamente pequeño ni arbitrariamente grande, y ciertos números racionales que en notación decimal tienen un número finito de cifras decimales, como 1/10, no tienen representación exacta en binario, en los circuitos del computador. Pasa como cuando queremos representar 1/3 en notación decimal, que hay que poner 0,33333… y en alguna parte hay que cortar, con el error de truncamiento correspondiente. Lo malo es cuando los errores de redondeo o truncamiento se propagan de cálculo en cálculo, tal vez amplificándose en cada nuevo ciclo de operaciones. Como los computadores pueden tener que hacer millones de operaciones dependientes una de otra para calcular un determinado resultado, hay veces que resulta muy difícil acotar el error. Con esas limitaciones aritméticas, la fiabilidad de los resultados dependerá de que los programadores sean conscientes de sus efectos y tomen medidas para paliarlos, pero esto, lamentablemente, no siempre sucede ¡Cualquiera se fía! :-)

  • Ger dice:

    A mi lo que me parece triste es que se permitan que el bendito Matlab tenga el mismo fallo … y hay veces, sobre todo en problemas en los que se involucran cantidades enormemente pequeñas, por ejemplo capacidades, donde este error de redondeo puede empezar a ser significativo

    Un saludo
    Grandioso estreno de sección

  • Ger dice:

    Se me ocurre un problema de magnetismo, donde puede aparecer valores de velocidades de la luz elevados a -2 (1.11e-17) ( eso en el mejor de los casos, con epsr = 1 y mur = 1). En este caso, el valor a hallar es menor que el redondeo, si no me equivoco

  • Serlio dice:

    No está directamente relacionado con la entrada, pero ayer descubrí casualmente que el buscador de MSN también soluciona ecuaciones (creo que es a través de Encarta), aunque me parece que sólo lo hace en su versión en inglés 😕

  • Remo dice:

    Efectivamente, Serlio:

    Encarta Answers also includes additional search functionality to help you with your homework or other everyday uses .
    • Definitions. Define a word or phrase, such as define lunar eclipse
    • Calculations. Calculate math problems, such as 2*5
    • Equations. Calculate math equations, such as 3x+4=10
    • Conversions. Get common conversions, such as what is 35 degrees Celsius in Fahrenheit

    Pero sólo resuelve ecuaciones polinómicas hata grado tres. Ni exponenciales, ni trigonométricas… Nada útil 😉

  • hairanakh dice:

    jop, Ger… pues yo con el Matlab sufro lo contrario. Como sólo sabe hacer cuentas con números de doble precisión (Double), cada vez que intento jugar con imágenes sufro mucho con la memoria.

    Por cierto, que para evitar esos problemas bastaría con limitar la precisión de las respuestas…

  • Heimy dice:

    Ger: El asunto es que no es un “fallo”. Es algo inherente a la representación de los números en el computador. Lo más que puedes hacer (y algunos lenguajes lo admiten, como Smalltalk), es utilizar tipos que representen números fraccionarios en lugar de decimales, y no convertirlos al valor real hasta el último momento. Pero esto es un poco ineficiente porque los cálculos en flotante los hace una unidad de cálculo del procesador, y las operaciones con fraccionarios los tienes que hacer tú “a mano”.

  • Tronfi dice:

    Acabo de hacer la prueba con Python 2.4 y pasa exactamente lo mismo :)

  • josemi dice:

    Recuerdo haber oido hablar de este tema alla por el año 84 u 85, una de las antiguas “muy interesante especial ordenadores”.

    En aquel momento, en la epoca del Commodore 64 y el Spectrum, cada modelo de BASIC manejaba los numeros como le daba la gana, aunque evidentemente todos tenian el problema.

    Lo peor es que parece que cada generacion tiene que descubrirlo de manera independiente :-)

  • deibyz dice:

    Ay… Qué recuerdos de esos primeros días de programador que ya dejé atrás…

    Como recuerdo volverme loco con los if()… Esas tardes a última hora llenándolo todo de sprintf()… Menos mal que desde hace mucho tiempo solo uso perl y un $#=”%.2f” lo arregla todo :)

    WN21@Deibyz /cygdrive/d$ perl -e ‘print ( 1 – .9 – .1 )’

    -2.77555756156289e-17

    WN21@Deibyz /cygdrive/d$ perl -e ‘$# = “%.2f”; print ( 1 – .9 – .1 )’
    0.00

    También recuerdo una práctica de C++ en la universidad en la que teníamos que definir una clase de float que almacenaba las cantidades en formato decimal (4bit por dígito si no recuerdo mal…). Lo dicho, ¡qué tiempos aquellos! Y qué recuerdos cada vez que un pogramador novel te viene completamente loco con un “bug” de este tipo… 😀

  • PseudoGoN dice:

    Esto nos lo explicaron a fondo el primer año de carrera (informática). Como bien dicen, es una consecuencia de la representación en punto flotante (que es similar a la notación científica de toda la vida pero en binario). El caso es que cuando se producen estos errores al principio de los cálculos, al arrastrarlos se pueden formar errores importantes, por lo que la única solución posible es cambiar el orden de los mismos y dejar para el final las restas y las divisiones.

    Por cierto, las divisiones si que traen tela. Mientras que el resto de operaciones, aun siendo en punto flotante, se pueden segmentar en fases, consiguiendo así que, por ejemplo, varias multiplicaciones se hagan simultaneamente (cada multiplicación está en una de las fases en las que la operación se ha segmentado), la división no se segmenta, por lo que por cada división puedes perder decenas de ciclos de reloj. También es verdad que un motivo importante por el que no se divide en suboperaciones es porque se usa más bien poco y no merece la pena gastar más recursos en ella.

    Lo que no se es si en procesadores más avanzados, la han segmentado (por el momento estamos estudiando procesadores bastante sencillos). ¿Alguién lo sabe?

    saludos

    saludos

  • javi dice:

    Llevo tiempo siguiendo esta pagina, dando al botón de cpi al azar he llegado aquí, posiblemente esta entrada no sea ni si quiera conocida por la antigüedad de la última, (vamos, que esta entrada esta mas abandonada que el TAV de Cantabria) pero bueno, dejo lo siguiente:

    1 – 0x9 – 0.1 = -8.1

    Si no me equivoco, 0.9=0 y 0.1=0, por lo tanto 1-0-0=1, mis matemáticas creo que no fallan.

  • Remo dice:

    Javi: No entiendo bien tus cálculos. No sé si es que se ha estropeado algo al publicar, pero lo de 0.9=0 no me cuadra…

  • javi dice:

    jaja, yo te lo explico, es una demostración más de que la calculadora no sabe realizar operaciones, por eso son operaciones al azar, y puse algo sencillo según se me ocurrió

  • Charlie1 dice:

    Es injusto abusar de la calculadora de google!! Para hacer multplicaciones debemos poner el asterico (*) en vez de la equis (x). Efectivamente javi, si escribes lo que tu pones en la calculadora de google da ese resultado (-8.1),pero google no entiende el simbolo “x” como multiplicacion. Si ponemos esto: 1-0*9-0.1 da el resultado correcto: 0.9. CPI LIFE FOREVER

  • Jose Piñeiro dice:

    @PseudoGoN

    Si lo que quieres es velocidad, la mejor forma de dividir es multiplicar.
    Por ejemplo, si en tu programa quries dividir entre 5, lo que debes hacer es multiplicar por 1/5 (es decir 0.2)
    Por tanto “a=b/5” es mucho mas lento que “a=b*0.2”

    El spectrum, que alguno lo mencinaba, cometia errores por otro motivo. Cuando tenemos poca potencia de calculo, multiplicar y dividir resulta sumamente lento. Para evitar este problema podemos hayar el loagritmos de los dos numeros, sumar los logaritmos y hyar el antilogaritmo. De esa manera hemos multiplicado.
    alog (log(x)+log(y)) = x*y
    alog (log(x)-log(y)) = x/y

    Obtener logaritmos y antilogaritmos es sencillo (solo hay que consultar una tabla). La suma y la resta son operaciones sencillas y rapidas.
    El unico problema es que nuestra precision queda limitada por la cantidad de decimales que tenga nuestra tabla de logaritmos.

    La mejor forma de dividir, ya que

  • Deja un comentario

    Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *