Perché Scala e framework come Spark e Scalding hanno sia reduce
e foldLeft
? Allora qual è la differenza tra reduce
e fold
?
Perché Scala e framework come Spark e Scalding hanno sia reduce
e foldLeft
? Allora qual è la differenza tra reduce
e fold
?
Risposte:
Una grande grande differenza, non menzionata in qualsiasi altra risposta di stackoverflow relativa a questo argomento chiaramente, è che reduce
dovrebbe essere assegnato un monoide commutativo , cioè un'operazione che è sia commutativa che associativa. Ciò significa che l'operazione può essere parallelizzata.
Questa distinzione è molto importante per Big Data / MPP / calcolo distribuito e l'intera ragione per cui reduce
esiste. La raccolta può essere sminuzzata e reduce
può operare su ogni blocco, quindi reduce
può operare sui risultati di ogni blocco - infatti il livello di suddivisione non deve fermarsi a un livello profondo. Potremmo anche sminuzzare ogni pezzo. Questo è il motivo per cui la somma degli interi in una lista è O (log N) se viene fornito un numero infinito di CPU.
Se guardi solo le firme non c'è motivo reduce
di esistere perché puoi ottenere tutto ciò che puoi reduce
con un file foldLeft
. La funzionalità di foldLeft
è maggiore della funzionalità di reduce
.
Ma non puoi parallelizzare a foldLeft
, quindi il suo tempo di esecuzione è sempre O (N) (anche se inserisci un monoide commutativo). Questo perché si presume che l'operazione non sia un monoide commutativo e quindi il valore cumulato verrà calcolato da una serie di aggregazioni sequenziali.
foldLeft
non assume commutatività né associatività. È l'associatività che dà la possibilità di dividere la raccolta, ed è la commutatività che rende facile il cumulo perché l'ordine non è importante (quindi non importa quale ordine aggregare ciascuno dei risultati da ciascuno dei blocchi). A rigor di termini, la commutatività non è necessaria per la parallelizzazione, ad esempio gli algoritmi di ordinamento distribuito, ma semplifica semplicemente la logica perché non è necessario dare un ordine ai blocchi.
Se dai un'occhiata alla documentazione di Spark reduce
, dice specificamente "... operatore binario commutativo e associativo"
http://spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.rdd.RDD
Ecco la prova che reduce
NON è solo un caso speciale difoldLeft
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
Ora è qui che ci si avvicina un po 'alle radici matematiche / FP e un po' più complicato da spiegare. Reduce è definito formalmente come parte del paradigma MapReduce, che si occupa di raccolte senza ordine (multiset), Fold è formalmente definito in termini di ricorsione (vedi catamorfismo) e quindi assume una struttura / sequenza per le raccolte.
Non esiste un fold
metodo in Scalding perché sotto il modello di programmazione Map Reduce (rigoroso) non possiamo definirlo fold
perché i blocchi non hanno un ordine e fold
richiedono solo l'associatività, non la commutatività.
In parole povere, reduce
funziona senza un ordine di cumulo, fold
richiede un ordine di cumulo ed è quell'ordine di cumulo che richiede un valore zero NON l'esistenza del valore zero che li distingue. A rigor di termini reduce
dovrebbe funzionare su una raccolta vuota, perché il suo valore zero può essere dedotto prendendo un valore arbitrario x
e poi risolvendo x op y = x
, ma ciò non funziona con un'operazione non commutativa in quanto può esistere un valore zero sinistro e destro distinti (cioè x op y != y op x
). Ovviamente Scala non si preoccupa di capire quale sia questo valore zero in quanto ciò richiederebbe di fare un po 'di matematica (che probabilmente non è computabile), quindi lancia un'eccezione.
Sembra (come spesso accade in etimologia) che questo significato matematico originale sia andato perduto, poiché l'unica differenza evidente nella programmazione è la firma. Il risultato è che reduce
è diventato un sinonimo di fold
MapReduce, anziché preservare il suo significato originale. Ora questi termini sono spesso usati in modo intercambiabile e si comportano allo stesso modo nella maggior parte delle implementazioni (ignorando le raccolte vuote). La stranezza è esacerbata da peculiarità, come in Spark, che ora affronteremo.
Così Spark non hanno una fold
, ma l'ordine in cui vengono combinati sub risultati (uno per ogni partizione) (al momento della scrittura) è lo stesso ordine in cui sono espletate le attività - e quindi non deterministica. Grazie a @CafeFeed per aver sottolineato che fold
utilizza runJob
, che dopo aver letto il codice mi sono reso conto che non è deterministico. Ulteriore confusione è creata da Spark che ha un treeReduce
ma no treeFold
.
C'è una differenza tra reduce
e fold
anche quando applicato a sequenze non vuote. Il primo è definito come parte del paradigma di programmazione MapReduce su raccolte con ordine arbitrario ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf ) e si dovrebbe presumere che gli operatori siano commutativi oltre ad essere associativo per dare risultati deterministici. Quest'ultimo è definito in termini di catomorfismi e richiede che le collezioni abbiano una nozione di sequenza (o siano definite ricorsivamente, come liste concatenate), quindi non necessitano di operatori commutativi.
In pratica a causa della natura non matematica della programmazione, reduce
e fold
tendono a comportarsi allo stesso modo, correttamente (come in Scala) o non correttamente (come in Spark).
La mia opinione è che la confusione sarebbe evitata se l'uso del termine fold
fosse completamente abbandonato in Spark. Almeno Spark ha una nota nella loro documentazione:
Questo si comporta in modo un po 'diverso dalle operazioni di piegatura implementate per raccolte non distribuite in linguaggi funzionali come Scala.
foldLeft
contiene il Left
nel suo nome e perché c'è anche un metodo chiamato fold
.
.par
, (List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)
ottengo risultati diversi ogni volta.
reallyFold
magnaccia, come :, rdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f)
questo non avrebbe bisogno di f per andare al lavoro.
Se non sbaglio, anche se l'API Spark non lo richiede, fold richiede anche che la f sia commutativa. Perché l'ordine in cui le partizioni verranno aggregate non è garantito. Ad esempio nel codice seguente viene ordinata solo la prima stampa:
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("")(_ + _))
}
Stampare:
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("")(_ + _)
più volte con 2 o più core, penso che vedrai che produce un ordine casuale (in base alla partizione). Ho aggiornato di conseguenza la mia risposta.
fold
in Apache Spark non è lo stesso delle fold
raccolte non distribuite. Infatti richiede una funzione commutativa per produrre risultati deterministici:
Questo si comporta in modo un po 'diverso dalle operazioni di piegatura implementate per raccolte non distribuite in linguaggi funzionali come Scala. Questa operazione di piegatura può essere applicata alle partizioni individualmente, quindi piega quei risultati nel risultato finale, piuttosto che applicare la piega a ciascun elemento in sequenza in un ordine definito. Per le funzioni che non sono commutative, il risultato può differire da quello di una piega applicata a una raccolta non distribuita.
Questo è stato dimostrato da Mishael Rosenthal e suggerito da Make42 nel suo commento .
È stato suggerito che il comportamento osservato sia correlato a HashPartitioner
quando in realtà parallelize
non si mescola e non usa 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)
Spiegato:
Struttura difold
per 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
}
è la stessa struttura direduce
per 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"))
}
dove runJob
viene eseguito senza tenere conto dell'ordine di partizione e richiede una funzione commutativa.
foldPartition
e reducePartition
sono equivalenti in termini di ordine di elaborazione ed effettivamente (per eredità e delega) implementati da reduceLeft
e foldLeft
avanti TraversableOnce
.
Conclusione: fold
su RDD non può dipendere dall'ordine dei blocchi e necessita di commutatività e associatività .
fold
on RDD
s è davvero uguale a reduce
, ma questo non rispetta le differenze matematiche di base (ho aggiornato la mia risposta per essere ancora più chiara). Anche se non sono d'accordo sul fatto che abbiamo davvero bisogno di commutatività, a condizione che si sia sicuri che qualunque cosa stia facendo il proprio partitore, è preservare l'ordine.
runJob
codice vedo che in effetti fa la combinazione in base a quando un'attività è terminata, NON all'ordine delle partizioni. È questo dettaglio chiave che fa sì che tutto vada a posto. Ho modificato di nuovo la mia risposta e quindi ho corretto l'errore che hai segnalato. Per favore, potresti rimuovere la tua taglia visto che ora siamo d'accordo?
Un'altra differenza per Scalding è l'uso di combinatori in Hadoop.
Immagina che la tua operazione sia monoide commutativa, con riduci verrà applicato anche sul lato mappa invece di mescolare / ordinare tutti i dati ai riduttori. Con foldLeft questo non è il caso.
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 }
}
È sempre buona norma definire le proprie operazioni come monoide in Scalding.