web-dev-qa-db-ja.com

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

私はScalaよりPythonを好みます。ただし、SparkはScalaでネイティブに記述されているため、ScalaではPythonバージョンよりも明確な理由でコードが高速で実行されると予想していました。

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

Scala Code

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パフォーマンスステージ0(38分)、ステージ1(18秒) enter image description here

Pythonパフォーマンスステージ0(11分)、ステージ1(7秒) enter image description here

両方とも異なるDAG視覚化グラフを生成します(両方の写真がScala(map)とPython(reduceByKey)に対して異なるステージ0関数を示しているため)

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

パフォーマンスに関しては、このような実際のデータのScalaコードは、Pythonバージョンよりも 4倍遅く実行されるようです。私にとって良いニュースは、Pythonを使い続ける良いモチベーションを与えてくれたことです。悪いニュースは、私がその理由をよく理解していなかったことですか?

154
Mrityunjay

コードについて説明した元の回答は以下にあります。


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

RDD API

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

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

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

    • インタプリタのメモリフットプリント
    • ロードされたライブラリのフットプリント
    • 効率の悪いブロードキャスト(各プロセスにはブロードキャストの独自のコピーが必要です)
  • Pythonコード自体のパフォーマンス。一般的に、ScalaはPythonより高速ですが、タスクごとに異なります。さらに、 Numba などのJIT、C拡張( Cython )、または 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インタープリター間の完全な往復です。可能であれば、組み込み式の構成を優先する必要があります( 。Python UDFの動作はSpark 2.0.0で改善されましたが、ネイティブ実行と比較するとまだ最適ではありません ベクトル化UDF(SPARK-21190) の導入により、将来的に改善される可能性があります。

また、DataFramesRDDsの間で不必要なデータの受け渡しを行わないようにしてください。これには、Pythonインタープリターとの間のデータ転送は言うまでもなく、高価なシリアル化と逆シリアル化が必要です。

Py4J呼び出しの待ち時間がかなり長いことに注意してください。これには、次のような単純な呼び出しが含まれます。

from pyspark.sql.functions import col

col("foo")

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

GraphXおよびSpark DataSets

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

実際には、GraphXの開発はほぼ完全に停止し、プロジェクトは現在 関連するJIRAチケットはnot fix でメンテナンスモードになっています。 GraphFrames libraryは、Pythonバインディングを持つ代替のグラフ処理ライブラリを提供します。

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

ストリーミング

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

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

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

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

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

PySpark APIはScalaに相当するものを密接に反映しているため、正確にはPythonicではありません。つまり、言語間でマッピングするのは非常に簡単ですが、同時にPythonコードを理解することは非常に困難です。

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

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

さらに、ネイティブPython関数は、引き続きSQLの世界で2番目に重要な役割を果たします。将来的には、Apache Arrowのシリアル化でこれが改善されることを願っています( 現在の努力はターゲットデータcollection しかしUDF serdeは 長期目標 )。

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

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

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

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


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

(免責事項:Pythonistaの観点。ほとんどの場合、いくつかのScalaトリックを見逃しました)

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

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

通常、私はそれにこだわるつもりはありませんが、私が知る限り、あなたのScalaコードのボトルネックです。 JVMでの文字列の結合はかなり高価な操作です(例: scalaの文字列連結はJavaの場合と同じくらいコストがかかりますか? )。これは、コードの_.reduceByKey((v1: String, v2: String) => v1 + ',' + v2)に相当するこのinput4.reduceByKey(valsConcat)のようなものは、良いアイデアではないことを意味します。

groupByKeyを避けたい場合は、aggregateByKeyStringBuilderとともに使用してみてください。これに似た何かがトリックを行うはずです:

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

しかし、私はそれがすべての大騒ぎの価値があるとは思わない。

上記を念頭に置いて、コードを次のように書き直しました。

スカラ

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

結果

Executorごとに4GBのメモリを搭載したlocal[6]モード(Intel(R)Xeon(R)CPU E3-1245 V2 @ 3.40GHz)では、次のようになります(n = 3):

  • Scala-平均:250.00秒、標準偏差:12.49
  • Python-平均:246.66秒、stdev: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:])
309
zero323

上記の回答の拡張-

Scalaはpythonと比較して多くの点で高速であることが証明されていますが、pythonがscalaよりも人気を博している正当な理由がいくつかあります。

PythonのApache Sparkは、簡単に習得して使用できます。しかし、これがPysparkがScalaよりも良い選択である唯一の理由ではありません。他にもあります。

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

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

Pythonには、機械学習と自然言語処理に関連するいくつかのライブラリが付属しています。これは、データ分析を支援し、非常に成熟したタイムテスト済みの統計も備えています。たとえば、numpy、pandas、scikit-learn、seaborn、matplotlibなどです。

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

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

0
user11731048