ScalaとPythonのSparkパフォーマンス


178

ScalaよりPythonの方が好きです。しかし、SparkはScalaでネイティブに記述されているため、明らかな理由により、コードがPythonバージョンよりもScalaで高速に実行されることを期待していました。

その前提で、1 GBのデータ用の非常に一般的ないくつかの前処理コードのScalaバージョンを学び、書くことを考えました。データはKaggleのSpringLeafコンテストから選択されます。データの概要を説明するだけです(1936のディメンションと145232の行が含まれています)。データは、int、float、string、booleanなどのさまざまなタイプで構成されます。Spark処理には8つのうち6つのコアを使用しています。そのminPartitions=6ため、すべてのコアに処理対象があるように使用しました。

Scalaコード

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コード

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 Performance ステージ0(38分)、ステージ1(18秒) ここに画像の説明を入力してください

Pythonパフォーマンス ステージ0(11分)、ステージ1(7秒) ここに画像の説明を入力してください

どちらも異なるDAG視覚化グラフを生成します(これにより、両方の図はScala(map)とPython(reduceByKey)の異なるステージ0関数を示しています)

ただし、基本的には両方のコードがデータを(dimension_id、値のリストの文字列)RDDに変換してディスクに保存しようとします。出力は、各ディメンションのさまざまな統計を計算するために使用されます。

パフォーマンスに関しては、このような実際のデータのScalaコードは、Pythonバージョンの4倍の速度で実行されるようです。私にとって朗報は、Pythonを使い続ける動機が与えられたことです。悪い知らせは、その理由がよくわからなかったのですか?


8
おそらくこれはコードとアプリケーションに依存しているため、他の結果が得られます。πのライプニッツ式の10億項を合計すると、apache spark pythonはscalaよりも遅くなります
Paul

3
興味深い質問です!ところで、こちらもご覧ください:emptypipes.org/2015/01/17/python-vs-scala-vs-sparkコアが多いほど、言語間の違いが少なくなります。
Markon

既存の回答を受け入れることを検討しましたか?
10465355はモニカを復活させる

回答:


357

コードを議論する元の答えは以下にあります。


まず最初に、異なるタイプのAPIを区別する必要があり、それぞれに独自のパフォーマンスの考慮事項があります。

RDD API

(JVMベースのオーケストレーションによる純粋なPython構造)

これは、PythonコードのパフォーマンスとPySpark実装の詳細によって最も影響を受けるコンポーネントです。Pythonのパフォーマンスが問題になる可能性はかなり低いですが、考慮すべき少なくともいくつかの要因があります。

  • JVM通信のオーバーヘッド。実際には、Pythonエグゼキューターとの間でやり取りされるすべてのデータは、ソケットとJVMワーカーを介して渡される必要があります。これは比較的効率的なローカル通信ですが、まだ無料ではありません。
  • プロセスベースの実行プログラム(Python)とスレッドベースの実行プログラム(単一のJVM複数スレッド)の実行プログラム(Scala)。各Python executorは独自のプロセスで実行されます。副作用として、JVMの同等物よりも強力な分離が提供され、executorライフサイクルをある程度制御できますが、メモリ使用量が大幅に高くなる可能性があります。

    • インタプリタのメモリフットプリント
    • ロードされたライブラリのフットプリント
    • 効率の悪いブロードキャスト(各プロセスにはブロードキャストの独自のコピーが必要)
  • Pythonコード自体のパフォーマンス。一般的に言えば、ScalaはPythonよりも高速ですが、タスクによって異なります。さらに、Numba、C拡張(Cython)などのJIT 、またはTheanoなどの専用ライブラリを含む複数のオプションがあります。最後に、ML / MLlib(または単にNumPyスタック)を使用しない場合はPyPyを代替インタープリターとして使用することを検討してください。SPARK-3094を参照してください。

  • PySpark構成は、 spark.python.worker.reuse各タスクのPythonプロセスをフォークするか、既存のプロセスを再利用するかを選択できるオプションがされています。後者のオプションは、高価なガベージコレクションを回避するのに役立つようです(体系的なテストの結果よりも印象的です)。一方、前者のオプション(デフォルト)は、高価なブロードキャストとインポートの場合に最適です。
  • CPythonの最初の行のガベージコレクションメソッドとして使用される参照カウントは、典型的なSparkワークロード(ストリームのような処理、参照サイクルなし)でかなり機能し、長いGC一時停止のリスクを軽減します。

MLlib

(PythonとJVMの混合実行)

