なぜScalaと、SparkとScaldingのようなフレームワークにはreduce
とfoldLeft
の両方があるのでしょうか?では、reduce
およびfold
?
このトピックに関連する他のスタックオーバーフローの答えで明確に言及されていない大きな大きな違いは、reduce
に可換モノイド、つまり可換および結合の両方の操作を与える必要があることですこれは、操作を並列化できることを意味します。
この区別は、ビッグデータ/ MPP /分散コンピューティング、およびreduce
が存在する理由全体にとって非常に重要です。コレクションを切り刻み、reduce
を各チャンクで操作し、reduce
で各チャンクの結果を操作できます。実際、チャンキングのレベルは1レベル深くする必要はありません。各チャンクも切り刻むことができます。これが、無限数のCPUが与えられた場合、リスト内の整数の合計がO(log N)である理由です。
署名だけを見ると、reduce
が存在する理由はありません。なぜなら、reduce
を使用してfoldLeft
でできることはすべて達成できるからです。 foldLeft
の機能は、reduce
の機能よりも優れています。
ただし、foldLeft
を並列化することはできません。そのため、ランタイムは常にO(N)です(可換モノイドを入力した場合でも)。これは、操作がnot可換モノイドであると想定されているため、累積値は一連の連続的な集計によって計算されるためです。
foldLeft
は、可換性も結合性も想定していません。コレクションを切り刻む機能を提供するのは結合性であり、順序は重要ではないため、累積は簡単になります(したがって、各チャンクからの各結果を集約する順序は関係ありません)。厳密に言えば、分散ソートアルゴリズムなどの並列化には、厳密に言えば可換性は必要ありません。チャンクに順序を付ける必要がないため、ロジックが簡単になります。
reduce
のSparkドキュメントを見ると、具体的には「...可換および連想バイナリ演算子」と書かれています。
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 /数学的なルートに少し近づき、説明するのが少し難しい場所です。 Reduceは、順序のないコレクション(マルチセット)を扱うMapReduceパラダイムの一部として正式に定義されています。
Scaldingにはfold
メソッドがありません。(厳密な)Map Reduceプログラミングモデルでは、fold
を定義できないためです。チャンクには順序がなく、fold
には可換性ではなく結合性のみが必要です。
簡単に言うと、reduce
は累積の順序なしで機能します。fold
は累積の順序を必要とし、ゼロの値を必要とするのは累積の順序です。厳密に言えばreduce
shouldは空のコレクションで動作します。これは、任意の値x
を取得してからx op y = x
を解くことでそのゼロ値を推測できるためです別個の左右のゼロ値が存在する可能性があるため、非可換演算を使用します(つまり、x op y != y op x
)。もちろん、Scalaは、このゼロ値が何であるかを計算することに煩わされないため、数学(おそらく計算できない)を行う必要があるため、例外をスローするだけです。
プログラミングの唯一の明白な違いは署名であるため、この元の数学的意味は失われているようです(語源の場合によくあることです)。その結果、reduce
はMapReduceの本来の意味を保持するのではなく、fold
の同義語になりました。現在、これらの用語は互換的に使用されることが多く、ほとんどの実装で同じように動作します(空のコレクションを無視します)。奇妙さは、Sparkのような特異性によって悪化します。
Sparkdoesにはfold
がありますが、サブ結果(各パーティションに1つ)が結合される順序(執筆時点)は、同じ順序ですどのタスクが完了するか-したがって、非決定的です。 fold
がrunJob
を使用していることを指摘してくれた@CafeFeedに感謝します。これは、コードを読んだ後、非決定的であることに気付きました。 SparkがtreeReduce
を持ち、treeFold
を持たないことにより、さらに混乱が生じます。
空でないシーケンスに適用される場合でも、reduce
とfold
には違いがあります。前者は、任意の順序( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf )のコレクションに関するMapReduceプログラミングパラダイムの一部として定義されており、演算子は、決定論的な結果を得るために結合的であることに加えて、可換的です。後者はカトモルフィズムの観点から定義されており、コレクションにシーケンスの概念がある(またはリンクリストのように再帰的に定義される)必要があるため、可換演算子は必要ありません。
実際には、プログラミングの非数学的な性質のため、reduce
とfold
は、正しく(Scalaのように)または間違って(Sparkのように)同じように動作する傾向があります。
私の意見では、用語fold
の使用がSparkで完全に削除された場合、混乱は回避されると考えられます。少なくともsparkのドキュメントにはメモがあります。
これは、Scalaのような関数型言語の非分散コレクションに実装されたフォールド操作とは多少異なる動作をします。
間違っていなければ、Spark APIはそれを必要としませんが、foldは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
Scaldingのもう1つの違いは、Hadoopでのコンバイナーの使用です。
あなたの操作がreduceで可換モノイドであり、すべてのデータをリデューサーにシャッフル/ソートする代わりにマップ側にも適用されると想像してください。 foldLeftの場合、これは当てはまりません。
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 }
}
Scaldingでは、操作をモノイドとして定義することを常にお勧めします。
Apacheのfold
Sparkは、非分散コレクションのfold
と同じではありません。実際には 交換関数が必要です を生成します決定的な結果:
これは、Scalaのような関数型言語の非分散コレクションに実装されたフォールド操作とは多少異なる動作をします。この折り畳み操作はパーティションに個別に適用でき、定義された順序で各要素に連続して折り畳みを適用するのではなく、それらの結果を最終結果に折り畳むことができます。可換でない関数の場合、結果は、非分散コレクションに適用されるフォールドの結果と異なる場合があります。
これは 表示されています によって Mishael Rosenthal によって提案され、 Make42 in his comment で示されています。
推奨されています 実際に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
on TraversableOnce
。
結論:RDDのfold
は、チャンクの順序とニーズの可換性と結合性に依存できません。