Lập trình hàm - tính bất biến có đắt không? [đóng cửa]


98

Câu hỏi gồm hai phần. Đầu tiên là khái niệm. Phần tiếp theo xem xét câu hỏi tương tự một cách cụ thể hơn trong Scala.

  1. Việc chỉ sử dụng cấu trúc dữ liệu bất biến trong ngôn ngữ lập trình có làm cho việc triển khai các thuật toán / logic nhất định vốn tính toán cao hơn trong thực tế không? Điều này rút ra một thực tế rằng tính bất biến là nguyên lý cốt lõi của các ngôn ngữ chức năng thuần túy. Có những yếu tố nào khác tác động đến điều này không?
  2. Hãy lấy một ví dụ cụ thể hơn. Quicksort thường được dạy và triển khai bằng cách sử dụng các phép toán có thể thay đổi trên cấu trúc dữ liệu trong bộ nhớ. Làm cách nào để triển khai một thứ như vậy theo cách chức năng TINH KHIẾT với chi phí lưu trữ và tính toán có thể so sánh được với phiên bản có thể thay đổi. Cụ thể là trong Scala. Tôi đã bao gồm một số điểm chuẩn thô bên dưới.

Thêm chi tiết:

Tôi đến từ nền tảng lập trình mệnh lệnh (C ++, Java). Tôi đã tìm hiểu về lập trình chức năng, cụ thể là Scala.

Một số nguyên tắc cơ bản của lập trình hàm thuần túy:

  1. Chức năng là công dân hạng nhất.
  2. Các hàm không có tác dụng phụ và do đó các đối tượng / cấu trúc dữ liệu là bất biến .

Mặc dù các JVM hiện đại cực kỳ hiệu quả với việc tạo đối tượng và việc thu gom rác rất rẻ đối với các đối tượng tồn tại trong thời gian ngắn, nhưng có lẽ vẫn tốt hơn là giảm thiểu việc tạo đối tượng đúng không? Ít nhất là trong một ứng dụng đơn luồng, nơi đồng thời và khóa không phải là một vấn đề. Vì Scala là một mô hình lai, người ta có thể chọn viết mã mệnh lệnh với các đối tượng có thể thay đổi nếu cần thiết. Nhưng, là một người đã dành nhiều năm cố gắng tái sử dụng các đối tượng và giảm thiểu phân bổ. Tôi muốn hiểu rõ về trường phái tư tưởng thậm chí không cho phép điều đó.

Như một trường hợp cụ thể, tôi hơi ngạc nhiên với đoạn mã này trong hướng dẫn 6 này . Nó có một phiên bản Java của Quicksort theo sau là một triển khai Scala trông giống nhau.

Đây là nỗ lực của tôi để đánh giá việc triển khai. Tôi chưa làm hồ sơ chi tiết. Tuy nhiên, tôi đoán là phiên bản Scala chậm hơn vì số lượng đối tượng được phân bổ là tuyến tính (một đối tượng cho mỗi lần gọi đệ quy). Có cách nào để tối ưu hóa cuộc gọi đuôi có thể phát huy tác dụng không? Nếu tôi đúng, Scala hỗ trợ tối ưu hóa cuộc gọi đuôi cho các cuộc gọi tự đệ quy. Vì vậy, nó chỉ nên được giúp đỡ nó. Tôi đang sử dụng Scala 2.8.

Phiên bản Java

public class QuickSortJ {

    public static void sort(int[] xs) {
      sort(xs, 0, xs.length -1 );
    }

    static void sort(int[] xs, int l, int r) {
      if (r >= l) return;
      int pivot = xs[l];
      int a = l; int b = r;
      while (a <= b){
        while (xs[a] <= pivot) a++;
        while (xs[b] > pivot) b--;
        if (a < b) swap(xs, a, b);
      }
      sort(xs, l, b);
      sort(xs, a, r);
    }

    static void swap(int[] arr, int i, int j) {
      int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
    }
}

Phiên bản Scala

object QuickSortS {

  def sort(xs: Array[Int]): Array[Int] =
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length / 2)
      Array.concat(
        sort(xs filter (pivot >)),
        xs filter (pivot ==),
        sort(xs filter (pivot <)))
    }
}

Mã Scala để so sánh các triển khai

import java.util.Date
import scala.testing.Benchmark

