Іскра: Чому Python значно перевершує Scala в моєму випадку використання?


16

Для порівняння продуктивності Spark при використанні Python та Scala я створив однакову роботу на обох мовах та порівняв час виконання. Я очікував, що обидві роботи займуть приблизно стільки ж часу, але робота в Python зайняла лише 27minтой час, коли робота Scala зайняла 37min(майже на 40% більше!). Я реалізував ту саму роботу і в Java, і це 37minutesтеж зайняло . Як це можливо, що Python так швидше?

Мінімальний приклад, який можна перевірити:

Робота Python:

# Configuration
conf = pyspark.SparkConf()
conf.set("spark.hadoop.fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider")
conf.set("spark.executor.instances", "4")
conf.set("spark.executor.cores", "8")
sc = pyspark.SparkContext(conf=conf)

# 960 Files from a public dataset in 2 batches
input_files = "s3a://commoncrawl/crawl-data/CC-MAIN-2019-35/segments/1566027312025.20/warc/CC-MAIN-20190817203056-20190817225056-00[0-5]*"
input_files2 = "s3a://commoncrawl/crawl-data/CC-MAIN-2019-35/segments/1566027312128.3/warc/CC-MAIN-20190817102624-20190817124624-00[0-3]*"

# Count occurances of a certain string
logData = sc.textFile(input_files)
logData2 = sc.textFile(input_files2)
a = logData.filter(lambda value: value.startswith('WARC-Type: response')).count()
b = logData2.filter(lambda value: value.startswith('WARC-Type: response')).count()

print(a, b)

Робота Scala:

// Configuration
config.set("spark.executor.instances", "4")
config.set("spark.executor.cores", "8")
val sc = new SparkContext(config)
sc.setLogLevel("WARN")
sc.hadoopConfiguration.set("fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider")

// 960 Files from a public dataset in 2 batches 
val input_files = "s3a://commoncrawl/crawl-data/CC-MAIN-2019-35/segments/1566027312025.20/warc/CC-MAIN-20190817203056-20190817225056-00[0-5]*"
val input_files2 = "s3a://commoncrawl/crawl-data/CC-MAIN-2019-35/segments/1566027312128.3/warc/CC-MAIN-20190817102624-20190817124624-00[0-3]*"

// Count occurances of a certain string
val logData1 = sc.textFile(input_files)
val logData2 = sc.textFile(input_files2)
val num1 = logData1.filter(line => line.startsWith("WARC-Type: response")).count()
val num2 = logData2.filter(line => line.startsWith("WARC-Type: response")).count()

println(s"Lines with a: $num1, Lines with b: $num2")

Тільки дивлячись на код, вони здаються ідентичними. Я подивився DAG, і вони не дали ніякої інформації (або, принаймні, мені не вистачає ноу-хау, щоб придумати пояснення на їх основі).

Я б дуже вдячний за будь-які покажчики.


Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

1
Я би розпочав аналіз, перш ніж щось запитувати, тимчасом відповідних блоків та висловлювань, щоб побачити, чи існувало певне місце, де версія python швидша. Тоді вам, можливо, вдалося б загострити питання до того, "чому цей оператор python швидше".
Terry Jan Reedy

Відповіді:


11

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

import scala.io.Source
import java.time.{Duration, Instant}

object App {
  def main(args: Array[String]) {
    val Array(filename, string) = args

    val start = Instant.now()

    Source
      .fromFile(filename)
      .getLines
      .filter(line => line.startsWith(string))
      .length

    val stop = Instant.now()
    val duration = Duration.between(start, stop).toMillis
    println(s"${start},${stop},${duration}")
  }
}

Пітон один

import datetime
import sys

if __name__ == "__main__":
    _, filename, string = sys.argv
    start = datetime.datetime.now()
    with open(filename) as fr:
        # Not idiomatic or the most efficient but that's what
        # PySpark will use
        sum(1 for _ in filter(lambda line: line.startswith(string), fr))

    end = datetime.datetime.now()
    duration = round((end - start).total_seconds() * 1000)
    print(f"{start},{end},{duration}")

