2011-01-25 30 views
21

tôi đã viết một câu trả lời cho câu hỏi đầu tiên Project Euler:Tại sao bộ lọc ở phía trước foldLeft chậm ở Scala?

Thêm tất cả các số tự nhiên dưới một ngàn mà là bội số của 3 hoặc 5.

Việc đầu tiên mà đến với tôi là:

(1 until 1000).filter(i => (i % 3 == 0 || i % 5 == 0)).foldLeft(0)(_ + _) 

nhưng nó chậm (phải mất 125   ms), vì vậy tôi viết lại nó, chỉ cần nghĩ đến việc 'một cách khác' so với 'cách nhanh'

(1 until 1000).foldLeft(0){ 
    (total, x) => 
     x match { 
      case i if (i % 3 == 0 || i % 5 ==0) => i + total // Add 
      case _ => total //skip 
     } 
} 

Điều này nhanh hơn nhiều (chỉ 2   ms). Tại sao? Tôi đoán phiên bản thứ hai chỉ sử dụng trình tạo phạm vi và không hiển thị bộ sưu tập được nhận dạng đầy đủ theo bất kỳ cách nào, thực hiện tất cả trong một lần, nhanh hơn và ít bộ nhớ hơn. Tôi có đúng không?

Đây mã trên IdeOne: http://ideone.com/GbKlP

+7

Bạn đo lường mã đó như thế nào là "các đơn đặt hàng có cường độ nhanh hơn"? Trên máy tính xách tay * cực kỳ * cũ, * cực kỳ * của tôi, trong trình thông dịch Scala *, phiên bản bạn yêu cầu là "các đơn đặt hàng có cường độ chậm hơn" mất ít hơn 300 µs (đó là * micro * giây), do đó, bao lâu * phiên bản nhanh * có? Liệu nó đi ngược thời gian? Hầu hết các JVM hiệu suất cao cần khoảng 5 * giây * để làm ấm bộ nhớ cache và các công cụ của chúng, trước khi chúng đạt tốc độ tối đa. (Trình biên dịch C2 trong JVM HotSpot, là trình biên dịch tối ưu hóa, thậm chí không biên dịch một phương thức cho đến khi nó chạy 20000 lần.) –

+0

Phiên bản thứ hai có vẻ nhanh hơn gấp 3 lần (bằng cách đo lường hoàn toàn không khoa học của tôi bằng cách sử dụng '(1 đến 10000000) 'trong REPL). Tôi sẽ không gọi đó là "đơn đặt hàng của cường độ", nhưng vẫn còn. –

+0

@ Jörg: bạn có thể thấy thời gian chạy tại liên kết ideone của mình, nhưng tôi sẽ chỉnh sửa thông tin đó vào câu hỏi để nó không bị mất nếu liên kết ideone biến mất. –

Trả lời

23

Sự cố, như những người khác đã nói, là filter tạo bộ sưu tập mới. Thay thế withFilter thì không, nhưng không có foldLeft. Ngoài ra, việc sử dụng .view, .iterator hoặc .toStream tất cả sẽ tránh tạo bộ sưu tập mới theo nhiều cách khác nhau, nhưng tất cả đều chậm hơn ở đây so với phương pháp đầu tiên bạn sử dụng.

Nhưng, sau đó ...Xem, 1 until 1000 là một Range, có kích thước thực sự là rất nhỏ, bởi vì nó không lưu trữ từng phần tử. Ngoài ra, là cực kỳ được tối ưu hóa và thậm chí là specialized, không phải là trường hợp của bất kỳ bộ sưu tập nào khác. Kể từ khi foldLeft được triển khai dưới dạng foreach, miễn là bạn ở lại với một Range bạn có thể tận hưởng các phương pháp được tối ưu hóa của nó.

(_: Range).foreach:

@inline final override def foreach[@specialized(Unit) U](f: Int => U) { 
    if (length > 0) { 
     val last = this.last 
     var i = start 
     while (i != last) { 
      f(i) 
      i += step 
     } 
     f(i) 
    } 
} 

(_: Range).view.foreach

def foreach[U](f: A => U): Unit = 
    iterator.foreach(f) 

(_: Range).view.iterator

override def iterator: Iterator[A] = new Elements(0, length) 

protected class Elements(start: Int, end: Int) extends BufferedIterator[A] with Serializable { 
    private var i = start 

    def hasNext: Boolean = i < end 

    def next: A = 
    if (i < end) { 
     val x = self(i) 
     i += 1 
     x 
    } else Iterator.empty.next 

    def head = 
    if (i < end) self(i) else Iterator.empty.next 

    /** $super 
    * '''Note:''' `drop` is overridden to enable fast searching in the middle of indexed sequences. 
    */ 
    override def drop(n: Int): Iterator[A] = 
    if (n > 0) new Elements(i + n, end) else this 

    /** $super 
    * '''Note:''' `take` is overridden to be symmetric to `drop`. 
    */ 
    override def take(n: Int): Iterator[A] = 
    if (n <= 0) Iterator.empty.buffered 
    else if (i + n < end) new Elements(i, i + n) 
    else this 
} 

(_: Range).view.iterator.foreach

def foreach[U](f: A => U) { while (hasNext) f(next()) } 

Và đó, tất nhiên, thậm chí không đếm filter giữa viewfoldLeft:

