Неожиданные результаты при работе с очень большими целыми числами на интерпретируемых языках

197

Я пытаюсь получить сумму 1 + 2 + ... + 1000000000, но я получаю смешные результаты в PHP и 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

Правильный ответ можно рассчитать, используя

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

Правильный ответ = 500000000500000000, поэтому я решил попробовать другой язык.

GO

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

Но все работает отлично! Итак, что не так с моим PHP и Node.js кодом?

Возможно, это проблема интерпретируемых языков и почему она работает на компилированном языке, таком как Go? Если да, будут ли другие интерпретируемые языки, такие как Python и Perl, иметь такую ​​же проблему?

  • 36
    вам нужно это: php.net/manual/en/book.bc.php , иначе вы будете биться головой о IEEE 754, пока ад не замерзнет.
  • 0
    bcadd почти навсегда .. Я должен был отказаться
Показать ещё 26 комментариев
Теги:
integer-overflow
precision
integer-arithmetic

36 ответов

163

Работает Python:

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

Или:

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

Python int автоматически продвигается к Python long, который поддерживает произвольную точность. Он даст правильный ответ на 32 или 64-битных платформах.

Это можно увидеть, подняв 2 на мощность, намного превышающую ширину бита платформы:

>>> 2**99
633825300114114700748351602688L

Вы можете продемонстрировать (с Python), что ошибочные значения, которые вы получаете в PHP, - это то, что PHP продвигает к float, когда значения больше, чем 2 ** 32-1:

>>> int(sum(float(x) for x in xrange(1000000000+1)))
500000000067108992
  • 0
    Вы запускали это на 32- или 64-битной системе?
  • 0
    64-битная сборка на OS X. Занимает около 25 секунд.
Показать ещё 8 комментариев
106

В коде Go используется целочисленная арифметика с достаточным количеством бит, чтобы дать точный ответ. Никогда не касался PHP или Node.js, но из результатов, которые я подозреваю, математика выполняется с использованием чисел с плавающей запятой, и поэтому ожидается, что это не будет быть точным для чисел этой величины.

  • 46
    Ага. 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
  • 3
    А в NodeJS (и JavaScript в целом) все арифметические операции (кроме битовых операций) ведут себя так, как если бы они выполнялись с числами с плавающей запятой. Независимо от того, являются ли они на самом деле, является скрытым отличием в зависимости от решений отдельных движков JavaScript.
Показать ещё 14 комментариев
48

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

Максимальное целочисленное значение для PHP для:

  • 32-разрядная версия 2147483647
  • 64-разрядная версия 9223372036854775807

Таким образом, это означает, что вы используете 32-битную ЦП или 32-битную ОС или 32-битную скомпилированную версию PHP. Его можно найти с помощью PHP_INT_MAX. sum будет правильно рассчитан, если вы сделаете это на 64-битной машине.

Максимальное целочисленное значение в JavaScript 9007199254740992. Наибольшее точное целочисленное значение, с которым вы можете работать, - 2 53 (взято из этого question). sum превышает этот предел.

Если целочисленное значение не превышает эти пределы, тогда вы добры. В противном случае вам придется искать произвольные целые библиотеки точности.

30

Вот ответ на C, для полноты:

#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;
}

Ключ в этом случае использует C99 long long тип данных. Он обеспечивает самое большое примитивное хранилище C, которое может работать и работает очень быстро. Тип long long также будет работать на большинстве 32 или 64-битных машин.

Существует одно предостережение: компиляторы, предоставленные Microsoft, явно не поддерживают 14-летний стандарт C99, поэтому его запуск в Visual Studio - это crapshot.

  • 3
    MSVC ++ - это компилятор C ++, а C ++ стал long long в стандарте C ++ 11. Это было расширение MSVC ++ и g ++ в течение нескольких лет.
  • 0
    @MSalters Какой выпуск Visual Studio получил это? Я думаю, что последняя версия, которую я использовал, была текущей от 3 или 4 лет назад.
Показать ещё 12 комментариев
22

Мое предположение заключается в том, что когда сумма превышает емкость нативного int (2 32 -1 = 2,147,483,647), Node.js и PHP переключаются на представление с плавающей запятой, и вы начните получать ошибки округления. Язык, подобный Go, вероятно, будет стараться придерживаться целочисленной формы (например, 64-битных целых чисел) как можно дольше (если это не началось с этого). Поскольку ответ соответствует 64-битовому целому, вычисление является точным.

  • 0
    Node.js явно не имеет тип int. Это работает в типе поплавка.
  • 0
    @greyfade - Да, я думаю, это верно для всех сред, совместимых с EcmaScript.