class BenchSort(sortfn: (Array[Int]) => Unit, name:String) extends Benchmark {

  val ints = new Array[Int](100000);

  override def prefix = name
  override def setUp = {
    val ran = new java.util.Random(5);
    for (i <- 0 to ints.length - 1)
      ints(i) = ran.nextInt();
  }
  override def run = sortfn(ints)
}

val benchImmut = new BenchSort( QuickSortS.sort , "Immutable/Functional/Scala" )
val benchMut   = new BenchSort( QuickSortJ.sort , "Mutable/Imperative/Java   " )

benchImmut.main( Array("5"))
benchMut.main( Array("5"))

Các kết quả

Thời gian tính bằng mili giây cho năm lần chạy liên tiếp

Immutable/Functional/Scala    467    178    184    187    183
Mutable/Imperative/Java        51     14     12     12     12

10
Nó là tốn kém khi thực hiện một cách ngây thơ hoặc với các phương pháp được phát triển cho các ngôn ngữ mệnh lệnh. Một trình biên dịch thông minh (ví dụ: GHC, trình biên dịch Haskell - và Haskell chỉ có các giá trị bất biến) có thể tận dụng tính bất biến và hiệu suất hoạt động có thể cạnh tranh với mã sử dụng khả năng thay đổi. Không cần phải nói, việc triển khai ngây thơ của quicksort vẫn khá chậm vì nó sử dụng, trong số những thứ đắt tiền khác, đệ quy nặng và kết hợp O(n)danh sách. Đó là ngắn hơn so với phiên bản giả mặc dù;)

3
A, bài viết blog tuyệt vời liên quan là ở đây: blogs.sun.com/jrose/entry/larval_objects_in_the_vm khuyến khích anh ta, bởi vì điều này sẽ có lợi cho Java cũng như ngôn ngữ VM chức năng đáng kể
andersoj

2
chủ đề SO này có rất nhiều cuộc thảo luận chi tiết về hiệu quả của lập trình chức năng. stackoverflow.com/questions/1990464/… . Trả lời rất nhiều điều tôi muốn biết.
smartnut007,

5
Điều ngây thơ nhất ở đây là điểm chuẩn của bạn. Bạn không thể chuẩn bất cứ thứ gì với mã như thế! Bạn nên nghiêm túc đọc một số bài báo về việc thực hiện điểm chuẩn trên JVM trước khi đưa ra bất kỳ kết luận nào ... bạn có biết rằng JVM có thể chưa JITted mã của bạn trước khi bạn chạy nó không? Bạn đã đặt kích thước intial và max của heap một cách thích hợp (để bạn không xem xét thời gian mà các quy trình JVM yêu cầu thêm bộ nhớ?)? Bạn có biết những phương pháp nào đang được biên dịch hoặc biên dịch lại không? Bạn có biết về GC không? Kết quả bạn nhận được từ mã này hoàn toàn không có ý nghĩa gì!
Bruno Reis

2
@userunknown Không, đó là khai báo. Lập trình mệnh lệnh "thay đổi trạng thái bằng các lệnh" trong khi lập trình chức năng "là một mô hình lập trình khai báo" "tránh thay đổi trạng thái" ( Wikipedia ). Vì vậy, vâng, chức năng và mệnh lệnh là hai thứ hoàn toàn khác nhau và mã bạn đã viết không bắt buộc.
Brian McCutchon

Câu trả lời:


106

Vì có một vài quan niệm sai lầm đang bay quanh đây, tôi muốn làm rõ một số điểm.

  • Quicksort “tại chỗ” không thực sự đúng vị trí (và theo định nghĩa , nhanh chóng không phải là tại chỗ). Nó yêu cầu lưu trữ bổ sung ở dạng không gian ngăn xếp cho bước đệ quy, theo thứ tự là O (log n ) trong trường hợp tốt nhất, nhưng O ( n ) trong trường hợp xấu nhất.

  • Việc triển khai một biến thể chức năng của quicksort hoạt động trên các mảng sẽ đánh bại mục đích. Mảng không bao giờ bất biến.

  • Việc triển khai chức năng "thích hợp" của quicksort sử dụng danh sách bất biến. Tất nhiên nó không phải tại chỗ nhưng nó có cùng thời gian chạy tiệm cận trong trường hợp xấu nhất ( O ( n ^ 2)) và độ phức tạp không gian ( O ( n )) như phiên bản thủ tục tại chỗ.

    Trung bình, thời gian chạy của nó vẫn ngang bằng với thời gian chạy của biến thể tại chỗ ( O ( n log n )). Tuy nhiên, độ phức tạp không gian của nó vẫn là O ( n ).


