Perché la somma è molto più veloce di inject (: +)?


129

Quindi stavo eseguendo alcuni benchmark in Ruby 2.4.0 e me ne sono reso conto

(1...1000000000000000000000000000000).sum

calcola immediatamente mentre

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

impiega così tanto tempo che ho appena interrotto l'operazione. Avevo l'impressione che Range#sumfosse un alias per Range#inject(:+)ma sembra che non sia vero. Quindi, come sumfunziona e perché è molto più veloce di inject(:+)?

NB La documentazione per Enumerable#sum(che è implementata da Range) non dice nulla sulla valutazione pigra o qualcosa del genere.

Risposte:


227

Risposta breve

Per un intervallo intero:

  • Enumerable#sum ritorna (range.max-range.min+1)*(range.max+range.min)/2
  • Enumerable#inject(:+) scorre su ogni elemento.

Teoria

La somma di numeri interi tra 1 ed nè chiamata numero triangolare ed è uguale a n*(n+1)/2.

La somma degli interi tra ne mè il numero triangolare di mmeno il numero triangolare di n-1, che è uguale a m*(m+1)/2-n*(n-1)/2e può essere scritto (m-n+1)*(m+n)/2.

Enumerable # sum in Ruby 2.4

Questa proprietà è utilizzata Enumerable#sumper intervalli interi:

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 Somiglia a questo :

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

che equivale a:

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

l'uguaglianza di cui sopra!

Complessità

Grazie mille a @k_g e @ Hynek-Pichi-Vychodil per questa parte!

somma

(1...1000000000000000000000000000000).sum richiede tre aggiunte, una moltiplicazione, una sottrazione e una divisione.

È un numero costante di operazioni, ma la moltiplicazione è O ((log n) ²), quindi Enumerable#sumè O ((log n) ²) per un intervallo intero.

iniettare

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

richiede 99999999999999999999999999999998 aggiunte!

L'aggiunta è O (log n), così Enumerable#injectcome O (n log n).

Con 1E30come input, injectcon mai ritorno. Il sole esploderà molto prima!

Test

È facile controllare se vengono aggiunti 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

Infatti, dai enum.ccommenti:

Enumerable#sumIl metodo potrebbe non rispettare la ridefinizione del metodo di "+" metodi come Integer#+.


17
Questa è un'ottimizzazione davvero buona da avere poiché il calcolo della somma di un intervallo di numeri è banale se usi la formula giusta ed è lancinante se lo fai iterativamente. È come cercare di implementare la moltiplicazione come una serie di operazioni di addizione.
Tadman,

Quindi l'aumento delle prestazioni è n+1solo per le gamme? Non ho 2.4 installato o mi metterei alla prova ma sono altri Enumerable Objects gestiti dall'aggiunta di base in quanto sarebbero inject(:+)meno il sovraccarico del simbolo da proc.
engineermnky,

8
Lettori, ricordate dalla vostra matematica del liceo che n, n+1, n+2, .., mcostituisce una serie aritmetica la cui somma è uguale (m-n+1)*(m+n)/2. Analogamente, la somma di una serie geometrica , n, (α^1)n, (α^2)n, (α^3)n, ... , (α^m)n. può essere calcolato da un'espressione a forma chiusa.
Cary Swoveland,

4
\ begin {nitpick} La somma # numerabile è O ((log n) ^ 2) e l'iniezione è O (n log n) quando i numeri possono essere illimitati. \ end {nitpick}
k_g

6
@EliSadoff: significa numeri davvero grandi. Significa numeri che non rientrano nella parola dell'architettura, cioè che non possono essere calcolati da un'istruzione e da un'operazione nel core della CPU. Il numero della dimensione N potrebbe essere codificato da log_2 N bit, quindi l'aggiunta è l'operazione O (logN) e la moltiplicazione è O ((logN) ^ 2) ma potrebbe essere O ((logN) ^ 1.585) (Karasuba) o anche O (logN * log (logN) * ​​log (log (LogN)) (FFT).
Hynek -Pichi- Vychodil
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.