¿Por qué la suma es mucho más rápida que la inyección (: +)?

129

Así que estaba ejecutando algunos puntos de referencia en Ruby 2.4.0 y me di cuenta de que

(1...1000000000000000000000000000000).sum

calcula inmediatamente mientras que

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

lleva tanto tiempo que acabo de abortar la operación. Tenía la impresión de que Range#sumera un alias, Range#inject(:+)pero parece que eso no es cierto. Entonces, ¿cómo sumfunciona y por qué es mucho más rápido que inject(:+)?

Nota : la documentación para Enumerable#sum(que se implementa por Range) no dice nada acerca de la evaluación diferida ni nada parecido.

Eli Sadoff
fuente

Respuestas:

227

Respuesta corta

Para un rango entero:

  • Enumerable#sum devoluciones (range.max-range.min+1)*(range.max+range.min)/2
  • Enumerable#inject(:+) itera sobre cada elemento.

Teoría

La suma de los enteros entre 1 y nse llama un número triangular , y es igual a n*(n+1)/2.

La suma de los enteros entre ny mes el número triangular de mmenos el número triangular de n-1, que es igual a m*(m+1)/2-n*(n-1)/2, y se puede escribir (m-n+1)*(m+n)/2.

Enumerable # sum en Ruby 2.4

Esta propiedad se utiliza en Enumerable#sumrangos enteros:

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 Se ve como esto :

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

que es equivalente a:

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

¡La mencionada igualdad!

Complejidad

¡Muchas gracias a @k_g y @ Hynek-Pichi-Vychodil por esta parte!

suma

(1...1000000000000000000000000000000).sum requiere tres adiciones, una multiplicación, una resta y una división.

Es un número constante de operaciones, pero la multiplicación es O ((log n) ²), también lo Enumerable#sumes O ((log n) ²) para un rango entero.

inyectar

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

requiere 99999999999999999999999999999998 adiciones!

La suma es O (log n), también lo Enumerable#injectes O (n log n).

Con 1E30como entrada, injectcon nunca retorno. ¡El sol explotará mucho antes!

Prueba

Es fácil verificar si se están agregando 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

De hecho, de los enum.ccomentarios:

Enumerable#sumEl método puede no respetar la redefinición de "+" métodos como Integer#+.

Eric Duminil
fuente
17
Esta es una optimización realmente buena ya que calcular la suma de un rango de números es trivial si usas la fórmula correcta, y es insoportable si lo haces de forma iterativa. Es como tratar de implementar la multiplicación como una serie de operaciones de suma.
tadman
Entonces, ¿el aumento de rendimiento es n+1solo para rangos? No tengo 2.4 instalado o me probaría a mí mismo, pero hay otros objetos enumerables manejados por adición básica, ya que estarían en inject(:+)menos la sobrecarga del símbolo para el proceso.
ingenierosmnky
8
Lectores, recuerden de sus matemáticas de secundaria que n, n+1, n+2, .., mconstituyen una serie aritmética cuya suma es igual (m-n+1)*(m+n)/2. Del mismo modo, la suma de una serie geométrica , n, (α^1)n, (α^2)n, (α^3)n, ... , (α^m)n. se puede calcular a partir de una expresión de forma cerrada.
Cary Swoveland el
44
\ begin {nitpick} La suma # numerable es O ((log n) ^ 2) y la inyección es O (n log n) cuando se permite que sus números sean ilimitados. \ end {nitpick}
k_g
66
@EliSadoff: Significa números realmente grandes. Significa números que no se ajustan a la palabra de arquitectura, es decir, no pueden calcularse mediante una instrucción y una operación en el núcleo de la CPU. El número de tamaño N podría codificarse mediante log_2 N bits, por lo que la suma es la operación O (logN) y la multiplicación es O ((logN) ^ 2) pero podría ser O ((logN) ^ 1.585) (Karasuba) o incluso O (logN * log (logN) * ​​log (log (LogN)) (FFT).
Hynek -Pichi- Vychodil