Результати (300 повторень кожен, Python 3.7.6, Scala 2.11.12), на Posts.xmlз hermeneutics.stackexchange.com даних дамп з поєднанням Відповідності і невідповідності моделей відповідності:

тривалості витримки в мілісах для вищезгаданих програм

  • Python 273.50 (258.84, 288.16)
  • Scala 634.13 (533.81, 734.45)

Як ви бачите, Python не тільки систематично швидший, але і є більш послідовним (нижчий розкид).

Повідомлення “Take away” - не вірте необгрунтованому FUD - мови можуть бути швидшими або повільнішими у конкретних завданнях або в конкретних середовищах (наприклад, тут Scala може потрапити під час запуску JVM та / або GC та / або JIT), але якщо ви заявляєте наприклад "XYZ швидше X4" або "XYZ повільний порівняно з ZYX (..) Приблизно, у 10 разів повільніше", це означає, що хтось написав дійсно поганий код для перевірки речей.

Редагувати :

Щоб вирішити деякі проблеми, висловлені в коментарях:

  • У коді OP дані передаються здебільшого в одному напрямку (JVM -> Python), і реальна серіалізація не потрібна (цей специфічний шлях просто проходить шляхом випробування як є і декодує на UTF-8 з іншого боку). Це так само дешево, як і коли йдеться про "серіалізацію".
  • Те, що передається назад, - це лише одне ціле число за розділом, тому в цьому напрямку вплив незначний.
  • Зв'язок здійснюється через локальні сокети (вся комунікація на робочому місці після початкового підключення та автентифікації виконується за допомогою дескриптора файлів, повернутого з local_connect_and_auth, і нічого іншого, ніж асоційований файл з сокетом ). Знову ж настільки ж дешевим, як це стосується спілкування між процесами.
  • Враховуючи різницю в сировинних показниках, показаних вище (набагато вище, ніж ви бачите в програмі), велика націнка на накладні витрати, перелічені вище.
  • Цей випадок зовсім відрізняється від випадків, коли або прості, або складні об'єкти потрібно передавати в інтерпретатор і Python у формі, доступній обом сторонам у вигляді сумісних з маринованими відвалами (найбільш помітні приклади включають старий стиль UDF, деякі частини старої -стиль MLLib).

Редагувати 2 :

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

Ось результати для 2003360 рядків / 5.6G (той самий вхід, просто дублюється кілька разів, 30 повторень), що перевищує все, що ви можете очікувати в одному завданні Spark.

введіть тут опис зображення

  • Python 22809.57 (21466.26, 24152.87)
  • Scala 27315,28 (24367,24, 30263,31)

Зверніть увагу, що довірчі інтервали не збігаються.

Редагувати 3 :

Адресувати ще один коментар від Jasper-M:

Основна частина всієї обробки все ще відбувається всередині JVM у випадку Spark.

Це просто неправильно в даному конкретному випадку:

  • Задача, про яку йдеться, - це завдання на карті з єдиним глобальним зменшенням за допомогою RDS PySpark.
  • PySpark RDD (на відміну від скажімо DataFrame ) реалізує функціональних можливостей в Python, за винятком введення, виводу та зв'язку між вузлами.
  • Оскільки це одноетапне завдання, а кінцевий висновок є досить малим, щоб його ігнорувати, головна відповідальність JVM (якщо хтось повинен був запускати нитку, це реалізується здебільшого на Java, а не Scala) - викликати формат введення Hadoop та просувати дані через сокет файл у Python.
  • Прочитана частина є однаковою для JVM та Python API, тому її можна розглядати як постійні накладні витрати. Він також не вважається основною частиною обробки навіть для такої простої роботи, як ця.

3
відмінний підхід до проблеми. Дякую за те, що поділилися цим
Олександрос Біраціс,

