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.reuse
her 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 DataFrames
ve 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.linalg
daha 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
Dataset
Dondurulmuş 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 DataFrames
yerli 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 zipWithIndex
veya enumerate
dize 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, reduceByKey
toplama 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 groupByKey
kullanmak deneyebilirsiniz aggregateByKey
ile 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:])