hai nhược điểm rõ ràng của việc triển khai nhanh chức năng. Sau đây, chúng ta hãy xem xét việc triển khai tham chiếu này trong Haskell (tôi không biết Scala…) từ phần giới thiệu Haskell :

qsort []     = []
qsort (x:xs) = qsort lesser ++ [x] ++ qsort greater
    where lesser  = (filter (< x) xs)
          greater = (filter (>= x) xs)
  1. Bất lợi đầu tiên là sự lựa chọn của phần tử trục , rất không linh hoạt. Sức mạnh của triển khai quicksort hiện đại chủ yếu dựa vào sự lựa chọn thông minh của trục xoay (so sánh “Kỹ thuật một chức năng sắp xếp” của Bentley và cộng sự ). Thuật toán trên kém về mặt đó, làm giảm đáng kể hiệu suất trung bình.

  2. Thứ hai, thuật toán này sử dụng nối danh sách (thay vì xây dựng danh sách) là một phép toán O ( n ). Điều này không ảnh hưởng đến độ phức tạp tiệm cận nhưng nó là một yếu tố có thể đo lường được.

Một nhược điểm thứ ba hơi bị ẩn đi: không giống như biến thể "tại chỗ", việc triển khai này liên tục yêu cầu bộ nhớ từ heap cho các ô khuyết điểm của danh sách và có khả năng phân tán bộ nhớ khắp nơi. Kết quả là, thuật toán này có vị trí bộ nhớ cache rất kém . Tôi không biết liệu các trình phân bổ thông minh trong các ngôn ngữ lập trình chức năng hiện đại có thể giảm thiểu điều này hay không - nhưng trên các máy hiện đại, lỗi bộ nhớ cache đã trở thành một kẻ giết hiệu suất lớn.


Kết luận là gì? Không giống như những người khác, tôi sẽ không nói rằng quicksort vốn dĩ là bắt buộc và đó là lý do tại sao nó hoạt động không tốt trong môi trường FP. Ngược lại, tôi cho rằng quicksort là một ví dụ hoàn hảo về thuật toán chức năng: nó dịch liền mạch thành một môi trường bất biến, độ phức tạp không gian và thời gian chạy tiệm cận của nó ngang bằng với việc triển khai thủ tục và thậm chí việc triển khai thủ tục của nó cũng sử dụng đệ quy.

Nhưng thuật toán này vẫn hoạt động kém hơn khi bị giới hạn trong một miền bất biến. Lý do cho điều này là thuật toán có tính chất đặc biệt là được hưởng lợi từ rất nhiều tinh chỉnh (đôi khi mức thấp) chỉ có thể được thực hiện hiệu quả trên các mảng. Một mô tả ngây thơ về quicksort bỏ lỡ tất cả những phức tạp này (cả trong biến thể chức năng và thủ tục).

Sau khi đọc “Kỹ thuật một hàm sắp xếp”, tôi không còn có thể coi quicksort là một thuật toán tao nhã nữa. Được thực hiện một cách hiệu quả, nó là một mớ hỗn độn, tác phẩm của một kỹ sư, không phải của một nghệ sĩ (không làm giảm giá trị kỹ thuật! Điều này có thẩm mỹ riêng của nó).


Nhưng tôi cũng muốn chỉ ra rằng điểm này là đặc biệt đối với nhanh chóng. Không phải mọi thuật toán đều có thể tuân theo cùng một loại điều chỉnh cấp thấp. Rất nhiều thuật toán và cấu trúc dữ liệu thực sự có thể được thể hiện mà không làm giảm hiệu suất trong một môi trường bất biến.

Và tính bất biến thậm chí có thể làm giảm chi phí hiệu suất bằng cách loại bỏ nhu cầu sao chép tốn kém hoặc đồng bộ hóa chéo luồng.

Vì vậy, để trả lời câu hỏi ban đầu, “ tính bất biến có đắt không? ”- Trong trường hợp cụ thể của nhanh chóng, có một chi phí thực sự là kết quả của sự bất biến. Nhưng nói chung, không .


