sumがinject(:+)よりもはるかに速いのはなぜですか?


129

Ruby 2.4.0でいくつかのベンチマークを実行していて、

(1...1000000000000000000000000000000).sum

すぐに計算しますが

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

時間がかかるので、操作を中止しました。Range#sumエイリアスという印象を受けましたRange#inject(:+)が、そうではないようです。では、どのように機能するのでしょうか。sumなぜそれがこれよりもはるかに高速なのinject(:+)でしょうか。

注意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

Ruby 2.4のEnumerable#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 3つの加算、乗算、減算、除算が必要です。

これは定数の演算ですが、乗算はO((log n)²)なので、 Enumerable#sum、整数範囲のです。

注入する

(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 2017年

では、パフォーマンスの向上はn+1範囲のみですか?私は2.4をインストールしていないか、自分でテストしますが、inject(:+)procへのシンボルのオーバーヘッドがマイナスになるため、基本的な追加によって処理される他のEnumerableオブジェクトです。
Engineersmnky

8
読者の皆さんは、合計がとなる算術級数n, n+1, n+2, .., m構成する高校の数学を思い出してください。同様に、幾何学的系列の合計、。閉形式の式から計算できます。(m-n+1)*(m+n)/2n, (α^1)n, (α^2)n, (α^3)n, ... , (α^m)n
Cary Swoveland 2017年

4
\ begin {nitpick} Enumerable#sumはO((log n)^ 2)で、injectはO(n log n)です。\ end {nitpick}
k_g 2017年

6
@EliSadoff:それは本当に大きな数字を意味します。これは、アーキテクチャワードに適合しない数値を意味します。つまり、CPUコアの1つの命令と1つの演算では計算できません。サイズNの数はlog_2 Nビットでエンコードできるため、加算はO(logN)演算、乗算はO((logN)^ 2)ですが、O((logN)^ 1.585)(Karasuba)またはO(logN *ログ(logN個)*ログ(ログは(LOGN))(FFT)。
ハイネック-Pichi- Vychodil
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.