Іскрові показники для Scala vs Python


178

Я віддаю перевагу Python над Scala. Але, оскільки Spark споконвічно написаний у Scala, я очікував, що мій код запуститься швидше у Scala, ніж версія Python з зрозумілих причин.

З цим припущенням я думав вивчити та написати версію Scala деякого дуже поширеного коду попередньої обробки для приблизно 1 ГБ даних. Дані зібрані з змагань SpringLeaf на Kaggle . Просто для огляду даних (він містить 1936 розмірів і 145232 рядків). Дані складаються з різних типів, наприклад, int, float, string, boolean. Я використовую 6 ядер із 8 для обробки іскри; тому я використовував minPartitions=6так, що кожне ядро ​​має щось обробити.

Код Скали

val input = sc.textFile("train.csv", minPartitions=6)

val input2 = input.mapPartitionsWithIndex { (idx, iter) => 
  if (idx == 0) iter.drop(1) else iter }
val delim1 = "\001"

def separateCols(line: String): Array[String] = {
  val line2 = line.replaceAll("true", "1")
  val line3 = line2.replaceAll("false", "0")
  val vals: Array[String] = line3.split(",")

  for((x,i) <- vals.view.zipWithIndex) {
    vals(i) = "VAR_%04d".format(i) + delim1 + x
  }
  vals
}

val input3 = input2.flatMap(separateCols)

def toKeyVal(line: String): (String, String) = {
  val vals = line.split(delim1)
  (vals(0), vals(1))
}

val input4 = input3.map(toKeyVal)

def valsConcat(val1: String, val2: String): String = {
  val1 + "," + val2
}

val input5 = input4.reduceByKey(valsConcat)

input5.saveAsTextFile("output")

Код Пітона

input = sc.textFile('train.csv', minPartitions=6)
DELIM_1 = '\001'


def drop_first_line(index, itr):
  if index == 0:
    return iter(list(itr)[1:])
  else:
    return itr

input2 = input.mapPartitionsWithIndex(drop_first_line)

def separate_cols(line):
  line = line.replace('true', '1').replace('false', '0')
  vals = line.split(',')
  vals2 = ['VAR_%04d%s%s' %(e, DELIM_1, val.strip('\"'))
           for e, val in enumerate(vals)]
  return vals2


input3 = input2.flatMap(separate_cols)

def to_key_val(kv):
  key, val = kv.split(DELIM_1)
  return (key, val)
input4 = input3.map(to_key_val)

def vals_concat(v1, v2):
  return v1 + ',' + v2

input5 = input4.reduceByKey(vals_concat)
input5.saveAsTextFile('output')

Ефективність масштабування Scala 0 (38 хв), етап 1 (18 сек) введіть тут опис зображення

Ефективність Python, етап 0 (11 хв), етап 1 (7 сек) введіть тут опис зображення

Обидва створюють різні графіки візуалізації DAG (завдяки яким обидва зображення показують різні функції на етапі 0 для Scala ( map) та Python ( reduceByKey))

Але, по суті, обидва коди намагаються перетворити дані в (Dimen_id, рядок списку значень) RDD та зберегти на диску. Вихід буде використаний для обчислення різних статистичних даних для кожного виміру.

Ефективність, код Scala для таких реальних даних, схоже, працює в 4 рази повільніше, ніж версія Python. Гарна новина для мене полягає в тому, що це дало мені гарну мотивацію залишитися з Python. Погана новина: я не зовсім зрозумів, чому?


8
Можливо, це залежить від коду та програми, оскільки я отримую інший результат, що apache іскровий пітон повільніше, ніж шкала, коли підсумовуємо мільярд термінів формули Лейбніца для π
Павло

3
Цікаве запитання! Btw, подивіться також тут: emptypipes.org/2015/01/17/python-vs-scala-vs-spark Чим більше ядер у вас є, тим менше ви можете бачити відмінності між мовами.
Маркон

Чи розглядали ви, як прийняти існуючу відповідь?
10465355 повідомляє Відновити Моніку

Відповіді:


358

Оригінальну відповідь, що обговорює код, можна знайти нижче.


Перш за все, ви повинні розрізняти різні типи API, кожен з яких має власні міркування щодо продуктивності.

RDD API

(чисті структури Python з оркестрованістю на базі JVM)