21

Perl script дает нам ожидаемый результат:

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
    Вы запускали это на 32- или 64-битной системе?
  • 2
    он был выполнен в 64-битной системе
Показать ещё 4 комментария
17

Ответ на это "неожиданно" прост:

Сначала - как может показаться большинство из вас - 32-разрядное целое число от -2,147,483,648 до 2,147,483,647. Итак, что произойдет, если PHP получит результат, то есть LARGER, чем это?

Обычно можно ожидать немедленного "переполнения", в результате чего 2,147,483,647 + 1 превратится в -2,147,483,648. Однако это не так. Если PHP встречает большее число, он возвращает FLOAT вместо INT.

Если PHP встречает число за пределами целочисленного типа, оно будет интерпретироваться как float. Кроме того, операция, которая приводит к числу за пределами целочисленного типа, вместо этого возвращает float.

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

Это говорит о том, что реализация PHP FLOAT соответствует формату двойной точности IEEE 754, означает, что PHP способен обрабатывать цифры до 52 бит без потери точности. (В 32-разрядной системе)

Итак, в точке, где ваша сумма достигает 9,007,199,254,740,992 (которая 2 ^ 53), значение Float, возвращаемое PHP Maths, больше не будет достаточно точным.

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

Этот пример показывает точку, где PHP теряет точность. Во-первых, последний бит синтаксиса будет отброшен, в результате чего первые 2 выражения будут иметь равное число, а это не так.

В режиме "СЕЙЧАС" вся математика пойдет не так, если вы работаете со стандартными типами данных.

• Это одна и та же проблема для другого интерпретируемого языка, такого как Python или Perl?

Я так не думаю. Я думаю, что это проблема языков, которые не имеют безопасности типа. В то время как Integer Overflow, как упоминалось выше, произойдет на всех языках, использующих фиксированные типы данных, языки без безопасности типов могут попытаться поймать это с другими типами данных. Однако, как только они попадают в их "естественную" (системную) границу, они могут что-то вернуть, но правильный результат.

Однако для такого сценария для каждого языка могут быть разные потоки.

16

Другие ответы уже объяснили, что здесь происходит (точность с плавающей запятой, как обычно).

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

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

Написан на С#, но то же самое относится и к другим языкам.

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

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

  • 0
    Это новый .. 500.000.000.067.109.000 ??? Интересно
  • 0
    @Baba Это то же самое, что и с Node.js / JavaScript в OP. Что касается того, почему 500000000067109000 против 500000000067108992 ... не знаю.
Показать ещё 1 комментарий
15

Если у вас 32-разрядный PHP, вы можете рассчитать его с помощью bc:

<?php

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

В Javascript вам нужно использовать произвольную библиотеку чисел, например BigInteger:

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

Даже с такими языками, как Go и Java, вам в конечном итоге придется использовать произвольную библиотеку чисел, ваш номер просто оказался достаточно маленьким для 64-битного, но слишком высокого для 32-разрядного.

13

В Ruby:

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

Печать 500000000500000000, но занимает 6 минут на моем 2,6 ГГц Intel i7.


Магнусс и Яунти имеют гораздо больше решений Ruby:

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

Запуск теста:

$ 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 (1 миллиард) .inject (+)
  • 0
    @Magnuss: Это то, что я сначала попробовал, но это вызвало огромную утечку памяти. Ваш, кажется, работает ...
Показать ещё 1 комментарий
12

Я использую node -bigint для большого целого материала:
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);

Это не так быстро, как то, что может использовать собственный 64-разрядный материал для этого точного теста, но если вы попадаете в большее число, чем в 64-битный, он использует libgmp под капотом, который является одной из более быстрых библиотек произвольной точности там.

5

Чтобы получить правильный результат в php, я думаю, вам нужно будет использовать математические операторы BC: http://php.net/manual/en/ref.bc.php

Вот правильный ответ в Scala. Вы должны использовать Longs, иначе вы переполняете число:

println((1L to 1000000000L).reduce(_ + _)) // prints 500000000500000000
  • 0
    Почему не .sum ?
5

взял возраст в рубине, но дает правильный ответ:

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

У меня недостаточно репутации, чтобы комментировать @postfuturist Common Lisp ответ, но он может быть оптимизирован для завершения в ~ 500 мс с SBCL 1.1.8 на моей машине:

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
4

