Resultados inesperados al trabajar con enteros muy grandes en idiomas interpretados


192

Estoy tratando de obtener la suma de 1 + 2 + ... + 1000000000, pero yo estoy resultados divertidos en PHP y Node.js .

PHP

$sum = 0;
for($i = 0; $i <= 1000000000 ; $i++) {
    $sum += $i;
}
printf("%s", number_format($sum, 0, "", ""));   // 500000000067108992

Node.js

var sum = 0;
for (i = 0; i <= 1000000000; i++) {
    sum += i ;
}
console.log(sum); // 500000000067109000

La respuesta correcta se puede calcular usando

1 + 2 + ... + n = n(n+1)/2

Respuesta correcta = 500000000500000000 , así que decidí probar otro idioma.

VAMOS

var sum , i int64
for i = 0 ; i <= 1000000000; i++ {
    sum += i
}
fmt.Println(sum) // 500000000500000000

¡Pero funciona bien! Entonces, ¿qué hay de malo con mi código PHP y Node.js?

Quizás sea un problema de idiomas interpretados, y por eso funciona en un lenguaje compilado como Go. Si es así, ¿otros lenguajes interpretados como Python y Perl tendrían el mismo problema?


36
necesitas esto: php.net/manual/en/book.bc.php , o te golpearás la cabeza contra IEEE 754 hasta que el infierno se congele.
tereško

55
Para manejar grandes números en PHP (es decir, 64 bits), use las funciones GMP, en este caso gmp_add ().
Jeffrey

113
Para una super eficiencia, tus bucles realmente deberían comenzar en 1 en lugar de 0.: P
Graham Borland

55
sum (1 a N) = (N / 2) * (N + 1)
Phong

55
@Baba 0 es superfluo para su cálculo, por lo que no es necesario tener una iteración adicional del ciclo para agregar 0 a 0.
Brian Warshaw

Respuestas:


155

Python funciona:

>>> sum(x for x in xrange(1000000000 + 1))
500000000500000000

O:

>>> sum(xrange(1000000000+1))
500000000500000000

El intauto de Python se convierte en Python longque admite precisión arbitraria. Producirá la respuesta correcta en plataformas de 32 o 64 bits.

Esto se puede ver elevando 2 a una potencia mucho mayor que el ancho de bits de la plataforma:

>>> 2**99
633825300114114700748351602688L

Puede demostrar (con Python) que los valores erróneos que está obteniendo en PHP se deben a que PHP se está promoviendo a un valor flotante cuando los valores son mayores que 2 ** 32-1:

>>> int(sum(float(x) for x in xrange(1000000000+1)))
500000000067108992

¿Ejecutó esto en un sistema de 32 o 64 bits?
Baba

44
Debería funcionar independientemente (32 contra 64 bits) ya que los ints de Python se promocionan automáticamente con precisión arbitraria en lugar de desbordamiento. Puede que tarde un poco más.
dawg

3
Python en cualquier sistema funcionará en este caso, ya que Python cambia a enteros largos automáticamente si es necesario. Y si eso no es suficiente, también cambiará a enteros grandes.
Alok Singhal el

12
@ 0x499602D2: Eso es un poco duro. El propio OP lo votó. Preguntó específicamente si este era un problema similar en Python. Respuesta, no, no lo es. Código para mostrar que no lo es. WTH?
dawg

10
El ejemplo de Python es demasiado largo, solo use sum (xrange (int (1e9) +1)) (... sum funciona en iterables)
Jason Morgan

101

Su código Go usa aritmética de enteros con suficientes bits para dar una respuesta exacta. Nunca toqué PHP o Node.js, pero por los resultados sospecho que las matemáticas se hacen usando números de coma flotante y, por lo tanto, se espera que no sean exactos para números de esta magnitud.


46
Sí. If PHP encounters a number beyond the bounds of the integer type, it will be interpreted as a float instead. Also, an operation which results in a number beyond the bounds of the integer type will return a float instead.- php.net/manual/en/language.types.integer.php
Nate

3
Y en NodeJS (y JavaScript en general) todas las operaciones aritméticas (excepto las operaciones de bits) se comportan como si se hicieran con números de coma flotante. Si realmente lo son o no, es una distinción oculta sujeta a las decisiones de los motores JavaScript individuales.
Peter Olson

