Scala vs Python için kıvılcım performansı


178

Scala yerine Python'u tercih ederim. Ancak, Spark Scala'da yerel olarak yazıldığından, kodumun Scala'da belirgin nedenlerle Python sürümünden daha hızlı çalışmasını bekliyordum.

Bu varsayımla, yaklaşık 1 GB veri için bazı çok yaygın önişleme kodunun Scala sürümünü öğrenmeyi ve yazmayı düşündüm. Veriler, Kaggle'daki SpringLeaf yarışmasından toplandı . Sadece verilere genel bir bakış vermek için (1936 boyutları ve 145232 satır içerir). Veriler, int, float, string, boolean gibi çeşitli türlerden oluşur. Spark işleme için 8'den 6 çekirdek kullanıyorum; bu yüzden minPartitions=6her çekirdeğin işlemek için bir şeyleri kullandım.

Scala Kodu

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")

Python Kodu

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 Performansı Aşama 0 (38 dk.), Aşama 1 (18 sn.) resim açıklamasını buraya girin

Python Performansı Aşama 0 (11 dk.), Aşama 1 (7 sn.) resim açıklamasını buraya girin

Her ikisi de farklı DAG görselleştirme grafikleri üretir (bu nedenle her iki resim de Scala ( map) ve Python ( reduceByKey) için farklı aşama 0 işlevleri gösterdiği için )

Ancak, aslında her iki kod da verileri (dimension_id, değerler listesi dizisi) RDD'ye dönüştürmeye ve diske kaydetmeye çalışır. Çıktı, her bir boyut için çeşitli istatistikleri hesaplamak için kullanılacaktır.

Performans açısından, bu gerçek veriler için Scala kodu Python sürümünden 4 kat daha yavaş çalışıyor gibi görünüyor . Benim için iyi haber, Python ile kalmak için iyi bir motivasyon sağladığım. Kötü haber, nedenini tam olarak anlamadım mı?


8
Belki de bu, diğer sonuca ulaştığımda
Paul

3
İlginç soru! Btw, burada da bir göz atın: emptypipes.org/2015/01/17/python-vs-scala-vs-spark Ne kadar çok çekirdeğiniz varsa, diller arasındaki farkları o kadar az görebilirsiniz.
Markon

Mevcut cevabı kabul etmeyi düşündünüz mü ?
10465355 diyor Reinstate Monica

Yanıtlar:


358

Kodu tartışan orijinal cevap aşağıda bulunabilir.


Her şeyden önce, her biri kendi performans değerlendirmeleri olan farklı API türleri arasında ayrım yapmanız gerekir.

RDD API'sı

(JVM tabanlı düzenleme ile saf Python yapıları)

Bu, Python kodunun performansından ve PySpark uygulamasının ayrıntılarından en çok etkilenecek bileşendir. Python performansının bir sorun olması pek olası olmasa da, göz önünde bulundurmanız gereken en az birkaç faktör vardır:

  • JVM iletişim yükü. Pratik olarak Python yürütücüsüne gelen ve gelen tüm verilerin bir soket ve bir JVM çalışanından geçirilmesi gerekir. Bu nispeten verimli bir yerel iletişim olsa da, hala ücretsiz değildir.
  • İşlem tabanlı yürütücüler (Python) ve iş parçacığı tabanlı (tek JVM çoklu iş parçacıkları) yürütücüler (Scala). Her Python yürütücüsü kendi işleminde çalışır. Bir yan etki olarak, JVM muadilinden daha güçlü izolasyon ve yürütücü yaşam döngüsü üzerinde bazı kontroller sağlar, ancak potansiyel olarak önemli ölçüde daha yüksek bellek kullanımı sağlar:

    • tercüman bellek ayak izi
    • yüklü kütüphanelerin kapladığı alan
    • daha az verimli yayın (her işlem kendi yayın kopyasını gerektirir)
  • Python kodunun kendisi. Genel olarak Scala, Python'dan daha hızlıdır, ancak göreve göre değişir. Üstelik sizin gibi JITs dahil birden fazla seçenek var Numba , C uzantıları ( Cython ) ya da benzeri uzmanlaşmış kütüphaneler Theano . Son olarak, ML / MLlib (ya da sadece NumPy yığını) kullanmıyorsanız , alternatif bir yorumlayıcı olarak PyPy'yi kullanmayı düşünün . Bkz. SPARK-3094 .

  • PySpark yapılandırması, spark.python.worker.reuseher görev için Python işlemini çatallamak ve mevcut işlemi yeniden kullanmak arasında seçim yapmak için kullanılabilecek bir seçenek sunar. İkinci seçenek pahalı çöp toplamadan kaçınmak için yararlı görünmektedir (sistematik testlerin sonucundan daha fazla bir izlenimdir), birincisi (varsayılan) pahalı yayınlar ve ithalatlar için idealdir.
  • CPython'da ilk satır çöp toplama yöntemi olarak kullanılan referans sayma, tipik Spark iş yükleriyle (akış benzeri işleme, referans döngüsü yok) oldukça iyi çalışır ve uzun GC duraklatma riskini azaltır.