Общий Lisp является одним из самых быстрых интерпретируемых * языков и по умолчанию обрабатывает произвольно большие целые числа. Это занимает около 3 секунд с 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
  • Поняв, я имею в виду, что я запускал этот код из REPL, SBCL, возможно, сделал некоторые JITing внутри, чтобы заставить его работать быстро, но динамический опыт запуска кода тот же самый.
  • 0
    Можно упростить как (время (цикл для х от 1 до 1000000000 сум х)). Я получил скорость примерно в 5 раз, добавив объявление: (время (локально (объявить (оптимизировать (скорость 3) (безопасность 0)))) (цикл для i fixnum типа of от 1 до 1000000000 sum i fixnum типа)))
  • 0
    Это ошибочно Не позволяйте вам быть ослепленным другими языками! Правильный способ записи в lisp: (defun sum-from-1-to-n (n) (/ (* n (1+ n)) 2)) (time (sum-from-1-to-n) 1000000000)) потребовалось 14 микросекунд (0.000014 секунд) для запуска. В течение этого периода и с 4 доступными ядрами ЦП в режиме пользователя было потрачено 0 мкс (0,000000 секунд), в системном режиме - 0 мкс (0,000000 секунд) -> 500000000500000000
Показать ещё 1 комментарий
4

На самом деле это крутой трюк.

Предположим, что вместо этого было 1-100.

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

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

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

Формула:

При N = 100: Выход = N/2 * (N + 1)

При N = 1e9: Выход = N/2 * (N + 1)

Это намного быстрее, чем перебирать все эти данные. Ваш процессор поблагодарит вас за это. И вот интересная история по этой самой проблеме:

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

  • 22
    Он упоминает об этом в вопросе ...
  • 11
    Как вы думаете, возможно ли пройти через каждый мост через Прегель в Калининграде, не пересекая мост дважды? Многие люди пытались и потерпели неудачу, но никто еще не доказал, что это невозможно. Это похоже на вызов, который вы могли бы решить уникальным образом.
Показать ещё 2 комментария
3

AWK:

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

создает тот же неправильный результат, что и PHP:

500000000067108992

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

Тестирование:

$ 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
3

ActivePerl v5.10.1 на 32-битных окнах, intel core2duo 2.6:

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

результат: 5.00000000067109e + 017 через 5 минут.

С "use bigint" script работал в течение двух часов и работал бы больше, но я его остановил. Слишком медленно.

  • 0
    Может ли кто-нибудь подтвердить, что на самом деле добавление такого количества bigints занимает столько времени?
3

Для полноты, в Clojure (красивый, но не очень эффективный):

(reduce + (take 1000000000 (iterate inc 1))) ; => 500000000500000000
  • 1
    Единственное, что может пригодиться в ответах $ MY_FAVOURITE_LANGUAGE, это дать результат ...
  • 0
    @jwg да извините, я пропустил конец строки - обновленный ответ.
3

Smalltalk:

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

"500000000500000000"
3

Я хотел посмотреть, что произошло в CF Script

<cfscript>
ttl = 0;

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

Я получил 5.00000000067E + 017

Это был довольно опрятный эксперимент. Я вполне уверен, что мог бы лучше закодировать это с большим усилием.

3

Эрланг также дает ожидаемый результат.

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).

И используя его:

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

Racket v 5.3.4 (MBP; время в мс):

> (time (for/sum ([x (in-range 1000000001)]) x))
cpu time: 2943 real time: 2954 gc time: 0
500000000500000000
  • 1
    Мой ответ удален через 6 минут после вас, как только я заметил ваш. :)
3

Прекрасно работает в Rebol:

>> sum: 0
== 0

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

>> type? sum
== integer!

Это использовало Rebol 3, который, несмотря на 32-битную компиляцию, использует 64-битные целые числа (в отличие от Rebol 2, в котором используются 32-битные целые числа)

3

Это дает правильный результат в PHP, заставляя целочисленное преобразование.

$sum = (int) $sum + $i;
  • 0
    Какая система 32 или 64 битная ???
3

Категория другой интерпретируемый язык:

Tcl:

Если использовать Tcl 8.4 или старше, это зависит от того, была ли она скомпилирована с 32 или 64 бит. (8.4 - конец жизни).

Если вы используете Tcl 8.5 или newer с произвольными большими целыми числами, он отобразит правильный результат.

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

Я положил тест внутри proc, чтобы получить его скомпилированный байт.

2

Несколько ответов уже объяснили, почему ваш PHP и Node.js код не работают должным образом, поэтому я не буду повторять это здесь. Я просто хочу отметить, что это не имеет никакого отношения к "интерпретированным vs скомпилированным языкам".