13
En la especificación de JavaScript, no hay tipos enteros. Todos los números son puntos flotantes.
toasted_flakes

8
@grasGendarme Hay. La especificación ES5 especifica varias conversiones de enteros y exige que se llamen en cambios bit a bit , por ejemplo. Es decir, detrás de escena , los tipos enteros se usan en Javascript, pero todos los operadores aritméticos convierten sus operandos en números de punto flotante antes de hacer algo con ellos (salvo las optimizaciones del compilador).
Peter Olson

2
aquí está el código que supongo que su mal estado, porque yo solía float64 y no Int64 .. acaba de confirmar que no tiene nada que ver con 32 o 64 bits
Baba

45

La razón es que el valor de su variable entera sumexcede el valor máximo. Y lo sumque obtienes es el resultado de la aritmética de punto flotante que implica redondear. Como otras respuestas no mencionaron los límites exactos, decidí publicarlo.

El valor entero máximo para PHP para:

  • La versión de 32 bits es 2147483647
  • La versión de 64 bits es 9223372036854775807

Por lo tanto, significa que está utilizando una CPU de 32 bits o un sistema operativo de 32 bits o una versión compilada de PHP de 32 bits. Se puede encontrar usando PHP_INT_MAX. Se sumcalcularía correctamente si lo hace en una máquina de 64 bits.

El valor entero máximo en JavaScript es 9007199254740992 . El valor integral exacto más grande con el que puede trabajar es 2 53 (tomado de esta pregunta ). El sumexcede este límite.

Si el valor entero no excede estos límites, entonces eres bueno. De lo contrario, deberá buscar bibliotecas de enteros de precisión arbitraria.


28

Aquí está la respuesta en C, para completar:

#include <stdio.h>

int main(void)
{
    unsigned long long sum = 0, i;

    for (i = 0; i <= 1000000000; i++)    //one billion
        sum += i;

    printf("%llu\n", sum);  //500000000500000000

    return 0;
}

La clave en este caso es usar el long long tipo de datos C99 . Proporciona el almacenamiento primitivo más grande que C puede administrar y se ejecuta muy, muy rápido. El long longtipo también funcionará en la mayoría de las máquinas de 32 o 64 bits.

Hay una advertencia: los compiladores proporcionados por Microsoft explícitamente no son compatibles con el estándar C99 de 14 años, por lo que hacer que esto se ejecute en Visual Studio es una trampa.


3
MSVC ++ es un compilador de C ++, y C ++ entró long longen el estándar C ++ 11. Sin embargo, ha sido una extensión MSVC ++ y g ++ durante algunos años.
MSalters

1
@MSalters Por lo tanto, al ser una característica de C ++, realmente no ayudará a nadie a compilar un programa C directo. Nunca intenté cambiar de C a C ++, así que no sé si esa solución realmente funcionaría.
CyberSkull

19
Y muy bien, GCC o Clang con optimizaciones convierten todo el ciclo enmovabsq $500000000500000000, %rsi
Tor Klingberg

3
Solo gcc -O3o clang -O3. No sé el nombre de la optimización específica. Básicamente, el compilador nota que el resultado del bucle no depende de ningún argumento, y lo calcula en tiempo de compilación.
Tor Klingberg

1
C99 long long tiene un tamaño mínimo de 64 bits y, que yo sepa, es de 64 bits en las plataformas de 32 y 64 bits. No he visto soporte general para quad u octo ints.
Devin Lane, el

21

Mi suposición es que cuando la suma excede la capacidad de un nativo int(2 31 -1 = 2,147,483,647), Node.js y PHP cambian a una representación de punto flotante y comienza a obtener errores de redondeo. Un lenguaje como Go probablemente tratará de mantener una forma entera (por ejemplo, enteros de 64 bits) el mayor tiempo posible (si, de hecho, no comenzó con eso). Como la respuesta cabe en un entero de 64 bits, el cálculo es exacto.


Node.js explícitamente no tiene un tipo int. Está trabajando en un tipo flotante.
greyfade

@greyfade - Sí, supongo que eso es cierto para todos los entornos compatibles con EcmaScript.
Ted Hopp

¿No es eso (2 ** 31 - 1)?
Zachary Hunter

