왜 inject (: +)보다 합계가 훨씬 빠릅니까?


129

그래서 Ruby 2.4.0에서 일부 벤치 마크를 실행하고 있었고

(1...1000000000000000000000000000000).sum

반면에 즉시 계산

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

작업을 중단하는 데 너무 오래 걸립니다. 나는 Range#sum별명 인 인상 을 Range#inject(:+)받았지만 사실이 아닌 것 같습니다. 그렇다면 어떻게 sum작동하며 왜 그렇게 훨씬 빠릅 inject(:+)니까?

NB에Enumerable#sum 의해 구현 된 문서 Range는 게으른 평가 또는 그 라인을 따라 아무것도 말하지 않습니다.

답변:


227

짧은 답변

정수 범위의 경우 :

  • Enumerable#sum 보고 (range.max-range.min+1)*(range.max+range.min)/2
  • Enumerable#inject(:+) 모든 요소를 ​​반복합니다.

이론

1과 3 사이의 정수의 합을 삼각 수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에서 열거 가능한 #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 곱하기, 빼기 및 나누기의 세 가지 덧셈이 필요합니다.

상수 연산 수이지만 곱셈은 O ((log n) ²)이므로 Enumerable#sum정수 범위의 경우 O ((log n) ²)입니다.

주사하다

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

999999999999999999999999999998 추가 필요!

덧셈은 O (log n)이므로 Enumerable#injectO (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#sum방법은 "+" 과 같은 방법의 방법 재정의를 존중하지 않을 수 있습니다 Integer#+.


17
올바른 수식을 사용하면 숫자 범위의 합계를 계산하는 것이 쉽지 않으며 반복적으로 반복하면 문제가되기 때문에 이것은 정말 좋은 최적화입니다. 곱셈을 일련의 덧셈 연산으로 구현하는 것과 같습니다.
tadman

그렇다면 성능 향상은 n+1범위에만 해당됩니까? 2.4가 설치되어 있지 않거나 직접 테스트하지만 기본 덧셈으로 처리되는 다른 열거 가능한 객체는 inject(:+)proc의 심볼 오버 헤드를 뺀 것 입니다.
engineermnky

8
독자 여러분, 고등학교 수학에서 합계가 같은 산술 시리즈n, n+1, n+2, .., m구성하는 것을 상기하십시오 . 마찬가지로, 합계 등비 , . 닫힌 형식 식에서 계산할 수 있습니다. (m-n+1)*(m+n)/2n, (α^1)n, (α^2)n, (α^3)n, ... , (α^m)n
캐리 Swoveland

4
\ begin {nitpick} 열거 할 수있는 #sum은 O ((log n) ^ 2)이고 숫자가 제한되지 않으면 inject는 O (n log n)입니다. \ end {nitpick}
k_g

6
@ Elisadoff : 정말 큰 숫자를 의미 합니다. 이는 아키텍처 단어에 맞지 않는 숫자를 의미합니다. 즉 CPU 코어에서 하나의 명령과 하나의 연산으로 계산할 수 없습니다. 크기 N의 수는 log_2 N 비트로 인코딩 될 수 있으므로 덧셈은 O (logN) 연산이고 곱셈은 O ((logN) ^ 2)이지만 O ((logN) ^ 1.585) (Karasuba) 또는 O (logN * 일 수 있음 log (logN) * ​​log (log (LogN)) (FFT)
Hynek -Pichi- Vychodil
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.