Це компонент, на який найбільше вплине ефективність коду Python та деталі реалізації PySpark. Хоча ефективність Python навряд чи буде проблемою, є хоча б мало факторів, які слід врахувати:

  • Накладні витрати JVM-зв'язку. Практично всі дані, які надходять до виконавця Python і повинні передаватися через сокет і працівника JVM. Хоча це відносно ефективне місцеве спілкування, воно все ще не є безкоштовним.
  • Виконавці на основі процесів (Python) проти потоків (одиночні множинні JVM) виконавці (Scala). Кожен виконавець Python працює у своєму процесі. Як побічний ефект, він забезпечує більш сильну ізоляцію, ніж його аналог JVM, і деякий контроль над життєвим циклом виконавця, але потенційно значно більший об'єм пам'яті:

    • слід пам'яті перекладача
    • слід завантажених бібліотек
    • менш ефективне мовлення (кожен процес вимагає власної копії трансляції)
  • Виконання самого коду Python. Взагалі Scala швидше, ніж Python, але вона буде змінюватися залежно від завдання. Крім того, у вас є кілька варіантів, включаючи JIT, такі як Numba , розширення C ( Cython ) або спеціалізовані бібліотеки на зразок Theano . Нарешті, якщо ви не використовуєте ML / MLlib (або просто NumPy стек) , подумайте про використання PyPy в якості альтернативного інтерпретатора. Див. SPARK-3094 .

  • Конфігурація PySpark забезпечує spark.python.worker.reuseопцію, яку можна використовувати для вибору між розгортанням процесу Python для кожної задачі та повторним використанням існуючого процесу. Останній варіант здається корисним, щоб уникнути дорогого вивезення сміття (це більше враження, ніж результату систематичних тестів), тоді як перший варіант (за замовчуванням) є оптимальним у випадку дорогих трансляцій та імпорту.
  • Підрахунок посилань, який використовується як метод збору сміття першого рядка в CPython, добре працює з типовими робочими навантаженнями Spark (потокова обробка, відсутність еталонних циклів) і зменшує ризик тривалих пауз GC.

MLlib

(змішане виконання Python та JVM)

Основні міркування майже такі ж, як і раніше, з кількома додатковими питаннями. Хоча основні структури, що використовуються з MLlib, є простими об'єктами RDD Python, всі алгоритми виконуються безпосередньо за допомогою Scala.

Це означає додаткові витрати на перетворення об’єктів Python в об’єкти Scala та навпаки, збільшення використання пам'яті та деякі додаткові обмеження, які ми покриємо пізніше.

На даний момент (Spark 2.x) API на основі RDD знаходиться в режимі обслуговування та планується видалити в Spark 3.0 .

API DataFrame та Spark ML

(Виконання JVM з кодом Python обмежено драйвером)

Це, мабуть, найкращий вибір для стандартних завдань з обробки даних. Оскільки код Python здебільшого обмежений логічними операціями високого рівня на драйвері, між Python та Scala не повинно бути різниці в продуктивності.