10
+1 - câu trả lời tuyệt vời! Mặc dù với cá nhân tôi, đôi khi còn hơn là không . Tuy nhiên, đó chỉ là tính cách - bạn đã giải thích các vấn đề rất tốt.
Rex Kerr

6
Bạn nên nói thêm rằng một triển khai thích hợp bằng cách sử dụng các giá trị không thay đổi có thể song song ngay lập tức thay vì các phiên bản bắt buộc. Trong bối cảnh công nghệ hiện đại, điều này càng trở nên quan trọng.
Raphael

Bao nhiêu sẽ sử dụng qsort lesser ++ (x : qsort greater)trợ giúp?
Solomon Ucko

42

Có rất nhiều điều sai lầm khi coi đây là điểm chuẩn của lập trình chức năng. Điểm nổi bật bao gồm:

  • Bạn đang sử dụng nguyên bản, có thể phải được đóng hộp / mở hộp. Bạn không cố gắng kiểm tra chi phí của việc gói các đối tượng nguyên thủy, bạn đang cố gắng kiểm tra tính bất biến.
  • Bạn đã chọn một thuật toán mà hoạt động tại chỗ có hiệu quả bất thường (và có thể là như vậy). Nếu bạn muốn chứng minh rằng tồn tại các thuật toán nhanh hơn khi được triển khai một cách thay đổi, thì đây là một lựa chọn tốt; nếu không, điều này có thể gây hiểu lầm.
  • Bạn đang sử dụng sai chức năng thời gian. Sử dụng System.nanoTime.
  • Điểm chuẩn quá ngắn để bạn có thể tự tin rằng việc biên dịch JIT sẽ không chiếm một phần đáng kể trong thời gian đo được.
  • Mảng không được phân chia một cách hiệu quả.
  • Mảng có thể thay đổi, vì vậy dù sao thì việc sử dụng chúng với FP cũng là một so sánh kỳ lạ.

Vì vậy, so sánh này là một minh họa tuyệt vời rằng bạn phải hiểu ngôn ngữ (và thuật toán) của mình một cách chi tiết để viết mã hiệu suất cao. Nhưng nó không phải là một so sánh tốt giữa FP và không FP. Nếu bạn muốn điều đó, hãy xem Haskell so với C ++ tại Trò chơi Điểm chuẩn Ngôn ngữ Máy tính . Thông báo về nhà ở đó là hình phạt thường không nhiều hơn hệ số 2 hoặc 3 hoặc hơn, nhưng nó thực sự phụ thuộc. (Không có lời hứa rằng những người Haskell cũng đã viết ra các thuật toán nhanh nhất có thể, nhưng ít nhất một số người trong số họ có thể đã thử! Sau đó, một lần nữa, một số Haskell gọi thư viện C ....)

Bây giờ, giả sử bạn muốn một điểm chuẩn hợp lý hơn của Quicksort, nhận ra rằng đây có lẽ là một trong những trường hợp tồi tệ nhất đối với thuật toán FP so với có thể thay đổi và bỏ qua vấn đề cấu trúc dữ liệu (tức là giả sử rằng chúng ta có thể có một Mảng bất biến):

object QSortExample {
  // Imperative mutable quicksort
  def swap(xs: Array[String])(a: Int, b: Int) {
    val t = xs(a); xs(a) = xs(b); xs(b) = t
  }
  def muQSort(xs: Array[String])(l: Int = 0, r: Int = xs.length-1) {
    val pivot = xs((l+r)/2)
    var a = l
    var b = r
    while (a <= b) {
      while (xs(a) < pivot) a += 1
      while (xs(b) > pivot) b -= 1
      if (a <= b) {
        swap(xs)(a,b)
        a += 1
        b -= 1
      }
    }
    if (l<b) muQSort(xs)(l, b)
    if (b<r) muQSort(xs)(a, r)
  }