基本的な考慮事項は以前とほとんど同じですが、いくつかの追加の問題があります。MLlibで使用される基本構造はプレーンなPython RDDオブジェクトですが、すべてのアルゴリズムはScalaを使用して直接実行されます。

これは、PythonオブジェクトをScalaオブジェクトに変換するなどの追加コスト、メモリ使用量の増加、および後で取り上げる追加の制限を意味します。

現在(Spark 2.x)、RDDベースのAPIはメンテナンスモードであり、Spark 3.0では削除される予定です。

DataFrame APIとSpark ML

(ドライバーに限定されたPythonコードでのJVM実行)

これらはおそらく、標準のデータ処理タスクに最適です。Pythonコードは主にドライバーの高レベルの論理演算に限定されているため、PythonとScalaの間にパフォーマンスの違いはありません。

単一の例外は、行単位のPython UDFの使用です。これは、Scalaの同等のものよりも効率が大幅に低下します。改善の余地はありますが(Spark 2.0.0では大幅な開発が行われています)、最大の制限は内部表現(JVM)とPythonインタープリター間の完全な往復です。可能であれば、組み込み式の構成を優先する必要があります(例: Spark 2.0.0ではPython UDFの動作が改善されていますが、ネイティブの実行と比較すると依然として最適ではありません。

これは将来改善される可能性があり、ベクトル化されたUDF(SPARK-21190およびその他の拡張機能)の導入により大幅に改善されました。ほとんどのアプリケーションでは、二次的なオーバーヘッドは無視できます。

また、DataFramesとの間でデータを不必要に渡さないようにしてくださいRDDs。これには、Pythonインタプリタとの間のデータ転送は言うまでもなく、高価なシリアライゼーションとデシリアライゼーションが必要です。

Py4J呼び出しのレイテンシがかなり長いことは注目に値します。これには、次のような単純な呼び出しが含まれます。

from pyspark.sql.functions import col

col("foo")

通常、これは問題になりません(オーバーヘッドは一定であり、データの量に依存しません)。しかし、ソフトリアルタイムアプリケーションの場合は、Javaラッパーのキャッシュ/再利用を検討してください。

GraphXおよびSparkデータセット

今のところ(Spark 1.6 2.1)どちらもPySpark APIを提供していないため、PySparkはScalaよりもはるかに悪いと言えます。

GraphX

実際には、GraphX開発はほぼ完全に停止し、プロジェクトは現在メンテナンスモードにあり、関連するJIRAチケットは修正されないため閉じられていますGraphFramesライブラリは、Pythonバインディングを備えた代替のグラフ処理ライブラリを提供します。

データセット

主観的に言えばDatasets、Python で静的に型付けする場所があまりなく、現在のScalaの実装が単純すぎても、と同じパフォーマンス上の利点は得られませんDataFrame

ストリーミング

これまで見てきたことから、PythonではなくScalaを使用することを強くお勧めします。PySparkが構造化ストリームをサポートするようになれば、将来的には変更される可能性がありますが、現時点では、Scala APIの方がはるかに堅牢で包括的かつ効率的です。私の経験はかなり限られています。

Spark 2.xの構造化ストリーミングは言語間のギャップを減らすようですが、現時点ではまだ初期段階です。それにもかかわらず、RDDベースのAPIは、Databricksドキュメント(アクセス日2017-03-03)で「レガシーストリーミング」としてすでに参照されているため、さらなる統合の取り組みを期待するのは妥当です。

パフォーマンス以外の考慮事項

機能のパリティ

すべてのSpark機能がPySpark APIを通じて公開されるわけではありません。必要なパーツがすでに実装されているかどうかを確認し、考えられる制限を理解してください。

MLlibおよび同様の混合コンテキストを使用する場合は特に重要です(タスクからのJava / Scala関数の呼び出しを参照)。)。公平にするために、などのPySpark APIの一部は、mllib.linalgScalaよりも包括的なメソッドセットを提供します。

APIデザイン

PySpark APIは、対応するScalaを厳密に反映しているため、正確にはPythonicではありません。つまり、言語間でのマッピングは非常に簡単ですが、同時にPythonコードは非常に理解しにくくなる可能性があります。

複雑なアーキテクチャ

PySparkのデータフローは、純粋なJVMの実行に比べて比較的複雑です。PySparkプログラムまたはデバッグについて推論することははるかに困難です。さらに、少なくともScalaとJVMの一般的な基本的な理解は、ほぼ必須です。

Spark 2.x以降