Возможно, это проблема интерпретируемых языков и почему она работает на скомпилированном языке, таком как Go?

"Язык" - это просто набор четко определенных правил; реализация языка - это то, что интерпретируется или компилируется. Я мог бы взять язык, основная реализация которого скомпилирована (например, Go) и написать для него интерпретатор (и наоборот), но каждая программа, обработанная интерпретатором, должна давать идентичный результат, как запуск программы через скомпилированную реализацию, и этот вывод должен соответствовать спецификации языка. Результаты PHP и Node.js на самом деле соответствуют спецификациям языков (как указывают некоторые другие ответы), и это не имеет никакого отношения к тому, что основные реализации этих языков интерпретируются; скомпилированные реализации языков по определению также должны давать одинаковые результаты.

Ощутимым примером всего этого является Python, который имеет как широко используемые скомпилированные, так и интерпретированные реализации. Запуск переведенной версии вашей программы в интерпретируемой реализации:

>>> total = 0 
>>> for i in xrange(1000000001):
...     total += i
... 
>>> print total
500000000500000000

не должно, по определению Python, приводить к другому результату, чем выполнять его в скомпилированной реализации:

total = 0
for i in xrange(1000000001):
    total += i

print total
500000000500000000
2

Только для полноты.


В MATLAB нет проблем с автоматическим выбором типа:

tic; ii = 1:1000000; sum(ii); toc; ans

Elapsed time is 0.004471 seconds.
ans = 5.000005000000000e+11


И в интерактивном режиме F # автоматические типы блоков дают ошибку переполнения. Назначение типа int64 дает правильный ответ:

seq {int64 1.. int64 1000000} |> Seq.sum

val it : int64 = 500000500000L

Примечания:
Могу использовать Seq.reduce (+) вместо Seq.sum без заметного изменения эффективности. Однако использование Seq.reduce (+) с автоматическим типом устройства даст неправильный ответ, а не ошибку переполнения.
Время вычисления составляет < 5 секунд, но в настоящее время я ленив, и поэтому я не импортирую класс .NET-секундомера для получения более точного времени.

2

Смешная вещь, PHP 5.5.1 дает 499999999500000000 (в ~ 30 секунд), а Dart2Js дает 500000000067109000 (чего и следовало ожидать, так как он запускается JS). CLI Dart дает правильный ответ... мгновенно.

2

Порт:

proc Main()

   local sum := 0, i

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

   ? sum

   return

Результаты в 500000000500000000. (на обоих окнах /mingw/x 86 и osx/clang/x64)

2

Эрланг работает:

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).

Результаты: 41 > бесполезно: from_sum (1,1000000000). 500000000500000000

2

В рубине эти функционально схожие решения (которые возвращают правильный ответ) требуют значительно большего времени:

$ time ruby -e "(1..1000000000).inject{|sum, n| sum + n}"
real    1m26.005s
user    1m26.010s
sys 0m0.076s

$ time ruby -e "1.upto(1000000000).inject(:+)"
real    0m48.957s
user    0m48.957s
sys 0m0.045s

$ ruby -v
ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-darwin10.8.0]
  • 0
    а результат есть?
  • 0
    Правильный ответ, а именно 500000000500000000
2

Для кода PHP ответ здесь:

Размер целого числа зависит от платформы, хотя максимальное значение около двух миллиардов - это обычное значение (это 32 бита). Обычно 64-битные платформы имеют максимальное значение около 9E18. PHP не поддерживает целые числа без знака. Integer размер может быть определен с использованием константы PHP_INT_SIZE и максимального значения с использованием константы PHP_INT_MAX с PHP 4.4.0 и PHP 5.0.5.

1

И рубиновый:

[15] pry(main)> (1..1000000000).inject(0) { |sum,e| sum + e }
=> 500000000500000000

Кажется, чтобы получить правильное число.

1

Как отмечают другие люди, самый быстрый способ сделать это вычисление (независимо от языка) - это простая математическая функция (вместо интенсивного цикла процессора):

number = 1000000000;
result = (number/2) * (number+1);

Вам все равно придется решать любые 32/64-разрядные целые/float-проблемы, в зависимости от языка.

0

Javascript (и, возможно, PHP) представляет все числа как двойные и округляет их для целых значений. Это означает, что они имеют только 53 бит точности (вместо 64 бит, предоставляемых int64 и Java long), и приведут к ошибкам округления при больших значениях.

  • 0
    не верно для PHP. посмотри здесь
  • 4
    верно для 32-битного PHP. Все, что больше PHP_INT_MAX приводится к представлению с плавающей запятой.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню