Warum haben Scala und Frameworks wie Spark und Scalding beide reduce
und foldLeft
? Was ist dann der Unterschied zwischen reduce
und fold
?
Warum haben Scala und Frameworks wie Spark und Scalding beide reduce
und foldLeft
? Was ist dann der Unterschied zwischen reduce
und fold
?
Antworten:
Ein großer Unterschied, der in keiner anderen Stackoverflow-Antwort zu diesem Thema eindeutig erwähnt wird, besteht darin, dass reduce
ein kommutatives Monoid angegeben werden sollte , dh eine Operation, die sowohl kommutativ als auch assoziativ ist. Dies bedeutet, dass die Operation parallelisiert werden kann.
Diese Unterscheidung ist sehr wichtig für Big Data / MPP / Distributed Computing und der gesamte Grund, warum es reduce
überhaupt existiert. Die Sammlung kann zerhackt werden und die reduce
Dose kann an jedem Block arbeiten, dann reduce
kann die Dose an den Ergebnissen jedes Blocks arbeiten - tatsächlich muss die Chunking-Ebene nicht eine Ebene tiefer anhalten. Wir könnten auch jedes Stück zerhacken. Aus diesem Grund ist das Summieren von Ganzzahlen in einer Liste O (log N), wenn eine unendliche Anzahl von CPUs angegeben wird.
Wenn Sie sich nur die Signaturen ansehen, gibt es keinen Grund reduce
zu existieren, weil Sie reduce
mit einem alles erreichen können, was Sie können foldLeft
. Die Funktionalität von foldLeft
ist größer als die Funktionalität von reduce
.
Aber man kann eine nicht parallelisieren foldLeft
, so dass ihre Laufzeit ist immer O (N) (auch wenn Sie in einem kommutativen Monoid füttern). Dies liegt daran, dass angenommen wird, dass die Operation kein kommutatives Monoid ist und der kumulierte Wert daher durch eine Reihe aufeinanderfolgender Aggregationen berechnet wird.
foldLeft
nimmt weder Kommutativität noch Assoziativität an. Es ist die Assoziativität, die es ermöglicht, die Sammlung zu zerlegen, und die Kommutativität, die das Kumulieren erleichtert, da die Reihenfolge nicht wichtig ist (es spielt also keine Rolle, in welcher Reihenfolge die einzelnen Ergebnisse aus den einzelnen Blöcken aggregiert werden sollen). Genau genommen ist Kommutativität für die Parallelisierung nicht erforderlich, beispielsweise für verteilte Sortieralgorithmen. Sie erleichtert lediglich die Logik, da Sie Ihren Chunks keine Reihenfolge geben müssen.
Wenn Sie sich die Spark-Dokumentation ansehen, reduce
heißt es speziell "... kommutativer und assoziativer Binäroperator".
http://spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.rdd.RDD
Hier ist ein Beweis, der reduce
NICHT nur ein Sonderfall von istfoldLeft
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
Hier kommt es den FP / mathematischen Wurzeln etwas näher und es ist etwas schwieriger zu erklären. Reduzieren wird formal als Teil des MapReduce-Paradigmas definiert, das sich mit geordneten Sammlungen (Multisets) befasst. Falten wird formal als Rekursion definiert (siehe Katamorphose) und nimmt daher eine Struktur / Sequenz zu den Sammlungen an.
fold
In Scalding gibt es keine Methode, da wir sie unter dem (strengen) Map Reduce-Programmiermodell nicht definieren können, fold
da Chunks keine Reihenfolge haben und fold
nur Assoziativität und keine Kommutativität erfordern.
Einfach ausgedrückt, reduce
funktioniert ohne eine Reihenfolge der Kumulierung, fold
erfordert eine Reihenfolge der Kumulierung und es ist diese Reihenfolge der Kumulierung, die einen Nullwert erfordert, NICHT die Existenz des Nullwerts, der sie unterscheidet. Genau genommen reduce
sollte dies für eine leere Sammlung funktionieren, da ihr Nullwert abgeleitet werden kann, indem ein beliebiger Wert genommen x
und dann gelöst x op y = x
wird. Dies funktioniert jedoch nicht mit einer nicht kommutativen Operation, da es einen unterschiedlichen linken und rechten Nullwert geben kann (dh x op y != y op x
). Natürlich macht sich Scala nicht die Mühe, herauszufinden, was dieser Nullwert ist, da dies etwas Mathematik erfordern würde (die wahrscheinlich nicht berechenbar ist), also löst sie einfach eine Ausnahme aus.
Es scheint (wie es in der Etymologie häufig der Fall ist), dass diese ursprüngliche mathematische Bedeutung verloren gegangen ist, da der einzige offensichtliche Unterschied in der Programmierung die Signatur ist. Das Ergebnis ist, dass reduce
es zu einem Synonym für fold
MapReduce geworden ist , anstatt die ursprüngliche Bedeutung von MapReduce beizubehalten. Heutzutage werden diese Begriffe häufig synonym verwendet und verhalten sich in den meisten Implementierungen gleich (wobei leere Sammlungen ignoriert werden). Die Seltsamkeit wird durch Besonderheiten wie in Spark verschärft, auf die wir jetzt eingehen werden.
Spark hat also eine fold
, aber die Reihenfolge, in der Unterergebnisse (eines für jede Partition) kombiniert werden (zum Zeitpunkt des Schreibens), ist dieselbe Reihenfolge, in der Aufgaben erledigt werden - und somit nicht deterministisch. Vielen Dank an @CafeFeed für den Hinweis auf die fold
Verwendung runJob
. Nachdem ich den Code gelesen hatte, stellte ich fest, dass er nicht deterministisch ist. Weitere Verwirrung wird dadurch erzeugt, dass Spark ein treeReduce
aber nein hat treeFold
.
Es gibt einen Unterschied zwischen reduce
und fold
auch bei Anwendung auf nicht leere Sequenzen. Ersteres wird als Teil des MapReduce-Programmierparadigmas für Sammlungen mit beliebiger Reihenfolge definiert ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf ), und man sollte davon ausgehen, dass Operatoren nicht nur kommutativ sind assoziativ, um deterministische Ergebnisse zu liefern. Letzteres wird in Bezug auf Katomorphismen definiert und erfordert, dass die Sammlungen einen Sequenzbegriff haben (oder rekursiv wie verknüpfte Listen definiert sind) und daher keine kommutativen Operatoren erfordern.
In der Praxis aufgrund des unmathematischen Charakters der Programmierung reduce
und fold
neigen dazu, sich entweder korrekt (wie in Scala) oder falsch (wie in Spark) gleich zu verhalten.
Meiner Meinung nach würde Verwirrung vermieden, wenn die Verwendung des Begriffs fold
in Spark vollständig gestrichen würde. Zumindest hat spark einen Hinweis in der Dokumentation:
Dies verhält sich etwas anders als Fold-Operationen, die für nicht verteilte Sammlungen in funktionalen Sprachen wie Scala implementiert sind.
foldLeft
enthält das Left
in seinem Namen und warum gibt es auch eine Methode namens fold
.
.par
, (List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)
erhalte ich jedes Mal andere Ergebnisse.
reallyFold
Zuhälter schreiben , als :, das rdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f)
würde nicht f brauchen, um zu pendeln.
Wenn ich mich nicht irre, obwohl die Spark-API dies nicht erfordert, erfordert fold auch, dass das f kommutativ ist. Weil die Reihenfolge, in der die Partitionen aggregiert werden, nicht gewährleistet ist. Zum Beispiel wird im folgenden Code nur der erste Ausdruck sortiert:
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("")(_ + _))
}
Ausdrucken:
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("")(_ + _)
mehrmals mit 2+ Kernen arbeiten, werden Sie wahrscheinlich feststellen, dass eine zufällige (partitionierungsweise) Reihenfolge entsteht. Ich habe meine Antwort entsprechend aktualisiert.
fold
in Apache Spark ist nicht dasselbe wie fold
in nicht verteilten Sammlungen. Tatsächlich erfordert es eine kommutative Funktion , um deterministische Ergebnisse zu erzielen:
Dies verhält sich etwas anders als Fold-Operationen, die für nicht verteilte Sammlungen in funktionalen Sprachen wie Scala implementiert sind. Diese Faltoperation kann einzeln auf Partitionen angewendet werden und diese Ergebnisse dann in das Endergebnis falten, anstatt die Faltung nacheinander in einer definierten Reihenfolge auf jedes Element anzuwenden. Für Funktionen, die nicht kommutativ sind, kann das Ergebnis von dem einer Falte abweichen, die auf eine nicht verteilte Sammlung angewendet wird.
Dies wurde von Mishael Rosenthal gezeigt und von Make42 in seinem Kommentar vorgeschlagen .
Es wurde vermutet, dass das beobachtete Verhalten damit zusammenhängt, HashPartitioner
dass tatsächlich parallelize
nicht gemischt und nicht verwendet wirdHashPartitioner
.
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)
Erklärt:
Struktur vonfold
für 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
}
ist die gleiche Struktur wiereduce
für 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"))
}
wo runJob
wird die Partitionsreihenfolge nicht beachtet und es wird eine kommutative Funktion benötigt.
foldPartition
und reducePartition
sind in Bezug auf die Reihenfolge der Verarbeitung gleichwertig und werden effektiv (durch Vererbung und Delegierung) von reduceLeft
und foldLeft
an implementiert TraversableOnce
.
Schlussfolgerung: fold
RDD kann nicht von der Reihenfolge der Chunks abhängen und benötigt Kommutativität und Assoziativität .
fold
on RDD
s in der Tat genau das gleiche ist wie reduce
, aber dies berücksichtigt nicht die grundlegenden mathematischen Unterschiede (ich habe meine Antwort aktualisiert, um noch klarer zu sein). Obwohl ich nicht der Meinung bin, dass wir wirklich Kommutativität brauchen, vorausgesetzt, man ist zuversichtlich, was auch immer der Partionierer tut, es bewahrt die Ordnung.
runJob
Es tut mir leid, dass ich nicht herausfinden konnte, worum es Ihnen ging, aber nachdem ich den Code gelesen habe, sehe ich, dass das Kombinieren tatsächlich nach dem Ende einer Aufgabe erfolgt, NICHT nach der Reihenfolge der Partitionen. Es ist dieses Schlüsseldetail, das alles zusammenbringt. Ich habe meine Antwort erneut bearbeitet und damit den Fehler korrigiert, auf den Sie hinweisen. Könnten Sie bitte entweder Ihr Kopfgeld entfernen, da wir uns jetzt einig sind?
Ein weiterer Unterschied für Scalding ist die Verwendung von Kombinierern in Hadoop.
Stellen Sie sich vor, Ihre Operation ist ein kommutatives Monoid. Mit Reduzieren wird sie auch auf der Kartenseite angewendet, anstatt alle Daten zu Reduzierern zu mischen / zu sortieren. Bei foldLeft ist dies nicht der Fall.
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 }
}
Es ist immer empfehlenswert, Ihre Operationen in Scalding als Monoid zu definieren.