DatasetRDD APIの凍結により、API への継続的な移行は、Pythonユーザーに機会と課題の両方をもたらします。APIの高レベルの部分はPythonで公開する方がはるかに簡単ですが、より高度な機能を直接使用することはほとんど不可能です。

さらに、ネイティブPython関数は引き続きSQLの世界で2番目のクラスの市民です。うまくいけば、Apache Arrowのシリアライゼーション(現在の取り組みではデータを対象としていますcollectionが、UDF serdeが長期的な目標です)によって、これが将来的に改善されることを願っています。

Pythonコードベースに強く依存するプロジェクトの場合、純粋なPythonの代替(DaskRayなど)が興味深い代替になる可能性があります。

どちらかである必要はありません

Spark DataFrame(SQL、Dataset)APIは、PySparkアプリケーションにScala / Javaコードを統合するエレガントな方法を提供します。を使用DataFramesして、データをネイティブJVMコードに公開し、結果を読み取ることができます。一部のオプションについては別の場所で説明しましたが、Python-Scala往復の実際の例は、Pyspark内でScalaクラスを使用する方法にあります

ユーザー定義型を導入することでさらに拡張できます(「Spark SQLでカスタム型のスキーマを定義する方法」を参照)。


質問で提供されたコードの何が問題になっていますか

(免責事項:Pythonistaの視点。おそらく私はいくつかのScalaのトリックを逃した)

まず第一に、あなたのコードにはまったく意味をなさない部分があります。(key, value)を使用してペアをすでに作成している場合、zipWithIndexまたはenumerateすぐに分割するために文字列を作成するポイントは何ですか?flatMapは再帰的に機能しないため、タプルを生成し、フォローをスキップできますmap

私が問題だと思う別の部分はreduceByKeyです。一般的に言えば、reduceByKey集約関数を適用することで、シャッフルする必要があるデータの量を減らすことができる場合に役立ちます。単に文字列を連結するだけなので、ここでは何も得られません。参照の数などの低レベルのものを無視すると、転送する必要のあるデータの量はとまったく同じですgroupByKey

通常、それについては詳しく説明しませんが、私の知る限り、Scalaコードのボトルネックになります。JVMでの文字列の結合は、かなりコストのかかる操作です(たとえば、Javaの場合と同じように、Scalaでの文字列連結はコストがかかりますか?)。それはあなたのコードと_.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")

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

結果

local[6]エグゼキュータ当たり4GBのメモリとモード(インテル(R)Xeonプロセッサ(R)CPU E3-1245 V2 @ 3.40GHz)はかかり(N = 3)。

  • Scala-平均:250.00秒、標準偏差:12.49
  • Python-平均:246.66s、標準偏差:1.15

そのほとんどの時間は、シャッフル、シリアライズ、デシリアライズ、その他の二次的なタスクに費やされていると思います。面白くするために、このマシンで同じタスクを1分未満で実行する単純な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:])

22
しばらく出会った中で最も明確で包括的で有用な答えの1つ。ありがとう!
etov 2018年

あなたはなんてすばらしい人でしょう。
DennisLi

-4

上記の回答の拡張-

ScalaはPythonに比べて多くの点で高速であることが証明されていますが、PythonがScalaよりも人気になっている正当な理由がいくつかあります。そのいくつかを見てみましょう—

Python for Apache Sparkは、習得と使用が非常に簡単です。ただし、PysparkがScalaよりも優れている理由はこれだけではありません。もっとあります。

SparkのPython APIはクラスター上では遅くなる可能性がありますが、結局のところ、データサイエンティストはScalaに比べてはるかに多くのことができます。Scalaの複雑さはありません。インターフェースはシンプルで包括的です。

コードの読みやすさ、メンテナンス、Apache Spark用のPython APIの知識について話すことは、Scalaよりもはるかに優れています。

Pythonには、機械学習と自然言語処理に関連するいくつかのライブラリが付属しています。これはデータ分析に役立ち、非常に成熟しており、長い時間をかけて検証された統計情報もあります。たとえば、numpy、pandas、scikit-learn、seaborn、matplotlibなどです。

注:ほとんどのデータサイエンティストは、両方のAPIの最良のものを使用するハイブリッドアプローチを使用しています。

最後に、Scalaコミュニティは、プログラマーにとってあまり役に立たないことがよくあります。これにより、Pythonは非常に貴重な学習になります。Javaのような静的に型付けされたプログラミング言語について十分な経験があれば、Scalaをまったく使用しないことについて心配する必要がなくなります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.