@ZacharyHunter - De hecho lo es. Gracias por atrapar ese error.
Ted Hopp

19

El script de Perl nos da el resultado esperado:

use warnings;
use strict;

my $sum = 0;
for(my $i = 0; $i <= 1_000_000_000; $i++) {
    $sum += $i;
}
print $sum, "\n";  #<-- prints: 500000000500000000

3
¿Ejecutó esto en un sistema de 32 o 64 bits?
Baba

2
se ejecutó en un sistema de 64 bits
Miguel Prz

3
4.99999999067109e+017en Perl v5.16.1 MSWin32-x86.
Qtax

77
Si realmente necesita números grandes, use bignumo bigint. Ambos son módulos principales, es decir, se instalan con Perl v5.8.0 o superior. Ver http://perldoc.perl.org/bignum.htmlyhttp://perldoc.perl.org/bigint.html
shawnhcorey

Obtuve 500000000500000000 ejecutando esto en una Mac PPC de 32 bits, ejecutando Perl 5.12.4.
CyberSkull

17

La respuesta a esto es "sorprendentemente" simple:

Primero, como la mayoría de ustedes saben, un número entero de 32 bits oscila entre −2,147,483,648 y 2,147,483,647 . Entonces, ¿qué sucede si PHP obtiene un resultado, que es MÁS GRANDE que esto?

Por lo general, uno esperaría un "Desbordamiento" inmediato, que hace que 2,147,483,647 + 1 se convierta en −2,147,483,648 . Sin embargo, este no es el caso. SI PHP encuentra un número mayor, devuelve FLOAT en lugar de INT.

Si PHP encuentra un número más allá de los límites del tipo entero, se interpretará como un flotante. Además, una operación que resulta en un número más allá de los límites del tipo entero devolverá un flotante.

http://php.net/manual/en/language.types.integer.php

Dicho esto, y sabiendo que la implementación de PHP FLOAT sigue el formato de doble precisión IEEE 754, significa que PHP puede manejar números de hasta 52 bits, sin perder precisión. (En un sistema de 32 bits)

Entonces, en el punto, donde su suma llega a 9,007,199,254,740,992 (que es 2 ^ 53 ) El valor Float devuelto por PHP Maths ya no será lo suficientemente preciso.

E:\PHP>php -r "$x=bindec(\"100000000000000000000000000000000000000000000000000000\"); echo number_format($x,0);"

9,007,199,254,740,992

E:\PHP>php -r "$x=bindec(\"100000000000000000000000000000000000000000000000000001\"); echo number_format($x,0);"

9,007,199,254,740,992

E:\PHP>php -r "$x=bindec(\"100000000000000000000000000000000000000000000000000010\"); echo number_format($x,0);"

9,007,199,254,740,994

Este ejemplo muestra el punto, donde PHP está perdiendo precisión. Primero, el último bit de significación se descartará, lo que provocará que las 2 primeras expresiones den como resultado un número igual, que no lo son.

A partir de AHORA, toda la matemática saldrá mal cuando trabaje con tipos de datos predeterminados.

• ¿Es el mismo problema para otro lenguaje interpretado como Python o Perl?

No lo creo. Creo que este es un problema de idiomas que no tienen seguridad de escritura. Si bien se producirá un desbordamiento de enteros como se mencionó anteriormente en todos los idiomas que usan tipos de datos fijos, los idiomas sin seguridad de tipo podrían intentar detectar esto con otros tipos de datos. Sin embargo, una vez que alcanzan su frontera "natural" (dada por el sistema), pueden devolver cualquier cosa, pero el resultado correcto.

Sin embargo, cada idioma puede tener diferentes hilos para tal escenario.


15

Las otras respuestas ya explicaron lo que está sucediendo aquí (precisión de coma flotante como de costumbre).

Una solución es usar un tipo entero lo suficientemente grande, o esperar que el idioma elija uno si es necesario.

La otra solución es utilizar un algoritmo de suma que conozca el problema de precisión y lo solucione. A continuación encontrará la misma sumatoria, primero con un número entero de 64 bits, luego con coma flotante de 64 bits y luego usando punto flotante nuevamente, pero con el algoritmo de suma Kahan .

Escrito en C #, pero lo mismo vale para otros idiomas también.

