Tại sao Scala và các framework như Spark và Scalding có cả reduce
và foldLeft
? Vậy thì sự khác biệt giữa reduce
và là fold
gì?
Tại sao Scala và các framework như Spark và Scalding có cả reduce
và foldLeft
? Vậy thì sự khác biệt giữa reduce
và là fold
gì?
Câu trả lời:
Một sự khác biệt lớn, không được đề cập trong bất kỳ câu trả lời stackoverflow nào khác liên quan đến chủ đề này một cách rõ ràng, là nó reduce
phải được cung cấp một đơn thức giao hoán , tức là một phép toán vừa có tính chất giao hoán vừa có tính chất kết hợp. Điều này có nghĩa là hoạt động có thể được thực hiện song song.
Sự khác biệt này rất quan trọng đối với Dữ liệu lớn / MPP / điện toán phân tán và toàn bộ lý do tại sao reduce
thậm chí tồn tại. Bộ sưu tập có thể được cắt nhỏ và reduce
có thể hoạt động trên từng đoạn, sau đó reduce
có thể hoạt động dựa trên kết quả của từng đoạn - trên thực tế, mức độ phân khúc không cần phải dừng sâu một cấp. Chúng tôi cũng có thể chặt từng khúc. Đây là lý do tại sao tổng các số nguyên trong danh sách là O (log N) nếu có vô số CPU.
Nếu bạn chỉ nhìn vào các chữ ký thì không có lý do gì reduce
để tồn tại bởi vì bạn có thể đạt được mọi thứ bạn có thể reduce
với một foldLeft
. Chức năng của foldLeft
lớn hơn chức năng của reduce
.
Nhưng bạn không thể song song hóa a foldLeft
, vì vậy thời gian chạy của nó luôn là O (N) (ngay cả khi bạn cấp dữ liệu trong một đơn thức giao hoán). Điều này là do nó giả định rằng hoạt động không phải là một đơn nguyên giao hoán và do đó giá trị tích lũy sẽ được tính bằng một loạt các tổng hợp tuần tự.
foldLeft
không giả định tính giao hoán cũng như tính kết hợp. Tính liên kết mang lại khả năng chia nhỏ tập hợp và tính giao hoán giúp việc tích lũy trở nên dễ dàng vì thứ tự không quan trọng (vì vậy thứ tự nào để tổng hợp từng kết quả từ mỗi phần không quan trọng). Nói một cách chính xác, tính giao hoán không cần thiết cho song song, ví dụ như các thuật toán sắp xếp phân tán, nó chỉ làm cho logic dễ dàng hơn vì bạn không cần phải sắp xếp các khối của mình.
Nếu bạn xem tài liệu Spark cho reduce
nó cụ thể là "... toán tử nhị phân giao hoán và kết hợp"
http://spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.rdd.RDD
Đây là bằng chứng cho thấy reduce
KHÔNG chỉ là một trường hợp đặc biệt củafoldLeft
scala> val intParList: ParSeq[Int] = (1 to 100000).map(_ => scala.util.Random.nextInt()).par
scala> timeMany(1000, intParList.reduce(_ + _))
Took 462.395867 milli seconds
scala> timeMany(1000, intParList.foldLeft(0)(_ + _))
Took 2589.363031 milli seconds
Bây giờ đây là nơi nó tiến gần hơn một chút đến gốc FP / toán học và khó giải thích hơn một chút. Reduce được định nghĩa chính thức như là một phần của mô hình MapReduce, liên quan đến các tập hợp không có thứ tự (nhiều tập), Fold được định nghĩa chính thức về mặt đệ quy (xem catamorphism) và do đó giả định một cấu trúc / chuỗi cho các tập hợp.
Không có fold
phương pháp nào trong Scalding vì theo mô hình lập trình Map Reduce (nghiêm ngặt), chúng ta không thể xác định fold
vì các khối không có thứ tự và fold
chỉ yêu cầu tính liên kết chứ không phải tính giao hoán.
Nói một cách đơn giản, reduce
hoạt động mà không có thứ tự tích lũy, fold
yêu cầu một thứ tự tích lũy và chính thứ tự tích lũy đó yêu cầu một giá trị 0 KHÔNG phải sự tồn tại của giá trị 0 phân biệt chúng. Nói một cách chính xác là reduce
nên hoạt động trên một tập hợp rỗng, bởi vì giá trị 0 của nó có thể được suy ra bằng cách lấy một giá trị tùy ý x
và sau đó giải quyết x op y = x
, nhưng điều đó không hoạt động với một phép toán không giao hoán vì có thể tồn tại một giá trị 0 bên trái và bên phải khác nhau (tức là x op y != y op x
). Tất nhiên Scala không bận tâm đến việc tìm ra giá trị 0 này là gì vì nó sẽ yêu cầu thực hiện một số phép toán (có thể là không thể tính được), vì vậy chỉ cần ném một ngoại lệ.
Có vẻ như (thường là trường hợp trong từ nguyên học), ý nghĩa toán học ban đầu này đã bị mất, vì sự khác biệt rõ ràng duy nhất trong lập trình là chữ ký. Kết quả là nó reduce
đã trở thành một từ đồng nghĩa fold
, thay vì giữ nguyên nghĩa gốc của nó từ MapReduce. Giờ đây, các thuật ngữ này thường được sử dụng thay thế cho nhau và hoạt động giống nhau trong hầu hết các triển khai (bỏ qua các tập hợp trống). Sự kỳ lạ càng trở nên trầm trọng hơn bởi những đặc thù, như trong Spark, mà bây giờ chúng ta sẽ giải quyết.
Vì vậy, Spark không có một fold
, nhưng thứ tự mà sub kết quả (một cho mỗi phân vùng) được kết hợp (tại thời điểm viết bài) là thứ tự trong đó nhiệm vụ được hoàn thành - và do đó không xác định. Cảm ơn @CafeFeed đã chỉ ra cách fold
sử dụng runJob
, sau khi đọc qua mã, tôi nhận ra rằng nó không xác định. Sự nhầm lẫn hơn nữa được tạo ra bởi Spark có một treeReduce
nhưng không treeFold
.
Có sự khác biệt giữa reduce
và fold
ngay cả khi áp dụng cho các chuỗi không trống. Mô hình thứ nhất được định nghĩa là một phần của mô hình lập trình MapReduce trên các tập hợp có thứ tự tùy ý ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf ) và người ta phải giả định rằng các toán tử là giao hoán ngoài việc liên kết để đưa ra kết quả xác định. Phần thứ hai được định nghĩa theo catomorphisms và yêu cầu các tập hợp phải có khái niệm về trình tự (hoặc được định nghĩa đệ quy, như danh sách được liên kết), do đó không yêu cầu toán tử giao hoán.
Trong thực tế, do tính chất phi toán học của lập trình reduce
và fold
có xu hướng hoạt động theo cùng một cách, hoặc đúng (như trong Scala) hoặc không chính xác (như trong Spark).
Ý kiến của tôi là sẽ tránh được sự nhầm lẫn nếu việc sử dụng thuật ngữ fold
này bị loại bỏ hoàn toàn trong Spark. Ít nhất spark không có ghi chú trong tài liệu của họ:
Điều này hoạt động hơi khác với các hoạt động gấp được triển khai cho các bộ sưu tập không phân tán trong các ngôn ngữ chức năng như Scala.
foldLeft
có chứa tên Left
trong tên của nó và tại sao cũng có một phương thức được gọi fold
.
.par
, vì vậy (List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)
tôi nhận được kết quả khác nhau mỗi lần.
reallyFold
ma cô của riêng mình , như :, rdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f)
điều này không cần f để đi làm.
Nếu tôi không nhầm, mặc dù API Spark không yêu cầu nó, nhưng Folder cũng yêu cầu f phải có tính chất giao hoán. Bởi vì thứ tự mà các phân vùng sẽ được tổng hợp không được đảm bảo. Ví dụ trong đoạn mã sau, chỉ bản in đầu tiên được sắp xếp:
import org.apache.spark.{SparkConf, SparkContext}
object FoldExample extends App{
val conf = new SparkConf()
.setMaster("local[*]")
.setAppName("Simple Application")
implicit val sc = new SparkContext(conf)
val range = ('a' to 'z').map(_.toString)
val rdd = sc.parallelize(range)
println(range.reduce(_ + _))
println(rdd.reduce(_ + _))
println(rdd.fold("")(_ + _))
}
In ra:
abcdefghijklmnopqrstuvwxyz
abcghituvjklmwxyzqrsdefnop
defghinopjklmqrstuvabcwxyz
sc.makeRDD(0 to 9, 2).mapPartitions(it => { java.lang.Thread.sleep(new java.util.Random().nextInt(1000)); it } ).map(_.toString).fold("")(_ + _)
với 2+ lõi nhiều lần, tôi nghĩ bạn sẽ thấy nó tạo ra thứ tự ngẫu nhiên (phân vùng). Tôi đã cập nhật câu trả lời của mình cho phù hợp.
fold
trong Apache Spark không giống như fold
trên các bộ sưu tập không được phân phối. Trên thực tế, nó yêu cầu hàm giao hoán để tạo ra kết quả xác định:
Điều này hoạt động hơi khác với các hoạt động gấp được triển khai cho các bộ sưu tập không phân tán trong các ngôn ngữ chức năng như Scala. Thao tác gấp này có thể được áp dụng cho các phân vùng riêng lẻ và sau đó gấp các kết quả đó vào kết quả cuối cùng, thay vì áp dụng nếp gấp cho từng phần tử một cách tuần tự theo một số thứ tự xác định. Đối với các hàm không có tính chất giao hoán, kết quả có thể khác với kết quả của một nếp gấp áp dụng cho tập hợp không phân phối.
Điều này đã được thể hiện bởi Mishael Rosenthal và được Make42 gợi ý trong bình luận của mình .
Người ta cho rằng hành vi quan sát được có liên quan đến HashPartitioner
thời điểm thực tế parallelize
không xáo trộn và không sử dụng HashPartitioner
.
import org.apache.spark.sql.SparkSession
/* Note: standalone (non-local) mode */
val master = "spark://...:7077"
val spark = SparkSession.builder.master(master).getOrCreate()
/* Note: deterministic order */
val rdd = sc.parallelize(Seq("a", "b", "c", "d"), 4).sortBy(identity[String])
require(rdd.collect.sliding(2).forall { case Array(x, y) => x < y })
/* Note: all posible permutations */
require(Seq.fill(1000)(rdd.fold("")(_ + _)).toSet.size == 24)
Giải thích:
Cấu trúc củafold
RDD
def fold(zeroValue: T)(op: (T, T) => T): T = withScope {
var jobResult: T
val cleanOp: (T, T) => T
val foldPartition = Iterator[T] => T
val mergeResult: (Int, T) => Unit
sc.runJob(this, foldPartition, mergeResult)
jobResult
}
giống như cấu trúc củareduce
RDD:
def reduce(f: (T, T) => T): T = withScope {
val cleanF: (T, T) => T
val reducePartition: Iterator[T] => Option[T]
var jobResult: Option[T]
val mergeResult = (Int, Option[T]) => Unit
sc.runJob(this, reducePartition, mergeResult)
jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
}
nơi runJob
được thực hiện mà không quan tâm đến thứ tự phân vùng và kết quả là cần hàm giao hoán.
foldPartition
và reducePartition
tương đương về trình tự xử lý và hiệu quả (theo kế thừa và ủy quyền) được thực hiện bởi reduceLeft
và foldLeft
về sau TraversableOnce
.
Kết luận: fold
trên RDD không thể phụ thuộc vào thứ tự các khối và cần có tính giao hoán và tính liên kết .
fold
on RDD
s thực sự giống như vậy reduce
, nhưng điều này không tôn trọng sự khác biệt toán học cơ bản (tôi đã cập nhật câu trả lời của mình để thậm chí còn rõ ràng hơn). Mặc dù tôi không đồng ý rằng chúng ta thực sự cần tính giao hoán với điều kiện một người tin tưởng rằng bất cứ điều gì người dự tiệc của họ đang làm, đó là duy trì trật tự.
runJob
mã, tôi thấy rằng thực sự nó thực hiện việc kết hợp tùy theo thời điểm hoàn thành tác vụ, KHÔNG phải thứ tự của các phân vùng. Chính chi tiết quan trọng này đã làm cho mọi thứ vào đúng vị trí. Tôi đã chỉnh sửa lại câu trả lời của mình và do đó đã sửa lỗi bạn chỉ ra. Làm ơn, bạn có thể xóa tiền thưởng của mình không vì chúng tôi hiện đã đồng ý?
Một điểm khác biệt khác đối với Scalding là việc sử dụng các bộ kết hợp trong Hadoop.
Hãy tưởng tượng hoạt động của bạn là đơn nguyên giao hoán, với phép giảm, nó cũng sẽ được áp dụng trên bản đồ thay vì xáo trộn / sắp xếp tất cả dữ liệu vào bộ giảm. Với foldLeft, đây không phải là trường hợp.
pipe.groupBy('product) {
_.reduce('price -> 'total){ (sum: Double, price: Double) => sum + price }
// reduce is .mapReduceMap in disguise
}
pipe.groupBy('product) {
_.foldLeft('price -> 'total)(0.0){ (sum: Double, price: Double) => sum + price }
}
Việc xác định các hoạt động của bạn là monoid trong Scalding luôn là một thông lệ tốt.