Єдиним винятком є ​​використання строкових UDF-файлів Python, які значно менш ефективні, ніж їх еквіваленти Scala. Незважаючи на те, що є певні шанси на вдосконалення (істотний розвиток у Spark 2.0.0), найбільше обмеження - це повний перехід між внутрішнім представництвом (JVM) та інтерпретатором Python. Якщо можливо, вам слід віддати перевагу композиції вбудованих виразів ( наприклад . Поведінка Python UDF була покращена в Spark 2.0.0, але вона все ще є неоптимальною порівняно з нативним виконанням.

Це може покращитись у майбутньому, значно покращившись із впровадженням векторизованих UDF (SPARK-21190 та подальші розширення) , який використовує Arrow Streaming для ефективного обміну даними з десеріалізацією нульової копії. Для більшості програм їх вторинні накладні витрати можуть бути просто проігноровані.

Також не забудьте уникнути зайвих передач даних між DataFramesта RDDs. Для цього потрібна дорога серіалізація та десеріалізація, не кажучи вже про передачу даних до і з інтерпретатора Python.

Варто зазначити, що виклики Py4J мають досить високу затримку. Сюди входять прості дзвінки, такі як:

from pyspark.sql.functions import col

col("foo")

Зазвичай це не має значення (накладні витрати постійні і не залежать від кількості даних), але у випадку з м'якими програмами в реальному часі ви можете розглянути можливість кешування / повторного використання обгортки Java.

Набори даних GraphX ​​та Spark

Як і зараз (Spark 1.6 2.1) жоден не надає API PySpark, тому можна сказати, що PySpark нескінченно гірший, ніж Scala.

GraphX

На практиці розробка GraphX ​​майже повністю припинилася, і проект наразі перебуває в режимі технічного обслуговування, а відповідні квитки JIRA закриті, оскільки не виправлять . GraphFramesБібліотека надає альтернативну бібліотеку обробки графіків із прив'язкою Python.

Набір даних

Суб'єктивно кажучи, не існує багато місця для статичного набору Datasetsв Python, і навіть якщо поточна реалізація Scala надто спрощена і не дає тих же переваг продуктивності, що й DataFrame.

Потокове

З того, що я бачив до цього часу, я настійно рекомендую використовувати Scala над Python. Це може змінитися в майбутньому, якщо PySpark отримає підтримку структурованих потоків, але зараз API Scala здається набагато більш надійним, всеосяжним та ефективним. Мій досвід досить обмежений.

Структурована трансляція в Spark 2.x, здається, зменшує розрив між мовами, але поки що це ще в перші дні. Тим не менш, API на основі RDD вже посилається на "застаріле потокове" в Документації Databricks (дата доступу 2017-03-03), тому розумно очікувати подальших зусиль щодо об'єднання.

Невиконання міркувань

Паритет функції

Не всі функції іскри відкриваються через PySpark API. Не забудьте перевірити, чи потрібні вам частини вже реалізовані, і спробуйте зрозуміти можливі обмеження.

Це особливо важливо, коли ви використовуєте MLlib та подібні змішані контексти (див. Виклик функції Java / Scala із завдання ). Для справедливості деякі частини API PySpark, наприклад mllib.linalg, надають більш вичерпний набір методів, ніж Scala.

Дизайн API

API PySpark уважно відображає його аналог Scala і як такий не є точно Pythonic. Це означає, що карту між мовами досить легко, але в той же час зрозуміти код Python може бути значно важче.

Складна архітектура

Потік даних PySpark порівняно складний у порівнянні з чистим виконанням JVM. Набагато складніше міркувати про програми PySpark або налагодження. Більше того, принаймні базове розуміння Scala та JVM взагалі, як правило, необхідно мати.

Іскра 2.x і більше

Постійний перехід до DatasetAPI, заморожений API RDD приносить і можливості, і проблеми для користувачів Python. Хоча частини API високого рівня набагато простіше експонувати в Python, проте більш досконалі функції майже неможливо використовувати безпосередньо .

Більше того, нативні функції Python продовжують залишатися громадянами другого класу у світі SQL. Сподіваємось, це в майбутньому покращиться з серіалізацією Apache Arrow ( цільові дані про поточні зусилля,collection але serde UDF - це довгострокова мета ).

Для проектів, сильно залежних від кодової бази Python, чисті альтернативи Python (як Dask або Ray ) можуть бути цікавою альтернативою.

Це не повинно бути один проти іншого

API Spark DataFrame (SQL, набір даних) забезпечує елегантний спосіб інтеграції коду Scala / Java в додаток PySpark. Ви можете використовувати DataFramesдля викриття даних до нативного коду JVM та для читання результатів. Я пояснив деякі варіанти десь в іншому місці, і ви можете знайти робочий приклад зворотного переходу Python-Scala в розділі Як використовувати клас Scala всередині Pyspark .

Його можна додатково доповнити, ввівши визначені користувачем типи (див. Як визначити схему для користувацького типу в Spark SQL? ).


Що не так з кодом, наведеним у питанні

(Відмова: Точка зору Pythonista. Швидше за все, я пропустив деякі хитрощі Scala)

Перш за все, у вашому коді є одна частина, яка взагалі не має сенсу. Якщо у вас вже є (key, value)пари, створені за допомогою, zipWithIndexабо enumerateякий сенс у створенні рядка, щоб розділити його відразу після цього? flatMapне працює рекурсивно, так що ви можете просто отримати кортежі та пропустити наступні дії map.

Інша частина, яку я вважаю проблематичною, - це reduceByKey. Взагалі кажучи, reduceByKeyкорисно, якщо застосування сукупної функції може зменшити кількість даних, які підлягають переміщенню. Оскільки ви просто з'єднуєте рядки, тут нічого не виграти. Ігноруючи матеріали низького рівня, як кількість посилань, обсяг даних, який ви повинні передати, точно такий же, як і для groupByKey.

Зазвичай я б на цьому не зупинявся, але, наскільки я можу сказати, це вузьке місце у вашому коді Scala. Об'єднання рядків у JVM - досить дорога операція (див. Наприклад: Чи конкатенація рядків у шкалі така ж затратна, як і у Java? ). Це означає, що щось подібне, _.reduceByKey((v1: String, v2: String) => v1 + ',' + v2) що еквівалентно input4.reduceByKey(valsConcat)вашому коду, не є хорошою ідеєю.

Якщо ви хочете уникнути , groupByKeyви можете спробувати використовувати aggregateByKeyз StringBuilder. Щось подібне до цього має зробити трюк:

rdd.aggregateByKey(new StringBuilder)(
  (acc, e) => {
    if(!acc.isEmpty) acc.append(",").append(e)
    else acc.append(e)
  },
  (acc1, acc2) => {
    if(acc1.isEmpty | acc2.isEmpty)  acc1.addString(acc2)
    else acc1.append(",").addString(acc2)
  }
)

але я сумніваюсь, що це вартує суєти.

Маючи на увазі вищесказане, я переписав ваш код наступним чином:

Scala :

val input = sc.textFile("train.csv", 6).mapPartitionsWithIndex{
  (idx, iter) => if (idx == 0) iter.drop(1) else iter
}

val pairs = input.flatMap(line => line.split(",").zipWithIndex.map{
  case ("true", i) => (i, "1")
  case ("false", i) => (i, "0")
  case p => p.swap
})

val result = pairs.groupByKey.map{
  case (k, vals) =>  {
    val valsString = vals.mkString(",")
    s"$k,$valsString"
  }
}

result.saveAsTextFile("scalaout")

Пітон :

def drop_first_line(index, itr):
    if index == 0:
        return iter(list(itr)[1:])
    else:
        return itr

def separate_cols(line):
    line = line.replace('true', '1').replace('false', '0')
    vals = line.split(',')
    for (i, x) in enumerate(vals):
        yield (i, x)

input = (sc
    .textFile('train.csv', minPartitions=6)
    .mapPartitionsWithIndex(drop_first_line))

pairs = input.flatMap(separate_cols)

result = (pairs
    .groupByKey()
    .map(lambda kv: "{0},{1}".format(kv[0], ",".join(kv[1]))))

result.saveAsTextFile("pythonout")

Результати

У local[6]режимі (Intel (R) Xeon (R) процесор E3-1245 V2 @ 3,40 ГГц) з 4 Гб пам'яті на одного виконавця потрібно (n = 3):

  • Scala - означає: 250,00с, стдев: 12,49
  • Python - означає: 246,66s, stdev: 1,15

Я майже впевнений, що більшість цього часу витрачається на перемішування, серіалізацію, десеріалізацію та інші вторинні завдання. Для задоволення, ось наївний однопотоковий код в Python, який виконує те саме завдання на цій машині менше ніж за хвилину:

def go():
    with open("train.csv") as fr:
        lines = [
            line.replace('true', '1').replace('false', '0').split(",")
            for line in fr]
    return zip(*lines[1:])

23
Один з найбільш чітких, вичерпних і корисних відповідей, з якими я стикався певний час. Дякую!
етов

Який ти чудовий хлопець!
DennisLi

-4

Розширення до вищезазначених відповідей -

Scala виявляється швидше, багато в чому порівняно з python, але є кілька поважних причин, чому python стає все більш популярним, ніж scala, давайте подивимося декілька з них -

Python for Apache Spark досить простий у вивченні та використанні. Однак це не єдина причина, чому Pyspark - кращий вибір, ніж Скала. Є ще більше.

API Python for Spark може бути повільнішим на кластері, але, зрештою, вчені з даними можуть зробити набагато більше з ним порівняно зі Scala. Складність Scala відсутня. Інтерфейс простий і всеосяжний.

Якщо говорити про читаність коду, технічне обслуговування та знайомство з API Python для Apache Spark, це набагато краще, ніж Scala.

Python оснащений кількома бібліотеками, пов'язаними з машинним навчанням та обробкою природних мов. Це допомагає в аналізі даних, а також має статистику, яка є зрілою та перевірена часом. Наприклад, numpy, панди, scikit-learn, seaborn та matplotlib.

Примітка: Більшість науковців даних використовують гібридний підхід, коли вони використовують найкращі з обох API.

Нарешті, спільнота Scala часто виявляється набагато менш корисною для програмістів. Це робить Python набагато ціннішим навчанням. Якщо у вас є достатній досвід роботи з будь-якою статичною типовою мовою програмування, як Java, ви можете перестати турбуватися про те, щоб не використовувати Scala взагалі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.