Чому Scala та такі фреймворки, як Spark та Scalding, мають і те, reduce
і інше foldLeft
? Тоді в чому різниця між reduce
і fold
?
Чому Scala та такі фреймворки, як Spark та Scalding, мають і те, reduce
і інше foldLeft
? Тоді в чому різниця між reduce
і fold
?
Відповіді:
Велика різниця, яка не згадується в жодній іншій відповіді стак-потоку, що стосується цієї теми, полягає в тому, що reduce
слід надати комунативний моноїд , тобто операцію, яка є одночасно комутативною та асоціативною. Це означає, що операцію можна розпаралелювати.
Ця відмінність дуже важлива для обробки великих даних / MPP / розподілених обчислень, і вся причина, чому вона reduce
існує. Колекцію можна подрібнити, а reduce
баночка може оперувати на кожному шматку, тоді вона reduce
може діяти за результатами кожного шматка - адже рівень збивання не повинен зупинятися на рівні глибоко. Ми могли б також подрібнити кожен шматок. Ось чому підсумовування цілих чисел у списку дорівнює O (журнал N), якщо дано нескінченну кількість ЦП.
Якщо ви просто подивитеся на підписи, немає ніяких причин reduce
існувати, тому що ви можете досягти всього, що можете, з reduce
допомогою foldLeft
. Функціональність компанії foldLeft
більше, ніж функціональність reduce
.
Але ви не можете паралелізувати a foldLeft
, тому час його виконання завжди O (N) (навіть якщо ви подаєте в комутативний моноїд). Це пов’язано з тим, що передбачається, що операція не є комутативним моноїдом, і тому сукупне значення буде обчислюватися низкою послідовних агрегувань.
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 формально визначається з точки зору рекурсії (див. Катаморфізм) і, таким чином, передбачає структуру / послідовність для колекцій.
Немає fold
методу в Scalding, тому що в рамках (суворої) моделі програмування Map Reduce ми не можемо визначити, fold
оскільки фрагменти не мають впорядкування і fold
вимагають лише асоціативності, а не комутативності.
Простіше кажучи, reduce
працює без порядку кумуляції, fold
вимагає порядку кумуляції, і саме такий порядок кумуляції потребує нульового значення НЕ існування нульового значення, яке їх відрізняє. Власне кажучи, reduce
слід працювати над порожньою колекцією, оскільки її нульове значення можна визначити, взявши довільне значення, x
а потім вирішити x op y = x
, але це не працює з некомутативною операцією, оскільки може існувати ліве і праве нульове значення, які є чіткими (тобто x op y != y op x
). Звичайно, Скала не турбується, щоб зрозуміти, що це нульове значення, оскільки для цього потрібно буде робити якусь математику (яка, мабуть, незрозуміла), тому просто створює виняток.
Здається, (як це часто буває в етимології), це початкове математичне значення було втрачено, оскільки єдиною очевидною різницею в програмуванні є підпис. Як результат, це reduce
стало синонімом fold
, а не зберегти його початкове значення від MapReduce. Зараз ці терміни часто використовуються як взаємозамінні і поводяться однаково в більшості реалізацій (ігноруючи порожні колекції). Дивність посилюється особливостями, як у Spark, про які ми зараз звернемось.
Отже, Spark дійсно має a fold
, але порядок об'єднання підрезультатів (по одному для кожного розділу) (на момент написання статті) є таким самим порядком, в якому виконуються завдання - і, отже, недетермінованим. Дякую @CafeFeed за вказівку на те, що fold
використовує runJob
, який, прочитавши код, я зрозумів, що він не детермінований. Подальша плутанина створюється Спарком, treeReduce
але його немає treeFold
.
Існує різниця між reduce
і fold
навіть у застосуванні до непорожніх послідовностей. Перший визначається як частина парадигми програмування MapReduce у колекціях з довільним порядком ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf ), і слід вважати, що оператори є комутаційними на додаток до того, що вони є асоціативний для отримання детермінованих результатів. Останній визначається з точки зору катоморфізмів і вимагає, щоб колекції мали поняття послідовності (або визначалися рекурсивно, як зв’язані списки), тому не потребують комутативних операторів.
На практиці через не математичну природу програмування, 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.
Якщо я не помиляюся, навіть якщо API Spark цього не вимагає, складка також вимагає, щоб 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("")(_ + _))
}
Надрукувати:
а Б В Г Г Д Е Є Ж З И І Ї Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ ью я
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. Цю операцію згинання можна застосувати до розділів окремо, а потім скласти ці результати до кінцевого результату, а не застосовувати згинання до кожного елемента послідовно в певному визначеному порядку. Для функцій, які не є комутативними, результат може відрізнятися від результатів складок, застосованих до нерозподіленої колекції.
Це було показано на Mishael Rosenthal і запропонував 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 не може залежати від порядку відрізків і потребує комутативності та асоціативності .
fold
на RDD
s дійсно так само, як reduce
, але це не поважає кореневих математичних відмінностей (я оновив свою відповідь, щоб бути ще більш зрозумілою). Хоча я не згоден з тим, що нам дійсно потрібна комутативність за умови, що хтось впевнений, що б не робив їхній партьєр, він зберігає порядок.
runJob
хотіли сказати, але, прочитавши код, я бачу, що він справді виконує комбінування відповідно до того, коли завдання закінчено, А НЕ порядок розділів. Саме ця ключова деталь змушує все вставати на свої місця. Я відредагував мій відповідь знову і , таким чином , виправив помилку ви відзначаєте. Будь ласка, чи можете ви зняти свою суму, оскільки ми зараз домовляємось?
Ще одна відмінність для Scalding - використання комбайнерів у Hadoop.
Уявіть, ваша операція є комутативним моноїдом, з функцією зменшення вона буде застосована на стороні карти також замість перемішування / сортування всіх даних до редукторів. З 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 }
}
Завжди корисною визначити ваші операції як моноїд у «Скальдінгу».