Warum ist die Summe so viel schneller als die Injektion (: +)?


129

Also habe ich einige Benchmarks in Ruby 2.4.0 ausgeführt und das erkannt

(1...1000000000000000000000000000000).sum

berechnet sofort während

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

dauert so lange, dass ich gerade die Operation abgebrochen habe. Ich hatte den Eindruck, dass dies Range#sumein Alias ​​war, Range#inject(:+)aber es scheint, dass dies nicht wahr ist. Wie funktioniert das sumund warum ist es so viel schneller als inject(:+)?

NB Die Dokumentation für Enumerable#sum(die von implementiert wird Range) sagt nichts über eine verzögerte Bewertung oder etwas in dieser Richtung aus.

Antworten:


227

Kurze Antwort

Für einen ganzzahligen Bereich:

  • Enumerable#sum kehrt zurück (range.max-range.min+1)*(range.max+range.min)/2
  • Enumerable#inject(:+) iteriert über jedes Element.

Theorie

Die Summe der ganzen Zahlen zwischen 1 und nwird als Dreieckszahl bezeichnet und ist gleich n*(n+1)/2.

Die Summe der ganzen Zahlen zwischen nund mist die Dreieckszahl von mminus der Dreieckszahl von n-1, die gleich m*(m+1)/2-n*(n-1)/2ist und geschrieben werden kann (m-n+1)*(m+n)/2.

Aufzählbare # Summe in Ruby 2.4

Diese Eigenschaft wird in Enumerable#sumfür ganzzahlige Bereiche verwendet:

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 sieht aus wie das :

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);

was äquivalent ist zu:

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

die vorgenannte Gleichheit!

Komplexität

Vielen Dank an @k_g und @ Hynek-Pichi-Vychodil für diesen Teil!

Summe

(1...1000000000000000000000000000000).sum erfordert drei Additionen, eine Multiplikation, eine Subtraktion und eine Division.

Es ist eine konstante Anzahl von Operationen, aber die Multiplikation ist O ((log n) ²), ebenso Enumerable#sumwie O ((log n) ²) für einen ganzzahligen Bereich.

injizieren

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

erfordert 999999999999999999999999999998 Ergänzungen!

Die Addition ist O (log n), ebenso Enumerable#injectwie O (n log n).

Mit 1E30als Eingabe, injectmit nie zurückkehren. Die Sonne wird lange vorher explodieren!

Prüfung

Es ist einfach zu überprüfen, ob Ruby Integers hinzugefügt werden:

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

In der Tat aus enum.cKommentaren:

Enumerable#sumMethode kann Methode Neudefinition von "+" Methoden wie nicht respektieren Integer#+.


17
Dies ist eine wirklich gute Optimierung, da die Berechnung der Summe eines Zahlenbereichs trivial ist, wenn Sie die richtige Formel verwenden, und unerträglich, wenn Sie iterativ vorgehen. Es ist wie der Versuch, die Multiplikation als eine Reihe von Additionsoperationen zu implementieren.
Tadman

Der Leistungsschub gilt also nur für n+1Bereiche? Ich habe nicht 2.4 installiert oder ich würde mich selbst testen, aber es handelt sich um andere aufzählbare Objekte, die durch grundlegende Addition behandelt werden, da sie sich inject(:+)abzüglich des Overheads des zu verarbeitenden Symbols befinden würden.
Ingenieuremnky

8
Leser, erinnern Sie sich an Ihre High-School-Mathematik, n, n+1, n+2, .., mdie eine arithmetische Reihe darstellt, deren Summe gleich ist (m-n+1)*(m+n)/2. Ebenso ist die Summe einer geometrischen Reihe , n, (α^1)n, (α^2)n, (α^3)n, ... , (α^m)n. kann aus einem Ausdruck in geschlossener Form berechnet werden.
Cary Swoveland

4
\ begin {nitpick} Aufzählbare # Summe ist O ((log n) ^ 2) und Inject ist O (n log n), wenn Ihre Zahlen unbegrenzt sein dürfen. \ end {nitpick}
k_g

6
@EliSadoff: Es bedeutet wirklich große Zahlen. Dies bedeutet, dass Zahlen, die nicht zum Architekturwort passen, dh nicht durch einen Befehl und eine Operation im CPU-Kern berechnet werden können. Die Anzahl der Größen N könnte durch log_2 N Bits codiert werden, so dass die Addition O (logN) ist und die Multiplikation O ((logN) ^ 2) ist, aber O ((logN) ^ 1,585) (Karasuba) oder sogar O (logN *) sein kann log (logN) * ​​log (log (LogN)) (FFT).
Hynek-Pichi-Vychodil
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.