  // Functional quicksort
  def fpSort(xs: Array[String]): Array[String] = {
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length/2)
      val (small,big) = xs.partition(_ < pivot)
      if (small.length == 0) {
        val (bigger,same) = big.partition(_ > pivot)
        same ++ fpSort(bigger)
      }
      else fpSort(small) ++ fpSort(big)
    }
  }

  // Utility function to repeat something n times
  def repeat[A](n: Int, f: => A): A = {
    if (n <= 1) f else { f; repeat(n-1,f) }
  }

  // This runs the benchmark
  def bench(n: Int, xs: Array[String], silent: Boolean = false) {
    // Utility to report how long something took
    def ptime[A](f: => A) = {
      val t0 = System.nanoTime
      val ans = f
      if (!silent) printf("elapsed: %.3f sec\n",(System.nanoTime-t0)*1e-9)
      ans
    }

    if (!silent) print("Scala builtin ")
    ptime { repeat(n, {
      val ys = xs.clone
      ys.sorted
    }) }
    if (!silent) print("Mutable ")
    ptime { repeat(n, {
      val ys = xs.clone
      muQSort(ys)()
      ys
    }) }
    if (!silent) print("Immutable ")
    ptime { repeat(n, {
      fpSort(xs)
    }) }
  }

  def main(args: Array[String]) {
    val letters = (1 to 500000).map(_ => scala.util.Random.nextPrintableChar)
    val unsorted = letters.grouped(5).map(_.mkString).toList.toArray

    repeat(3,bench(1,unsorted,silent=true))  // Warmup
    repeat(3,bench(10,unsorted))     // Actual benchmark
  }
}

Lưu ý việc sửa đổi đối với Quicksort chức năng để nó chỉ duyệt qua dữ liệu một lần nếu có thể và so sánh với sắp xếp tích hợp. Khi chúng tôi chạy nó, chúng tôi nhận được một cái gì đó như:

Scala builtin elapsed: 0.349 sec
Mutable elapsed: 0.445 sec
Immutable elapsed: 1.373 sec
Scala builtin elapsed: 0.343 sec
Mutable elapsed: 0.441 sec
Immutable elapsed: 1.374 sec
Scala builtin elapsed: 0.343 sec
Mutable elapsed: 0.442 sec
Immutable elapsed: 1.383 sec

Vì vậy, ngoài việc học rằng cố gắng viết phân loại của riêng bạn là một ý tưởng tồi, chúng tôi thấy rằng có một hình phạt ~ 3 lần cho một đoạn nhanh không thay đổi nếu sau này được thực hiện một cách cẩn thận. (Bạn cũng có thể viết một phương thức trisect trả về ba mảng: những mảng nhỏ hơn, những mảng bằng nhau và những mảng lớn hơn pivot. Điều này có thể tăng tốc mọi thứ hơn một chút.)


Chỉ liên quan đến quyền anh / unboxing. Nếu có bất cứ điều gì thì đây sẽ là một hình phạt ở phía java phải không? Int không phải là kiểu chữ số ưa thích cho Scala (so với Số nguyên). Vì vậy, không có quyền anh nào xảy ra ở khía cạnh bỏng. Quyền anh chỉ là một vấn đề ở phía java vì autoboxing dạng scala Int tới java.lang.Integer / int. đây là một liên kết mà cuộc đàm phán về chủ đề này một cách chi tiết ansorg-it.com/en/scalanews-001.html
smartnut007

Vâng, tôi đang đóng vai người ủng hộ quỷ ở đây. Khả năng thay đổi là một phần nội bộ của thiết kế nhanh chóng. Đó là lý do tại sao tôi rất tò mò về cách tiếp cận chức năng thuần túy cho vấn đề. Haizz, tôi đã nói câu này lần thứ 10 trên chủ đề :-). Sẽ xem phần còn lại của bài viết của bạn khi tôi thức dậy và quay lại. cảm ơn.
smartnut007

2
@ smartnut007 - Bộ sưu tập Scala là chung. Generics yêu cầu các kiểu đóng hộp đối với hầu hết các phần (mặc dù có một nỗ lực đang được tiến hành để chuyên biệt hóa chúng cho một số kiểu nguyên thủy nhất định). Vì vậy, bạn không thể sử dụng tất cả các phương thức thu thập tiện lợi và giả định rằng sẽ không bị phạt khi bạn chuyển các tập hợp có kiểu nguyên thủy qua chúng. Có khả năng là loại nguyên thủy sẽ phải được đóng hộp trên đường vào và được mở hộp trên đường ra.
Rex Kerr

Tôi không thích thực tế là lỗ hổng đầu bạn đã nêu chỉ là một suy đoán :-)
smartnut007

