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

Why is sum so much faster than inject(:+)?

所以我在Ruby2.4.0中运行了一些基准测试,并意识到

1
(1...1000000000000000000000000000000).sum

立即计算,而

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

我花了这么长时间才放弃手术。我觉得Range#sumRange#inject(:+)的别名,但这似乎不是真的。那么,sum是如何工作的?为什么它比inject(:+)快得多?

注意:Enumerable#sum的文档(由Range实现)没有提到延迟评估或这些方面的任何内容。


简短的回答

对于整数范围:

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

理论

1和n之间的整数之和称为三角数,等于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中的可枚举和

Enumerable#sum中用于整数范围的此属性:

1
2
3
4
5
6
7
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如下:

1
2
3
4
5
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);

相当于:

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

上述平等!

复杂性

非常感谢@k_g和@hynek pichi vychodil为这部分!

总和

埃多克斯1〔13〕需要三个加法、一个乘法、一个减法和一个除法。

它是一个常量,但乘法是O((log n)2),所以对于整数范围,Enumerable#sum是O((logn)2)。

注入

埃多克斯1〔15〕

需要99999999999999999999999999999 8个附加项!

加上是O(对数N),所以Enumerable#inject是O(对数N)。

1E30为输入,inject不返回。太阳早就要爆炸了!

试验

很容易检查是否添加了Ruby整数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 method may not respect method redefinition of "+"
methods such as Integer#+.