override def filter(p: A => Boolean): This = newFiltered(p).asInstanceOf[This] 

protected def newFiltered(p: A => Boolean): Transformed[A] = new Filtered { val pred = p } 

trait Filtered extends Transformed[A] { 
    protected[this] val pred: A => Boolean 
    override def foreach[U](f: A => U) { 
    for (x <- self) 
     if (pred(x)) f(x) 
    } 
    override def stringPrefix = self.stringPrefix+"F" 
} 
+2

Đối với những gì nó có giá trị ... Điều này nhận được phiếu bầu của tôi cho câu trả lời tốt nhất cho đến nay. –

+0

Đồng ý. Tôi đã đưa nó cho Kevin đầu tiên của bạn, và tôi xin lỗi vì đã thay đổi nó, nhưng đó là câu trả lời này đã giúp tôi hiểu những gì đang xảy ra. – andyczerwonka

+0

@arcticpenguin - Khá đúng, tôi sẽ không có cách nào khác! –

3

Nhìn qua các mã, nó trông giống như filter không xây dựng một Seq mới mà trên đó các foldLeft được gọi. Thứ hai bỏ qua bit đó. Nó không phải là quá nhiều bộ nhớ, mặc dù điều đó không thể nhưng giúp đỡ, nhưng bộ sưu tập được lọc không bao giờ được xây dựng ở tất cả. Tất cả công việc đó không bao giờ được thực hiện.

Phạm vi sử dụng TranversableLike.filter, trông như thế này:

def filter(p: A => Boolean): Repr = { 
    val b = newBuilder 
    for (x <- this) 
    if (p(x)) b += x 
    b.result 
} 

Tôi nghĩ rằng đó là += trên dòng 4 đó là sự khác biệt. Lọc theo số foldLeft sẽ loại bỏ nó.

+1

Thú vị. Trình biên dịch GHC Haskell có lẽ sẽ thực hiện một số loại hợp nhất dòng ở đây, và về cơ bản biến phiên bản đầu tiên thành phiên bản thứ hai của riêng nó. Thật không may, các công cụ như thế này thực sự khó đạt được cho một ngôn ngữ không tinh khiết như Scala (đặc biệt nếu bạn thêm công văn động vào hỗn hợp).Trình biên dịch có lẽ sẽ phải chứng minh rằng cả hai khối được cung cấp cho 'filter' và' foldLeft' là tham chiếu trong suốt, cũng như chứng minh rằng không có lớp con buồn cười nào đang diễn ra. –

+0

@ Jörg W Mittag: Đúng vậy. Người ta tự hỏi nếu gọi toStream trên phạm vi sẽ giúp đỡ. Chắc là không. Nhưng chúng ta đang ở trong vùng tối ưu hóa vi mô ở đây. – sblundy

12

Cố gắng làm cho bộ sưu tập lười biếng đầu tiên, vì vậy

(1 until 1000).view.filter... 

thay vì

(1 until 1000).filter... 

Điều đó sẽ tránh được chi phí xây dựng một bộ sưu tập trung. Bạn cũng có thể nhận được hiệu suất tốt hơn từ việc sử dụng sum thay vì foldLeft(0)(_ + _), luôn có thể một số loại bộ sưu tập có thể có cách hiệu quả hơn để tổng số. Nếu không, nó vẫn sạch hơn và khai báo hơn ...

+0

Đánh tôi với nó. Liên lạc tốt đẹp với 'sum'. – Raphael

+0

Khi tôi khởi động JVM, sự chênh lệch nhỏ hơn so với bản gốc. Việc thêm chế độ xem mang lại nhiều thứ hơn, về cơ bản chúng giống nhau. – andyczerwonka

+6

Tôi không biết về bạn, nhưng thử nghiệm của tôi trên thân cây tôi có ở đây làm cho nó thực sự chậm hơn so với không có 'xem'. –

1

filter tạo một chuỗi hoàn toàn mới mà sau đó gọi là foldLeft. Hãy thử:

(1 until 1000).view.filter(i => (i % 3 == 0 || i % 5 == 0)).reduceLeft(_+_)

Điều này sẽ ngăn chặn cho biết tác dụng, chỉ đơn thuần gói điều gốc. Trao đổi foldLeft với reduceLeft chỉ là mỹ phẩm (trong trường hợp này).

+2

Lưu ý: trao đổi 'foldLeft' với' reduceLeft' chỉ là mỹ phẩm _here_ vì bạn biết _a priori_ rằng danh sách không trống. Nói chung, bạn cần gấp để tránh ngoại lệ tiềm ẩn. –

1

Thách thức hiện nay là, bạn có thể nghĩ ra một cách hiệu quả hơn chưa? Không phải là giải pháp của bạn quá chậm trong trường hợp này, nhưng nó có quy mô như thế nào không? Điều gì sẽ xảy ra nếu thay vì 1000, nó là 1000000000? Có một giải pháp có thể tính toán trường hợp sau cũng nhanh như trước đây.

+2

'def arithProg (a: Int, d: Int, n: Int): Long = n * (2 * a + (n - 1) * d.toLong)/2; def find (n: Int): Long = arithProg (3, 3, n/3) + arithProg (5, 5, n/5) - arithProg (15, 15, n/15); println (find (1000000000 - 1)) 'Tôi thắng gì? –

Các vấn đề liên quan