1
@egordoe Александрос сказав, що "тут немає посилань на АДС", а не про те, що "Python не викликається" - це має значення. Накладні витрати на серіалізацію важливі, коли дані обмінюються між системами (тобто, коли ви хочете передати дані в UDF і назад).
користувач10938362

1
@egordoe Ви чітко плутаєте дві речі - накладні витрати на серіалізацію, що є проблемою, коли нетривіальні об’єкти передаються туди-сюди. І накладні комунікації. Тут серіалізації мало або взагалі немає, тому що ви просто проходите і декодуєте бітестринги, і це відбувається в основному в напрямку, оскільки назад ви отримуєте одне ціле число на розділ. Комунікація викликає певне занепокоєння, але передача даних через локальні розетки є ефективною, оскільки вона дійсно отримує, коли мова йде про міжпроцесорну комунікацію. Якщо це не ясно, рекомендую прочитати джерело - це не важко і буде освічуючим.
користувач10938362

1
Крім того, методи серіалізації просто не зрівняються. Як показує випадок Spark, хороші методи серіалізації можуть знизити вартість до рівня, коли це вже не хвилює (див. Пандас АДС із стрілкою), а коли це станеться, інші фактори можуть домінувати (див. Наприклад порівняння продуктивності між віконними функціями Scala та їх еквівалентами з Pandas UDF - Python виграє набагато більший запас, ніж у цьому питанні).
користувач10938362

1
А ваша думка - @ Jasper-M? Індивідуальні завдання Spark, як правило, досить малі, щоб мати порівнянне навантаження з цим. Не сприймайте мене неправильно, але якщо у вас є фактичний контрприклад, який визнає недійсним це чи все питання, будь ласка, опублікуйте його. Я вже зазначав, що вторинні дії певною мірою сприяють цій цінності, але вони не домінують у вартості. Ми всі тут інженери (якісь) - давайте поговоримо про цифри та код, а не про переконання, чи не так?
користувач10938362

4

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

У коді є дві помилки:

val sc = new SparkContext(config) // LINE #1
sc.setLogLevel("WARN")
sc.hadoopConfiguration.set("fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider")
sc.hadoopConfiguration.set("spark.executor.instances", "4") // LINE #4
sc.hadoopConfiguration.set("spark.executor.cores", "8") // LINE #5
  1. Рядок 1. Після того, як лінія виконана, конфігурація ресурсів завдання Spark вже встановлена ​​та виправлена. З цього моменту не можна нічого коригувати. Ні кількість виконавців, ні кількість ядер на одного виконавця.
  2. РЯДКА 4-5. sc.hadoopConfigurationнеправильне місце для встановлення будь-якої конфігурації Spark. Він повинен бути встановлений у configвипадку, до якого ви переходите new SparkContext(config).

[ДОДАТИ] Маючи на увазі вищесказане, я б запропонував змінити код завдання Scala на

config.set("spark.executor.instances", "4")
config.set("spark.executor.cores", "8")
val sc = new SparkContext(config) // LINE #1
sc.setLogLevel("WARN")
sc.hadoopConfiguration.set("fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider")

і ще раз перевірити його. Надіюсь, версія Scala зараз стане в X рази швидшою.


Я перевірив, що обидві роботи паралельно виконують 32 завдання, тому я не думаю, що це є винуватцем?
maestromusica

дякую за редагування, спробую перевірити це зараз
maestromusica

Привіт @maestromusica це має бути щось у конфігурації ресурсу, оскільки, власне, Python може не перевершити Scala в цьому конкретному випадку використання. Іншою причиною можуть бути деякі некорельовані випадкові фактори, тобто навантаження кластеру в конкретний момент і подібні. Btw, який режим ви використовуєте? автономна, місцева, пряжа?
egordoe

Так, я перевірив, що ця відповідь неправильна. Час виконання те саме. Я також надрукував конфігурацію в обох випадках, і вона однакова.
maestromusica

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