1
@ smartnut007 - Đó là một thiếu sót hàng đầu vì rất khó để kiểm tra và nếu đúng thực sự sẽ củng cố kết quả. Nếu bạn chắc chắn rằng không có quyền anh, thì tôi đồng ý rằng lỗ hổng đó không hợp lệ. Lỗ hổng này không phải là có boxing, nó là bạn không biết liệu có đấm bốc (và tôi không chắc chắn một trong hai - chuyên môn đã thực hiện điều này khó khăn để tìm ra). Về phía Java (hoặc triển khai có thể thay đổi Scala) không có quyền anh vì bạn chỉ sử dụng các nguyên thủy. Dù sao, một phiên bản bất biến hoạt động thông qua n log n không gian, vì vậy bạn thực sự kết thúc việc so sánh chi phí so sánh / hoán đổi với cấp phát bộ nhớ.
Rex Kerr

10

Tôi không nghĩ rằng phiên bản Scala thực sự là đệ quy đuôi, vì bạn đang sử dụng Array.concat.

Ngoài ra, chỉ vì đây là mã Scala thành ngữ, điều này không có nghĩa là nó là cách tốt nhất để làm điều đó.

Cách tốt nhất để làm điều này là sử dụng một trong các chức năng sắp xếp có sẵn của Scala. Bằng cách đó, bạn nhận được sự đảm bảo về tính bất biến và biết rằng bạn có một thuật toán nhanh chóng.

Xem câu hỏi Stack Overflow Làm cách nào để sắp xếp một mảng trong Scala? Ví dụ.


4
cũng được, tôi không nghĩ rằng có một đệ quy đuôi nhanh chóng loại có thể, khi bạn cần phải thực hiện hai cuộc gọi đệ quy
Alex Lo

1
Có thể, bạn chỉ cần sử dụng các bao đóng tiếp tục để nâng các khung sẽ-sắp-xếp của mình lên đống.
Brian

inbuilt scala.util.Sorting.quickSort (mảng) thay đổi mảng. Nó hoạt động nhanh như java, không có gì đáng ngạc nhiên. Tôi quan tâm đến một giải pháp chức năng thuần túy hiệu quả. Nếu không, tại sao. Đó có phải là một hạn chế của Scala hay mô hình chức năng nói chung. đại loại vậy.
smartnut007

@ smartnut007: Bạn đang dùng phiên bản Scala nào vậy? Trong Scala 2.8, bạn có thể thực hiện việc array.sortednày trả về một mảng được sắp xếp mới, không thay đổi mảng ban đầu.
missfaktor

@AlexLo - có thể sắp xếp nhanh đệ quy đuôi. Một cái gì đó như:TAIL-RECURSIVE-QUICKSORT(Array A, int lo, int hi): while p < r: q = PARTITION(A, lo, hi); TAIL-RECURSIVE-QUICKSORT(A, lo, q - 1); p = q + 1;
Jakeway

8

Tính bất biến không đắt. Nó chắc chắn có thể tốn kém nếu bạn đo lường một tập hợp con nhỏ các nhiệm vụ mà một chương trình phải thực hiện và chọn một giải pháp dựa trên khả năng thay đổi để khởi động - chẳng hạn như đo độ nhanh.

Nói một cách đơn giản, bạn sẽ không nhanh khi sử dụng các ngôn ngữ chức năng thuần túy.

Hãy xem xét điều này từ một góc độ khác. Hãy xem xét hai chức năng sau:

// Version using mutable data structures
def tailFrom[T : ClassManifest](arr: Array[T], p: T => Boolean): Array[T] = {
  def posIndex(i: Int): Int = {
    if (i < arr.length) {
      if (p(arr(i)))
        i
      else
        posIndex(i + 1)
    } else {
      -1
    }
  }

  var index = posIndex(0)

  if (index < 0) Array.empty
  else {
    var result = new Array[T](arr.length - index)
    Array.copy(arr, index, result, 0, arr.length - index)
    result
  }
}

// Immutable data structure:

def tailFrom[T](list: List[T], p: T => Boolean): List[T] = {
  def recurse(sublist: List[T]): List[T] = {
    if (sublist.isEmpty) sublist
    else if (p(sublist.head)) sublist
    else recurse(sublist.tail)
  }
  recurse(list)
}

