Почему сумма намного быстрее, чем инъекция (: +)?

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

(1...1000000000000000000000000000000).sum

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

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

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

N.B. Документация для Enumerable#sum (которая реализована Range) ничего не говорит о ленивой оценке или о том, что происходит по этим строкам.

Ответ 1

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

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

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

Перечислимая сумма в 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 Integer:

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#sum метод может не учитывать переопределение метода "+"методы, такие как Integer#+.