MLlib

(karışık Python ve JVM yürütme)

Temel hususlar, birkaç ek sorunla daha önce olduğu gibidir. MLlib ile kullanılan temel yapılar sade Python RDD nesneleriyken, tüm algoritmalar doğrudan Scala kullanılarak yürütülür.

Bu, Python nesnelerini Scala nesnelerine dönüştürmenin ek bir maliyeti, bunun tersi, artan bellek kullanımı ve daha sonra ele alacağımız bazı ek sınırlamalar anlamına gelir.

Şu andan itibaren (Spark 2.x), RDD tabanlı API bir bakım modunda ve Spark 3.0'da kaldırılması planlanıyor .

DataFrame API'sı ve Spark ML

(Sürücü ile sınırlı Python kodu ile JVM yürütme)

Bunlar muhtemelen standart veri işleme görevleri için en iyi seçimdir. Python kodu çoğunlukla sürücüdeki üst düzey mantıksal işlemlerle sınırlı olduğundan, Python ve Scala arasında hiçbir performans farkı olmamalıdır.

Tek bir istisna, Scala eşdeğerlerinden önemli ölçüde daha az verimli olan satır bazında Python UDF'lerin kullanılmasıdır. İyileştirme şansı olsa da (Spark 2.0.0'da önemli bir gelişme oldu), en büyük sınırlama dahili temsil (JVM) ve Python yorumlayıcısı arasında tam gidiş dönüş. Mümkünse, yerleşik ifadelerin bir bileşimini tercih etmelisiniz ( örnek . Python UDF davranışı Spark 2.0.0'da geliştirildi, ancak yerel yürütme ile karşılaştırıldığında hala yetersizdir.

Bu gelecek geliştirilmiş olabilir önemli tanıtımıyla iyileşmiştir vektörlü UDF (kıvılcım 21190 ve daha fazla uzatılması) sıfır kopya seri kaldırma verimli veri değişimi için Ok izle kullanır. Çoğu uygulama için ikincil genel giderleri göz ardı edilebilir.

Ayrıca DataFramesve arasında gereksiz veri iletmekten kaçının RDDs. Bu, pahalı serileştirme ve serileştirmeyi gerektirir, Python yorumlayıcısına ve Python yorumlayıcısından veri aktarımından bahsetmiyoruz.

Py4J çağrılarının oldukça yüksek gecikmeye sahip olduğunu belirtmek gerekir. Bu, aşağıdakiler gibi basit çağrıları içerir:

from pyspark.sql.functions import col

col("foo")

Genellikle, önemli olmamalıdır (ek yük sabittir ve veri miktarına bağlı değildir), ancak yumuşak gerçek zamanlı uygulamalar söz konusu olduğunda, Java paketleyicilerini önbelleğe almayı / yeniden kullanmayı düşünebilirsiniz.

GraphX ​​ve Spark Veri Kümeleri

Şimdilik (Spark 1.6 2.1) hiçbiri PySpark API sunmuyor, bu yüzden PySpark'ın Scala'dan sonsuz daha kötü olduğunu söyleyebilirsiniz.

GRAPHX

Uygulamada, GraphX ​​geliştirme neredeyse tamamen durdu ve proje şu anda ilgili JIRA biletleri kapalı olduğu için bakım modunda değil . GraphFrames kütüphanesi, Python bağlamaları olan alternatif bir grafik işleme kütüphanesi sağlar.

Veri kümesi

Sübjektif olarak konuşursak Datasets, Python'da statik olarak yazılmak için fazla yer yoktur ve mevcut Scala uygulaması olsa bile çok basittir ve aynı performans avantajlarını sağlamaz DataFrame.

Yayın Akışı

Şimdiye kadar gördüğüm kadarıyla, Scala'yı Python üzerinden kullanmanızı şiddetle tavsiye ederim. PySpark'ın yapılandırılmış akışlar için destek alması gelecekte değişebilir, ancak şu anda Scala API çok daha sağlam, kapsamlı ve verimli görünmektedir. Deneyimlerim oldukça sınırlıdır.

Spark 2.x'te yapılandırılmış akış, diller arasındaki boşluğu azaltıyor gibi görünüyor, ancak şimdilik hala ilk günlerinde. Bununla birlikte, RDD tabanlı API zaten Veritabanları Belgeleri'nde (erişim tarihi 2017-03-03) zaten "eski akış" olarak adlandırılmaktadır .

Performans dışı hususlar

Özellik paritesi

Tüm Spark özellikleri PySpark API'sı aracılığıyla gösterilmez. İhtiyacınız olan parçaların önceden uygulanmış olup olmadığını kontrol ettiğinizden ve olası sınırlamaları anlamaya çalıştığınızdan emin olun.

MLlib ve benzeri karışık bağlamları kullanırken özellikle önemlidir (bkz. Görevden Java / Scala işlevini arama ). Adil olmak gerekirse, PySpark API'sinin bazı bölümleri Scala'dan mllib.linalgdaha kapsamlı bir yöntem seti sağlar.

API tasarımı

PySpark API, Scala muadilini yakından yansıtıyor ve tam olarak Pythonic değil. Bu, diller arasında eşleştirmenin oldukça kolay olduğu anlamına gelir, ancak aynı zamanda Python kodunu anlamak çok daha zor olabilir.

Karmaşık mimari

PySpark veri akışı, saf JVM yürütmesine kıyasla nispeten karmaşıktır. PySpark programları veya hata ayıklama hakkında akıl yürütmek çok daha zordur. Dahası, Scala ve JVM'nin genel olarak en azından temel anlayışı hemen hemen bir zorunluluktur.

Spark 2.x ve Ötesi

DatasetDondurulmuş RDD API ile API'ye sürekli geçiş , Python kullanıcıları için hem fırsatlar hem de zorluklar getirir. API'nın üst düzey bölümlerinin Python'da gösterilmesi çok daha kolay olsa da, daha gelişmiş özelliklerin doğrudan kullanılması neredeyse imkansızdır .

Ayrıca yerli Python fonksiyonları SQL dünyasında ikinci sınıf vatandaş olmaya devam ediyor. Umarım bu gelecekte Apache Arrow serileştirme ile iyileşecektir ( mevcut çabalar hedef verilericollection ancak UDF serde uzun vadeli bir hedeftir ).

Python kod tabanına büyük ölçüde bağlı olan projeler için saf Python alternatifleri ( Dask veya Ray gibi ) ilginç bir alternatif olabilir.

Biri diğerine karşı olmak zorunda değil

Spark DataFrame (SQL, Veri Kümesi) API, Scala / Java kodunu PySpark uygulamasına entegre etmek için zarif bir yol sağlar. Sen kullanabilirsiniz DataFramesyerli JVM koduna verileri ortaya çıkarmak ve sonuçları geri okumak için. Başka bir yerde bazı seçenekleri açıkladım ve Python-Scala gidiş dönüşünün çalışan bir örneğini bulabilirsiniz. Pyspark içinde bir Scala sınıfı nasıl kullanılır .

Kullanıcı Tanımlı Türler tanıtılarak daha da artırılabilir (bkz . Spark SQL'de özel tür için şema nasıl tanımlanır? ).


Soruda verilen kodda yanlış olan ne?

(Feragatname: Pythonista bakış açısı. Büyük olasılıkla bazı Scala numaralarını kaçırdım)

Her şeyden önce, kodunuzda hiç mantıklı olmayan bir bölüm var. Zaten (key, value)kullanarak çiftleri oluşturduysanız zipWithIndexveya enumeratedize oluşturmanın sadece onu sonra bölmek için anlamı nedir? flatMapözyinelemeli olarak çalışmaz, böylece tuples verebilirsiniz ve aşağıdakileri atlayabilirsiniz map.

Sorunlu bulduğum bir diğer bölüm de reduceByKey. Genel olarak konuşursak, reduceByKeytoplama işlevi uygulandığında karıştırılması gereken veri miktarını azaltabilirse yararlıdır. Dizeleri birleştirdiğiniz için burada kazanılacak bir şey yoktur. Referans sayısı gibi, düşük düzeyli şeyleri göz ardı ederek aktarmanız gereken veri miktarı tam olarak aynıdır groupByKey.

Normalde bunun üzerinde durmam, ama söyleyebildiğim kadarıyla Scala kodunuzda bir darboğaz. JVM'de dizeleri birleştirmek oldukça pahalı bir işlemdir (bkz. Örneğin: scala'da dize birleştirme Java'da olduğu kadar maliyetli midir? ). Bu , kodunuzda _.reduceByKey((v1: String, v2: String) => v1 + ',' + v2) eşdeğer olan böyle bir şeyin input4.reduceByKey(valsConcat)iyi bir fikir olmadığı anlamına gelir .

Eğer kaçınmak istiyorsanız groupByKeykullanmak deneyebilirsiniz aggregateByKeyile StringBuilder. Buna benzer bir şey hile yapmalıdır:

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)
  }
)

