ทำไม Scala และเฟรมเวิร์กเช่น Spark และ Scalding จึงมีทั้งสองอย่างreduce
และfoldLeft
? แล้วอะไรคือความแตกต่างระหว่างreduce
และfold
?
ทำไม Scala และเฟรมเวิร์กเช่น Spark และ Scalding จึงมีทั้งสองอย่างreduce
และfoldLeft
? แล้วอะไรคือความแตกต่างระหว่างreduce
และfold
?
คำตอบ:
ความแตกต่างที่ยิ่งใหญ่ซึ่งไม่ได้กล่าวถึงในคำตอบ stackoverflow อื่น ๆ ที่เกี่ยวข้องกับหัวข้อนี้อย่างชัดเจนคือreduce
ควรได้รับmonoid แบบสับเปลี่ยนนั่นคือการดำเนินการที่มีทั้งแบบสับเปลี่ยนและเชื่อมโยง ซึ่งหมายความว่าการดำเนินการสามารถขนานกันได้
ความแตกต่างนี้มีความสำคัญมากสำหรับ Big Data / MPP / คอมพิวเตอร์แบบกระจายและเหตุผลทั้งหมดที่ว่าทำไมreduce
ถึงมีอยู่ สามารถสับคอลเลกชันและreduce
สามารถทำงานในแต่ละชิ้นจากนั้นreduce
สามารถทำงานกับผลลัพธ์ของแต่ละชิ้น - ในความเป็นจริงระดับของการแบ่งไม่จำเป็นต้องหยุดลึกเพียงระดับเดียว เราสามารถสับแต่ละชิ้นได้ด้วย นี่คือเหตุผลที่การรวมจำนวนเต็มในรายการคือ O (log N) หากกำหนดให้ CPU มีจำนวนไม่ จำกัด
หากคุณเพียงแค่ดูลายเซ็นไม่มีเหตุผลที่reduce
จะมีอยู่เพราะคุณสามารถบรรลุทุกสิ่งที่ทำได้reduce
ด้วยไฟล์foldLeft
. การทำงานของfoldLeft
เป็นมากกว่าฟังก์ชันการทำงานของreduce
.
แต่คุณไม่สามารถขนาน a foldLeft
ได้ดังนั้นรันไทม์จึงเป็น O (N) เสมอ (แม้ว่าคุณจะป้อนข้อมูลใน monoid แบบสับเปลี่ยน) นี่เป็นเพราะถือว่าการดำเนินการไม่ใช่ monoid แบบสับเปลี่ยนดังนั้นค่าที่สะสมจะคำนวณโดยชุดของการรวมตามลำดับ
foldLeft
ไม่ถือว่าการสับเปลี่ยนหรือการเชื่อมโยง เป็นความเชื่อมโยงที่ช่วยให้สามารถสับคอลเลกชันได้และเป็นการสับเปลี่ยนที่ทำให้การรวบรวมเป็นเรื่องง่ายเนื่องจากคำสั่งซื้อไม่สำคัญ (ดังนั้นจึงไม่สำคัญว่าคำสั่งใดที่จะรวมแต่ละผลลัพธ์จากแต่ละชิ้น) การสื่อสารที่พูดอย่างเคร่งครัดไม่จำเป็นสำหรับการขนานกันตัวอย่างเช่นอัลกอริทึมการเรียงลำดับแบบกระจายมันทำให้ตรรกะง่ายขึ้นเพราะคุณไม่จำเป็นต้องให้ชิ้นส่วนของคุณเรียงลำดับ
หากคุณได้ดูเอกสาร Spark ที่reduce
ระบุว่า "... ตัวดำเนินการไบนารีแบบสับเปลี่ยนและเชื่อมโยง"
http://spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.rdd.RDD
นี่คือข้อพิสูจน์ว่าreduce
ไม่ใช่แค่กรณีพิเศษของfoldLeft
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
ตอนนี้นี่คือจุดที่มันเข้าใกล้ FP / รากทางคณิตศาสตร์มากขึ้นเล็กน้อยและยากที่จะอธิบาย การลดถูกกำหนดอย่างเป็นทางการเป็นส่วนหนึ่งของกระบวนทัศน์ MapReduce ซึ่งเกี่ยวข้องกับคอลเล็กชันที่ไม่เป็นระเบียบ (หลายชุด) Fold ถูกกำหนดอย่างเป็นทางการในแง่ของการเรียกซ้ำ (ดู catamorphism) ดังนั้นจึงถือว่าโครงสร้าง / ลำดับของคอลเล็กชัน
ไม่มีfold
วิธีการใดใน Scalding เนื่องจากภายใต้รูปแบบการเขียนโปรแกรมลดแผนที่ (เข้มงวด) เราไม่สามารถกำหนดได้fold
เนื่องจากชิ้นส่วนไม่มีลำดับและfold
ต้องใช้การเชื่อมโยงเท่านั้นไม่ใช่การสับเปลี่ยน
พูดง่ายๆว่าreduce
ทำงานได้โดยไม่ต้องมีลำดับของการสะสมfold
ต้องมีลำดับของการสะสมและเป็นลำดับของการสะสมที่จำเป็นต้องมีค่าเป็นศูนย์ไม่ใช่การมีอยู่ของค่าศูนย์ที่แยกความแตกต่างออกไป การพูดอย่างเคร่งครัดreduce
ควรใช้กับคอลเลกชันที่ว่างเปล่าเนื่องจากค่าศูนย์สามารถอนุมานได้โดยการหาค่าตามอำเภอใจx
แล้วจึงแก้ปัญหาx op y = x
แต่จะใช้ไม่ได้กับการดำเนินการที่ไม่สับเปลี่ยนเนื่องจากอาจมีค่าศูนย์ซ้ายและขวาที่แตกต่างกัน (กล่าวคือx op y != y op x
). แน่นอนว่า Scala ไม่ต้องกังวลที่จะหาว่าค่าศูนย์นี้เป็นเท่าใดเนื่องจากต้องใช้คณิตศาสตร์บางอย่าง (ซึ่งอาจไม่สามารถคำนวณได้) ดังนั้นเพียงแค่โยนข้อยกเว้น
ดูเหมือนว่า (เช่นเดียวกับในนิรุกติศาสตร์) ความหมายทางคณิตศาสตร์ดั้งเดิมนี้ได้สูญหายไปเนื่องจากความแตกต่างที่ชัดเจนเพียงอย่างเดียวในการเขียนโปรแกรมคือลายเซ็น ผลลัพธ์คือreduce
กลายเป็นคำพ้องความหมายfold
แทนที่จะรักษาความหมายดั้งเดิมจาก MapReduce ปัจจุบันคำเหล่านี้มักใช้สลับกันและทำงานเหมือนกันในการใช้งานส่วนใหญ่ (ละเว้นคอลเล็กชันที่ว่างเปล่า) ความแปลกประหลาดถูกทำให้รุนแรงขึ้นโดยลักษณะเฉพาะเช่นเดียวกับใน Spark ที่เราจะกล่าวถึง
ดังนั้น Spark จึงมี a fold
แต่ลำดับที่ผลลัพธ์ย่อย (หนึ่งรายการสำหรับแต่ละพาร์ติชัน) ถูกรวมเข้าด้วยกัน (ในขณะที่เขียน) เป็นลำดับเดียวกันกับที่งานจะเสร็จสมบูรณ์ - และไม่ได้ถูกกำหนด ขอบคุณ @CafeFeed ที่ชี้ให้เห็นการfold
ใช้งานดังrunJob
กล่าวซึ่งหลังจากอ่านโค้ดแล้วฉันก็รู้ว่ามันไม่ใช่ปัจจัยกำหนด ความสับสนนอกจากนี้ถูกสร้างขึ้นโดย Spark มีแต่ไม่มีtreeReduce
treeFold
มีความแตกต่างระหว่างreduce
และfold
แม้ว่าจะใช้กับลำดับที่ไม่ว่างเปล่า อดีตถูกกำหนดให้เป็นส่วนหนึ่งของกระบวนทัศน์การเขียนโปรแกรม MapReduce บนคอลเลกชันที่มีคำสั่งตามอำเภอใจ ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf ) และควรถือว่าตัวดำเนินการมีการสับเปลี่ยนนอกเหนือจากการเป็น เชื่อมโยงเพื่อให้ผลลัพธ์ที่กำหนด คำหลังนี้ถูกกำหนดในรูปแบบของ catomorphisms และกำหนดให้คอลเลกชันมีความคิดของลำดับ (หรือกำหนดแบบวนซ้ำเช่นรายการที่เชื่อมโยง) จึงไม่จำเป็นต้องใช้ตัวดำเนินการสับเปลี่ยน
ในทางปฏิบัติเนื่องจากลักษณะทางคณิตศาสตร์ของการเขียนโปรแกรมreduce
และfold
มีแนวโน้มที่จะทำงานในลักษณะเดียวกันไม่ว่าจะถูกต้อง (เช่นใน Scala) หรือไม่ถูกต้อง (เช่นใน Spark)
ความคิดเห็นของฉันคือความสับสนจะหลีกเลี่ยงได้หากการใช้คำfold
นั้นถูกทิ้งใน Spark อย่างน้อย spark ก็มีหมายเหตุในเอกสารของพวกเขา:
สิ่งนี้ทำงานค่อนข้างแตกต่างจากการดำเนินการพับที่ใช้กับคอลเล็กชันที่ไม่กระจายในภาษาที่ใช้งานได้เช่น Scala
foldLeft
มีอยู่ในชื่อและทำไมยังมีวิธีการที่เรียกว่าLeft
fold
.par
จึง(List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)
ได้ผลลัพธ์ที่แตกต่างกันในแต่ละครั้ง
reallyFold
แมงดาของตัวเองได้เช่นrdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f)
นี้ไม่จำเป็นต้องใช้ f ในการเดินทาง
ถ้าฉันจำไม่ผิดแม้ว่า Spark API จะไม่ต้องการ แต่ก็ต้องมีการพับเพื่อให้ f มีการสับเปลี่ยน เนื่องจากลำดับในการรวมพาร์ติชันจะไม่มั่นใจ ตัวอย่างเช่นในรหัสต่อไปนี้จะมีการเรียงลำดับการพิมพ์ครั้งแรกเท่านั้น:
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("")(_ + _))
}
พิมพ์ออกมา:
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("")(_ + _)
ด้วย 2+ คอร์หลาย ๆ ครั้งฉันคิดว่าคุณจะเห็นว่ามันสร้างคำสั่งแบบสุ่ม (พาร์ติชันที่ชาญฉลาด) ฉันได้อัปเดตคำตอบตามนั้น
fold
ใน Apache Spark ไม่เหมือนกับfold
คอลเล็กชันที่ไม่ได้แจกจ่าย ในความเป็นจริงมันต้องใช้ฟังก์ชันสับเปลี่ยนเพื่อสร้างผลลัพธ์ที่กำหนด:
สิ่งนี้ทำงานค่อนข้างแตกต่างจากการดำเนินการพับที่ใช้กับคอลเล็กชันที่ไม่กระจายในภาษาที่ใช้งานได้เช่น Scala การดำเนินการพับนี้อาจนำไปใช้กับพาร์ติชั่นทีละพาร์ติชั่นแล้วพับผลลัพธ์เหล่านั้นเป็นผลลัพธ์สุดท้ายแทนที่จะใช้การพับกับแต่ละองค์ประกอบตามลำดับในลำดับที่กำหนดไว้ สำหรับฟังก์ชันที่ไม่ใช่การสับเปลี่ยนผลลัพธ์อาจแตกต่างจากการพับที่ใช้กับคอลเล็กชันแบบไม่กระจาย
นี้ได้รับการแสดงโดยมิชาเอลโรเซนธาลและแนะนำโดยMake42ในความคิดเห็นของเขา
จะได้รับการแนะนำว่าพฤติกรรมที่สังเกตที่เกี่ยวข้องกับHashPartitioner
ในเมื่อความจริงไม่ได้สับเปลี่ยนและไม่ได้ใช้parallelize
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)
อธิบาย:
โครงสร้างfold
สำหรับ 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
}
เหมือนกับโครงสร้างของreduce
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"))
}
ที่runJob
ดำเนินการโดยไม่คำนึงถึงลำดับพาร์ติชันและส่งผลให้ต้องมีฟังก์ชันสับเปลี่ยน
foldPartition
และreducePartition
เทียบเท่าในแง่ของคำสั่งของการประมวลผลและมีประสิทธิภาพ (โดยการรับมรดกและคณะ) ดำเนินการโดยreduceLeft
และบนfoldLeft
TraversableOnce
สรุป: fold
ใน RDD ไม่สามารถขึ้นอยู่กับคำสั่งของชิ้นและความต้องการcommutativity และการเชื่อมโยงกัน
fold
on RDD
s นั้นเหมือนกับจริง ๆreduce
แต่สิ่งนี้ไม่เคารพความแตกต่างทางคณิตศาสตร์ของราก (ฉันได้อัปเดตคำตอบของฉันให้ชัดเจนยิ่งขึ้น) แม้ว่าฉันจะไม่เห็นด้วยที่เราต้องการการสับเปลี่ยนจริงๆหากมีคนมั่นใจไม่ว่าพรรคพวกของพวกเขากำลังทำอะไรอยู่ แต่ก็รักษาความสงบเรียบร้อย
runJob
โค้ดแล้วฉันเห็นว่ามันทำการรวมตามเวลาที่งานเสร็จสิ้นไม่ใช่ลำดับของพาร์ติชัน นี่คือรายละเอียดที่สำคัญที่ทำให้ทุกอย่างเข้าที่ ฉันได้แก้ไขคำตอบของฉันอีกครั้งและแก้ไขข้อผิดพลาดที่คุณชี้ให้เห็นแล้ว ได้โปรดคุณช่วยลบค่าหัวของคุณเนื่องจากตอนนี้เราตกลงกันได้หรือไม่?
ความแตกต่างอีกอย่างหนึ่งสำหรับ Scalding คือการใช้ Combiners ใน Hadoop
ลองนึกภาพการดำเนินการของคุณเป็น monoid แบบสับเปลี่ยนโดยการลดจะถูกนำไปใช้กับด้านแผนที่แทนการสับ / จัดเรียงข้อมูลทั้งหมดไปยังตัวลด ด้วยการพับด้านซ้ายนี่ไม่ใช่กรณี
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 }
}
เป็นแนวทางปฏิบัติที่ดีเสมอในการกำหนดการดำเนินงานของคุณเป็นแบบ monoid ใน Scalding