为什么总和比注入(:+)快得多?

时间:2017-01-03 17:59:34

标签: ruby

所以我在Ruby 2.4.0中运行了一些基准并实现了

(1...1000000000000000000000000000000).sum

立即计算

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

需要很长时间,我只是中止了操作。我的印象是Range#sumRange#inject(:+)的别名,但似乎并非如此。那么sum如何运作,为什么它比inject(:+)快得多?

N.B。 Enumerable#sum的文档(由Range实现)没有提及延迟评估或其他任何内容。

1 个答案:

答案 0 :(得分:223)

简短回答

对于整数范围:

  • Enumerable#sum返回(range.max-range.min+1)*(range.max+range.min)/2
  • Enumerable#inject(:+)遍历每个元素。

理论

1和n之间的整数之和称为triangular number,等于n*(n+1)/2

nm之间的整数之和是m的三角形数减去n-1的三角形数,等于m*(m+1)/2-n*(n-1)/2,并且可以写成(m-n+1)*(m+n)/2

Ruby 2.4中的可枚举#sum

此属性在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 Integers:

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#+

等方法