เหตุใดผลรวมจึงเร็วกว่าการฉีด (: +) มาก


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สำหรับช่วงจำนวนเต็ม:

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#sumO ((log n) ²) สำหรับช่วงจำนวนเต็ม

ฉีด

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

ต้องเพิ่ม 999999999999999999999999999998!

การเพิ่มคือ O (log n) ดังนั้นEnumerable#injectO (n log n) ก็เช่นกัน

ด้วยการ1E30ป้อนข้อมูลinjectโดยไม่ส่งคืน ดวงจะระเบิดนานก่อน!

ทดสอบ

ง่ายต่อการตรวจสอบว่ามีการเพิ่ม 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

จาก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)/2ซึ่งเท่ากับผลรวม ในทำนองเดียวกันผลรวมของการให้ชุดเรขาคณิตn, (α^1)n, (α^2)n, (α^3)n, ... , (α^m)n , สามารถคำนวณได้จากนิพจน์รูปแบบปิด
Cary Swoveland

4
\ begin {nitpick} จำนวน # ที่นับได้คือ O ((log n) ^ 2) และการฉีดคือ O (n log n) เมื่อตัวเลขของคุณได้รับอนุญาตให้ไม่ผูกมัด \ สิ้นสุด {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.