long sum1 = 0;
for (int i = 0; i <= 1000000000; i++)
{
    sum1 += i ;
}
Console.WriteLine(sum1.ToString("N0"));
// 500.000.000.500.000.000

double sum2 = 0;
for (int i = 0; i <= 1000000000; i++)
{
    sum2 += i ;
}
Console.WriteLine(sum2.ToString("N0"));
// 500.000.000.067.109.000

double sum3 = 0;
double error = 0;
for (int i = 0; i <= 1000000000; i++)
{
    double corrected = i - error;
    double temp = sum3 + corrected;
    error = (temp - sum3) - corrected;
    sum3 = temp;
}
Console.WriteLine(sum3.ToString("N0"));
//500.000.000.500.000.000

El resumen de Kahan da un resultado hermoso. Por supuesto, lleva mucho más tiempo calcularlo. Si desea usarlo depende a) de sus necesidades de rendimiento frente a las de precisión, yb) cómo su idioma maneja los tipos de datos de enteros frente a coma flotante.


@Baba Es lo mismo que con Node.js / JavaScript en el OP. En cuanto a por qué 500000000067109000 vs. 500000000067108992 ... no tengo idea.
linac

Quizás Baba esté intrigado por el uso de puntos para miles de separadores: el inglés generalmente espera comas. También podría usar guiones bajos como una media más neutral.
didierc

14

Si tiene PHP de 32 bits, puede calcularlo con bc :

<?php

$value = 1000000000;
echo bcdiv( bcmul( $value, $value + 1 ), 2 );
//500000000500000000

En Javascript debe usar una biblioteca de números arbitrarios, por ejemplo BigInteger :

var value = new BigInteger(1000000000);
console.log( value.multiply(value.add(1)).divide(2).toString());
//500000000500000000

Incluso con lenguajes como Go y Java, eventualmente tendrá que usar una biblioteca de números arbitrarios, su número resultó ser lo suficientemente pequeño para 64 bits pero demasiado alto para 32 bits.


12

En rubí:

sum = 0
1.upto(1000000000).each{|i|
  sum += i
}
puts sum

Imprime 500000000500000000, pero tarda unos buenos 4 minutos en mi Intel i7 a 2.6 GHz.


Magnuss y Jaunty tienen una solución Ruby mucho más:

1.upto(1000000000).inject(:+)

Para ejecutar un punto de referencia:

$ time ruby -e "puts 1.upto(1000000000).inject(:+)"
ruby -e "1.upto(1000000000).inject(:+)"  128.75s user 0.07s system 99% cpu 2:08.84 total

10
1.upto (1000000000) .inject (: +)
Magnuss

@Magnuss: Eso es lo que pensé que intenté al principio, pero causó una pérdida masiva de memoria. El tuyo parece funcionar ...
cgenco

11

Uso node-bigint para cosas de enteros grandes:
https://github.com/substack/node-bigint

var bigint = require('bigint');
var sum = bigint(0);
for(var i = 0; i <= 1000000000; i++) { 
  sum = sum.add(i); 
}
console.log(sum);

No es tan rápido como algo que puede usar cosas nativas de 64 bits para esta prueba exacta, pero si obtiene números mayores que 64 bits, usa libgmp debajo del capó, que es una de las bibliotecas de precisión arbitraria más rápidas que existen.


4

tomó años en rubí, pero da la respuesta correcta:

(1..1000000000).reduce(:+)
 => 500000000500000000 

4

Para obtener el resultado correcto en php, creo que necesitaría usar los operadores matemáticos de BC: http://php.net/manual/en/ref.bc.php

Aquí está la respuesta correcta en Scala. Debe usar Longs, de lo contrario, desbordará el número:

println((1L to 1000000000L).reduce(_ + _)) // prints 500000000500000000

3

En realidad, hay un truco genial para este problema.

Supongamos que fue 1-100 en su lugar.

1 + 2 + 3 + 4 + ... + 50 +

100 + 99 + 98 + 97 + ... + 51

= (101 + 101 + 101 + 101 + ... + 101) = 101 * 50

Fórmula:

Para N = 100: Salida = N / 2 * (N + 1)

Para N = 1e9: Salida = N / 2 * (N + 1)