ama tüm yaygaraya değer olduğundan şüpheliyim.

Yukarıdakileri akılda tutarak, kodunuzu aşağıdaki gibi yeniden yazdım:

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")

Python :

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")

Sonuçlar

Gelen local[6]modu (Intel (R) Xeon (R) işlemci E3-1245 V2 @ 3.40GHz) 4GB bu alır uygulamakla başına hafıza (n = 3) ile:

  • Scala - ortalama: 250.00s, stdev: 12.49
  • Python - ortalama: 246.66s, stdev: 1.15

O zamanın çoğunun karıştırma, serileştirme, serileştirme ve diğer ikincil görevler için harcandığından eminim. Sadece eğlence için, Python'da aynı görevi bir dakikadan az bir sürede bu makinede gerçekleştiren saf tek iş parçacıklı kod:

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
Bir süredir karşılaştığım en açık, kapsamlı ve kullanışlı cevaplardan biri. Teşekkürler!
etov

Ne muhteşem bir adamsın!
DennisLi

-4

Yukarıdaki cevaplara genişletme -

Scala, python'a kıyasla birçok yönden daha hızlı kanıtlar, ancak python'un skalaların daha popüler hale gelmesinin bazı geçerli nedenleri vardır, bunlardan birkaçını görelim -

Apache Spark için Python'u öğrenmek ve kullanmak oldukça kolaydır. Ancak, Pyspark'ın Scala'dan daha iyi bir seçim olmasının tek nedeni bu değil. Fazlası var.

Spark için Python API kümede daha yavaş olabilir, ancak sonunda veri bilimcileri Scala ile karşılaştırıldığında çok daha fazlasını yapabilirler. Scala'nın karmaşıklığı yoktur. Arayüz basit ve kapsamlı.

Kod, bakım ve Apache Spark için Python API ile aşinalık hakkında konuşmak Scala'dan çok daha iyi.

Python, makine öğrenimi ve doğal dil işleme ile ilgili çeşitli kütüphanelerle birlikte gelir. Bu, veri analizine yardımcı olur ve ayrıca çok olgun ve zaman testli istatistiklere sahiptir. Örneğin, numpy, pandalar, scikit-öğren, deniz dibi ve matplotlib.

Not: Çoğu veri bilimcisi, her iki API'den en iyisini kullandıkları karma bir yaklaşım kullanır.

Son olarak, Scala topluluğu programcılar için genellikle çok daha az yardımcı oluyor. Bu, Python'u çok değerli bir öğrenme haline getirir. Java gibi statik olarak yazılmış herhangi bir programlama diliyle ilgili yeterli deneyime sahipseniz, Scala'yı tamamen kullanmama konusunda endişelenmeyi bırakabilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.