Почему sum намного быстрее, чем inject (: +)?

129

Итак, я провел несколько тестов в Ruby 2.4.0 и понял, что

(1...1000000000000000000000000000000).sum

вычисляет немедленно, тогда как

(1...1000000000000000000000000000000).inject(:+)

занимает так много времени, что я просто прервал операцию. У меня создалось впечатление, что Range#sumэто псевдоним, Range#inject(:+)но похоже, что это неправда. Так как же sumработает и почему намного быстрее inject(:+)?

NB. В документации для Enumerable#sum(которая реализована Range) ничего не говорится о ленивых вычислениях или о чем-то подобном.

Эли Садофф
источник

Ответы:

227

Короткий ответ

Для целочисленного диапазона:

  • Enumerable#sum возвращается (range.max-range.min+1)*(range.max+range.min)/2
  • Enumerable#inject(:+) перебирает каждый элемент.

теория

Сумма целых чисел от 1 nдо называется треугольным числом и равна n*(n+1)/2.

Сумма целых чисел между nи mпредставляет собой треугольное число mминус треугольное число n-1, которое равно m*(m+1)/2-n*(n-1)/2и может быть записано (m-n+1)*(m+n)/2.

Enumerable # sum в Ruby 2.4

Это свойство используется Enumerable#sumдля целочисленных диапазонов:

if (RTEST(rb_range_values(obj, &beg, &end, &excl))) {
    if (!memo.block_given && !memo.float_value &&
            (FIXNUM_P(beg) || RB_TYPE_P(beg, T_BIGNUM)) &&
            (FIXNUM_P(end) || RB_TYPE_P(end, T_BIGNUM))) { 
        return int_range_sum(beg, end, excl, memo.v);
    } 
}

int_range_sum выглядит так:

VALUE a;
a = rb_int_plus(rb_int_minus(end, beg), LONG2FIX(1));
a = rb_int_mul(a, rb_int_plus(end, beg));
a = rb_int_idiv(a, LONG2FIX(2));
return rb_int_plus(init, a);

что эквивалентно:

(range.max-range.min+1)*(range.max+range.min)/2

вышеупомянутое равенство!

сложность

Большое спасибо @k_g и @ Hynek-Pichi-Vychodil за эту часть!

сумма

(1...1000000000000000000000000000000).sum требует трех сложений, умножения, вычитания и деления.

Это постоянное количество операций, но умножение равно O ((log n) ²), поэтому Enumerable#sum как и O ((log n) ²) для целого диапазона.

инъекционные

(1...1000000000000000000000000000000).inject(:+)

требует 999999999999999999999999999998 дополнений!

Сложение - O (log n), Enumerable#injectкак и O (n log n).

В 1E30качестве ввода, injectбез возврата. Солнце взорвется задолго до этого!

Тест

Проверить, добавляются ли целые числа Ruby, легко:

module AdditionInspector
  def +(b)
    puts "Calculating #{self}+#{b}"
    super
  end
end

class Integer
  prepend AdditionInspector
end

puts (1..5).sum
#=> 15

puts (1..5).inject(:+)
# Calculating 1+2
# Calculating 3+3
# Calculating 6+4
# Calculating 10+5
#=> 15

Действительно, из enum.cкомментариев:

Enumerable#summethod может не учитывать переопределение "+" методов, таких как Integer#+.

Эрик Думинил
источник
17
Это действительно хорошая оптимизация, поскольку вычисление суммы диапазона чисел тривиально, если вы используете правильную формулу, и мучительно, если вы делаете это итеративно. Это похоже на попытку реализовать умножение как серию операций сложения.
tadman 03
Значит, повышение производительности n+1только для диапазонов? У меня нет версии 2.4, иначе я бы протестировал себя, но есть другие перечисляемые объекты, которые обрабатываются базовым добавлением, так как они были бы за inject(:+)вычетом накладных расходов символа для proc.
engineeringmnky
8
Читатели, вспомните из школьной математики, которая n, n+1, n+2, .., mпредставляет собой арифметический ряд , сумма которого равна (m-n+1)*(m+n)/2. Аналогичным образом , сумма геометрической прогрессии , n, (α^1)n, (α^2)n, (α^3)n, ... , (α^m)n. может быть вычислено из выражения в закрытой форме.
Cary Swoveland 03
4
\ begin {nitpick} Enumerable # сумма равна O ((log n) ^ 2), а inject - O (n log n), если ваши числа могут быть неограниченными. \ end {nitpick}
k_g
6
@EliSadoff: Это действительно большие цифры. Это означает числа, которые не подходят под архитектурное слово, т.е. не могут быть вычислены одной инструкцией и одной операцией в ядре процессора. Число размера N может быть закодировано с помощью log_2 N бит, поэтому сложение - это операция O (logN), а умножение - O ((logN) ^ 2), но может быть O ((logN) ^ 1,585) (Karasuba) или даже O (logN * log (logN) * ​​log (log (LogN)) (FFT).
Hynek -Pichi- Vychodil