Esto es mucho más rápido que recorrer todos esos datos. Su procesador se lo agradecerá. Y aquí hay una historia interesante sobre este mismo problema:

http://www.jimloy.com/algebra/gauss.htm


11
¿Crees que sería posible cruzar todos los puentes a través del Pregel en Kaliningrado, sin cruzar ningún puente dos veces? Muchas personas lo han intentado y han fallado, pero nadie ha establecido que sea imposible. Esto parece un desafío que usted estaría calificado para resolver.
jwg

3

Esto da el resultado adecuado en PHP al forzar la conversión de enteros.

$sum = (int) $sum + $i;

3

Common Lisp es uno de los lenguajes * más rápidos de interpretar y maneja enteros arbitrariamente grandes correctamente de forma predeterminada. Esto toma alrededor de 3 segundos con SBCL :

* (time (let ((sum 0)) (loop :for x :from 1 :to 1000000000 :do (incf sum x)) sum))

Evaluation took:
  3.068 seconds of real time
  3.064000 seconds of total run time (3.044000 user, 0.020000 system)
  99.87% CPU
  8,572,036,182 processor cycles
  0 bytes consed

500000000500000000
  • Por interpretado, quiero decir, ejecuté este código desde REPL, SBCL puede haber hecho algunos JITing internamente para que se ejecute rápido, pero la experiencia dinámica de ejecutar código inmediatamente es la misma.

Se puede simplificar como (tiempo (bucle para x de 1 a 1000000000 suma x)). Obtuve ~ 5x velocidad agregando declaración: (tiempo (localmente (declare (optimizar (velocidad 3) (seguridad 0))) (bucle para i de tipo fijo de 1 a 1000000000 suma i de tipo fijo)))
huaiyuan

Esto es erróneo ¡No te dejes cegar por los otros idiomas! La forma correcta de escribirlo en lisp es: (defun sum-from-1-to-n (n) (/ (* n (1+ n)) 2)) (time (sum-from-1-to-n 1000000000)) tardó 14 microsegundos (0,000014 segundos) en ejecutarse. Durante ese período, y con 4 núcleos de CPU disponibles, se gastaron 0 microsegundos (0.000000 segundos) en modo de usuario 0 microsegundos (0.000000 segundos) se gastaron en modo sistema -> 500000000500000000
informatimago

@informatimago: no es erróneo. Estaba copiando el estilo de bucle imperativo de la pregunta y, como muchos han señalado, la pregunta en sí misma menciona que hay una manera más fácil de calcular. Chillax
postfuturista

3

No tengo suficiente reputación para comentar la respuesta Common Lisp de @ postfuturist, pero se puede optimizar para completar en ~ 500 ms con SBCL 1.1.8 en mi máquina:

CL-USER> (compile nil '(lambda () 
                        (declare (optimize (speed 3) (space 0) (safety 0) (debug 0) (compilation-speed 0))) 
                        (let ((sum 0))
                          (declare (type fixnum sum))
                          (loop for i from 1 to 1000000000 do (incf sum i))
                          sum)))
#<FUNCTION (LAMBDA ()) {1004B93CCB}>
NIL
NIL
CL-USER> (time (funcall *))
Evaluation took:
  0.531 seconds of real time
  0.531250 seconds of total run time (0.531250 user, 0.000000 system)
  100.00% CPU
  1,912,655,483 processor cycles
  0 bytes consed

500000000500000000

3

Racket v 5.3.4 (MBP; tiempo en ms):

> (time (for/sum ([x (in-range 1000000001)]) x))
cpu time: 2943 real time: 2954 gc time: 0
500000000500000000

1
Borré mi respuesta publicada 6 minutos después de ti, una vez que noté la tuya. :)
Greg Hendershott

3

Funciona bien en Rebol:

>> sum: 0
== 0

>> repeat i 1000000000 [sum: sum + i]
== 500000000500000000

>> type? sum
== integer!

Esto estaba usando Rebol 3 que, a pesar de estar compilado de 32 bits, usa enteros de 64 bits (a diferencia de Rebol 2, que usaba enteros de 32 bits)


3

Quería ver qué pasó en CF Script

<cfscript>
ttl = 0;

for (i=0;i LTE 1000000000 ;i=i+1) {
    ttl += i;
}
writeDump(ttl);
abort;
</cfscript>

Tengo 5.00000000067E + 017

Este fue un experimento bastante bueno. Estoy bastante seguro de que podría haber codificado esto un poco mejor con más esfuerzo.


3

ActivePerl v5.10.1 en ventanas de 32 bits, Intel Core2duo 2.6:

$sum = 0;
for ($i = 0; $i <= 1000000000 ; $i++) {
  $sum += $i;
}
print $sum."\n";

resultado: 5.00000000067109e + 017 en 5 minutos.

Con el script "use bigint" funcionó durante dos horas y funcionaría más, pero lo detuve. Demasiado lento.


¿Alguien puede confirmar que ese es realmente el tiempo que lleva agregar tantos bigints?
jwg

3

En aras de la integridad, en Clojure (hermoso pero no muy eficiente):

(reduce + (take 1000000000 (iterate inc 1))) ; => 500000000500000000

1
Lo único útil que tienen las respuestas de $ MY_FAVOURITE_LANGUAGE es si proporcionan el resultado ...
jwg

@jwg sí, lo siento, me perdí el final de la línea - respuesta actualizada.
Blacksad

3

AWK:

BEGIN { s = 0; for (i = 1; i <= 1000000000; i++) s += i; print s }

produce el mismo resultado incorrecto que PHP:

500000000067108992

Parece que AWK usa coma flotante cuando los números son realmente grandes, por lo que al menos la respuesta es el orden de magnitud correcto.

Pruebas de funcionamiento:

$ awk 'BEGIN { s = 0; for (i = 1; i <= 100000000; i++) s += i; print s }'
5000000050000000
$ awk 'BEGIN { s = 0; for (i = 1; i <= 1000000000; i++) s += i; print s }'
500000000067108992

2

Categoría otro lenguaje interpretado:

Tcl:

Si utiliza Tcl 8.4 o anterior, depende de si se compiló con 32 o 64 bits. (8.4 es el final de la vida).

Si usa Tcl 8.5 o más reciente que tiene enteros grandes arbitrarios, mostrará el resultado correcto.

proc test limit {
    for {set i 0} {$i < $limit} {incr i} {
        incr result $i
    }
    return $result
}
test 1000000000 

Puse la prueba dentro de un proceso para compilarlo en bytes.


2

Para el código PHP, la respuesta está aquí :

El tamaño de un número entero depende de la plataforma, aunque un valor máximo de aproximadamente dos mil millones es el valor habitual (es decir, 32 bits con signo). Las plataformas de 64 bits generalmente tienen un valor máximo de aproximadamente 9E18. PHP no admite enteros sin signo. El tamaño entero se puede determinar usando la constante PHP_INT_SIZE, y el valor máximo usando la constante PHP_INT_MAX desde PHP 4.4.0 y PHP 5.0.5.


2

Puerto:

proc Main()

   local sum := 0, i

   for i := 0 to 1000000000
      sum += i
   next

   ? sum

   return

Los resultados en 500000000500000000. (en ambas ventanas / mingw / x86 y osx / clang / x64)


2

Erlang funciona:

from_sum(From,Max) ->
    from_sum(From,Max,Max).
from_sum(From,Max,Sum) when From =:= Max ->
    Sum;
from_sum(From,Max,Sum) when From =/= Max -> 
    from_sum(From+1,Max,Sum+From).

Resultados: 41> inútil: from_sum (1,1000000000). 500000000500000000


2

Lo curioso, PHP 5.5.1 da 499999999500000000 (en ~ 30s), mientras que Dart2Js da 500000000067109000 (que es de esperar, ya que se ejecuta JS). CLI Dart da la respuesta correcta ... al instante.


2

Erlang también da el resultado esperado.

sum.erl:

-module(sum).
-export([iter_sum/2]).

iter_sum(Begin, End) -> iter_sum(Begin,End,0).
iter_sum(Current, End, Sum) when Current > End -> Sum;
iter_sum(Current, End, Sum) -> iter_sum(Current+1,End,Sum+Current).

Y usándolo:

1> c(sum).
{ok,sum}
2> sum:iter_sum(1,1000000000).
500000000500000000

2

Charla:

(1 to: 1000000000) inject: 0 into: [:subTotal :next | subTotal + next ]. 

"500000000500000000"
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.