Điểm chuẩn RẰNG, và bạn sẽ thấy rằng mã sử dụng cấu trúc dữ liệu có thể thay đổi có hiệu suất kém hơn nhiều, bởi vì nó cần sao chép mảng, trong khi mã bất biến không cần phải quan tâm đến điều đó.

Khi bạn lập trình với cấu trúc dữ liệu bất biến, bạn cấu trúc mã của mình để tận dụng các điểm mạnh của nó. Nó không chỉ đơn giản là kiểu dữ liệu, hay thậm chí là các thuật toán riêng lẻ. Chương trình sẽ được thiết kế theo một cách khác.

Đó là lý do tại sao điểm chuẩn thường vô nghĩa. Hoặc bạn chọn các thuật toán phù hợp với phong cách này hay phong cách khác, và phong cách đó chiến thắng hoặc bạn đánh giá chuẩn cho toàn bộ ứng dụng, điều này thường không thực tế.


7

Sắp xếp một mảng, giống như, là nhiệm vụ cấp thiết nhất trong vũ trụ. Không có gì đáng ngạc nhiên khi nhiều chiến lược / triển khai 'bất biến' thanh lịch không thành công trên một microbenchmark 'sắp xếp một mảng'. Tuy nhiên, điều này không có nghĩa là tính bất biến là đắt "nói chung". Có nhiều tác vụ trong đó các triển khai không thể thay đổi sẽ thực hiện tương đương với các triển khai có thể thay đổi, nhưng sắp xếp mảng thường không phải là một trong số chúng.


7

Nếu bạn chỉ đơn giản viết lại các thuật toán và cấu trúc dữ liệu mệnh lệnh của mình sang ngôn ngữ chức năng thì quả thực sẽ rất tốn kém và vô ích. Để làm cho mọi thứ trở nên sáng sủa, bạn nên sử dụng các tính năng chỉ có trong lập trình chức năng: tính bền bỉ của cấu trúc dữ liệu, đánh giá lười biếng, v.v.


bạn có thể đủ tốt để cung cấp một triển khai trong Scala.
smartnut007

3
powells.com/biblio/17-0521631246-0 (Cấu trúc dữ liệu chức năng thuần túy của Chris Okasaki) - chỉ cần xem qua cuốn sách này. Nó có một câu chuyện mạnh mẽ để kể về việc tận dụng các lợi ích của lập trình chức năng, khi triển khai các thuật toán và cấu trúc dữ liệu hiệu quả.
Vasil Remeniuk

1
code.google.com/p/pfds một số cấu trúc dữ liệu được Debashish Ghosh triển khai trong Scala
Vasil Remeniuk

Bạn có thể vui lòng giải thích tại sao bạn nghĩ rằng Scala không bắt buộc? list.filter (foo).sort (bar).take (10)- điều gì có thể bắt buộc hơn?
người dùng không xác định

7

Chi phí bất biến trong Scala

Đây là một phiên bản gần như nhanh hơn phiên bản Java. ;)

object QuickSortS {
  def sort(xs: Array[Int]): Array[Int] = {
    val res = new Array[Int](xs.size)
    xs.copyToArray(res)
    (new QuickSortJ).sort(res)
    res
  }
}

Phiên bản này tạo một bản sao của mảng, sắp xếp nó tại chỗ bằng phiên bản Java và trả về bản sao. Scala không buộc bạn phải sử dụng cấu trúc bất biến trong nội bộ.

Vì vậy, lợi ích của Scala là bạn có thể tận dụng khả năng thay đổi và tính bất biến khi bạn thấy phù hợp. Điều bất lợi là nếu bạn làm điều đó sai, bạn không thực sự nhận được những lợi ích của tính bất biến.


Mặc dù đây không phải là câu trả lời chính xác cho câu hỏi, nhưng tôi nghĩ nó là một phần của bất kỳ câu trả lời hay nào: Quicksort nhanh hơn khi sử dụng cấu trúc có thể thay đổi. Nhưng lợi thế chính của tính bất biến là giao diện và trong Scala ít nhất bạn có thể có cả hai. Khả năng thay đổi nhanh hơn đối với quicksort, nhưng điều đó không ảnh hưởng đến cách viết của bạn, hầu hết là mã bất biến.
Paul Draper

7

QuickSort được biết là nhanh hơn khi được thực hiện tại chỗ, vì vậy đây khó có thể là một so sánh công bằng!

Đã nói rằng ... Array.concat? Nếu không có gì khác, bạn đang chỉ ra cách một kiểu tập hợp được tối ưu hóa cho lập trình mệnh lệnh đặc biệt chậm khi bạn thử và sử dụng nó trong một thuật toán chức năng; hầu như bất kỳ sự lựa chọn nào khác sẽ nhanh hơn!


Một điểm rất quan trọng khác cần xem xét, có lẽ vấn đề quan trọng nhất khi so sánh hai cách tiếp cận là: "cách này mở rộng ra nhiều nút / lõi tốt như thế nào?"

Rất có thể, nếu bạn đang tìm kiếm một mạch nhanh bất biến thì bạn đang làm như vậy bởi vì bạn thực sự muốn một mạch nhanh song song. Wikipedia có một số trích dẫn về chủ đề này: http://en.wikipedia.org/wiki/Quicksort#Parallelizations

Phiên bản scala có thể chỉ cần fork trước khi hàm này tái diễn, cho phép nó sắp xếp rất nhanh một danh sách chứa hàng tỷ mục nhập nếu bạn có đủ lõi.

Hiện tại, GPU trong hệ thống của tôi có sẵn 128 lõi nếu tôi có thể chạy mã Scala trên đó và đây là trên một hệ thống máy tính để bàn đơn giản chậm hơn thế hệ hiện tại hai năm.

Tôi tự hỏi điều đó sẽ xếp chồng lên nhau như thế nào so với cách tiếp cận mệnh lệnh đơn luồng ...

Có lẽ câu hỏi quan trọng hơn là:

"Do các lõi riêng lẻ sẽ không nhanh hơn nữa và đồng bộ hóa / khóa tạo ra một thách thức thực sự đối với song song, khả năng thay đổi có đắt không?"


Không có đối số ở đó. Quicksort theo định nghĩa là một sắp xếp trong bộ nhớ. Tôi chắc rằng hầu hết mọi người đều nhớ điều đó từ thời đại học. Nhưng, làm thế nào để bạn nhanh chóng sắp xếp theo một cách chức năng thuần túy. tức là không có tác dụng phụ.
smartnut007

Nguyên nhân quan trọng của nó, có những tuyên bố rằng mô hình chức năng có thể nhanh như các chức năng có tác dụng phụ.
smartnut007

Phiên bản danh sách cắt giảm một nửa thời gian. Tuy nhiên, không phải bất kỳ nơi nào gần với tốc độ của phiên bản java.
smartnut007

Bạn có thể vui lòng giải thích tại sao bạn nghĩ rằng Scala không bắt buộc? list.filter (foo).sort (bar).take (10)- điều gì có thể bắt buộc hơn? Cảm ơn.
người dùng không xác định

@ người dùng không xác định: Có lẽ bạn có thể làm rõ những gì bạn nghĩ "mệnh lệnh" có nghĩa là, bởi vì ví dụ đã nêu của bạn trông rất hữu ích đối với tôi. Bản thân Scala không mang tính bắt buộc cũng như không mang tính khai báo, ngôn ngữ hỗ trợ cả hai kiểu và các thuật ngữ này được sử dụng tốt nhất để mô tả các ví dụ cụ thể.
Kevin Wright

2

Người ta nói rằng lập trình OO sử dụng tính trừu tượng để che giấu sự phức tạp và lập trình chức năng sử dụng tính bất biến để loại bỏ sự phức tạp. Trong thế giới hỗn hợp của Scala, chúng ta có thể sử dụng OO để ẩn mã bắt buộc để lại mã ứng dụng không có gì là khôn ngoan hơn. Thật vậy, các thư viện bộ sưu tập sử dụng nhiều mã bắt buộc nhưng điều đó không có nghĩa là chúng ta không nên sử dụng chúng. Như những người khác đã nói, sử dụng một cách cẩn thận, bạn thực sự có được điều tốt nhất của cả hai thế giới ở đây.


Bạn có thể vui lòng giải thích tại sao bạn nghĩ rằng Scala không bắt buộc? list.filter (foo).sort (bar).take (10)- điều gì có thể bắt buộc hơn? Cảm ơn.
người dùng không xác định

Tôi không thấy nơi anh ấy nói Scala không bắt buộc.
Janx
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.