計算タスクのPythonパフォーマンスベンチマーク

やる気



ごく最近、Python用のNumbaオプティマイザーJI​​Tコンパイラライブラリの新しいバージョン0.34がリリースされました。 乾杯! 待望の注釈のセマンティクスと並列計算を編成するための一連のメソッドが登場しました。 基礎は、 Intel Parallel Acceleratorテクノロジーによって採用されました。



この記事では、クアッドコアプロセッサを備えた最新のマシンで、このライブラリに基づいたコンピューティング速度の最初のテストの結果を共有したいと思います。



はじめに



現在、Pythonは科学コンピューティングで非常に積極的に使用されており、一般に機械学習の分野ではほぼ標準の1つです。 しかし、もう少し詳しく見てみると、ほとんどすべての場所でPythonが低レベルのライブラリのラッパーとして使用されており、ほとんどがC / C ++で書かれています。 純粋なPythonで本当に高速で並列的なコードを書くことは可能ですか?



非常に単純なタスクを検討してください。 3次元空間のN点の2つのセット、 pqを与えます。 すべてのポイント間のペアワイズ距離に基づいて特別なマトリックスを計算する必要があります。





Rij= frac11+ |piqj |2







すべてのテストで、 N = 5000を取ります。計算時間は10回の開始で平均されます。



C ++実装



参考として、C ++で次の実装を使用します。



void getR(std::vector<Point3D> & p, std::vector<Point3D> & q, Matrix & R) { double rx, ry, rz; #pragma omp parallel for for (int i = 0; i < p.size(); i++) { for (int j = 0; j < q.size(); j++) { rx = p[i].x - q[j].x; ry = p[i].y - q[j].y; rz = p[i].z - q[j].z; R(i, j) = 1 / (1 + sqrt(rx * rx + ry * ry + rz * rz)); } } }
      
      





pポイントの外側のループは、 OpenMPテクノロジーを使用して並列に実行されます。



リードタイム:44 ms。



ピュアパイソン



純粋なPythonコードで速度テストを始めましょう:



 def get_R(p, q): R = np.empty((p.shape[0], q.shape[1])) for i in range(p.shape[0]): for j in range(q.shape[1]): rx = p[i, 0] - q[0, j] ry = p[i, 1] - q[1, j] rz = p[i, 2] - q[2, j] R[i, j] = 1 / (1 + math.sqrt(rx * rx + ry * ry + rz * rz)) return R
      
      





実行時間:52,861ミリ秒、基本実装よりも1000倍以上遅い。



Pythonはインタープリター言語であり、PythonはGILを内部で使用するため、コード自体のレベルでは並列化が不可能になります。 それはすべて非常に遅いです。 今、すべてをスピードアップし始めます。



Python + NumPy + SciPy



数値問題のPythonの遅さの問題は、かなり前に認識されていました。 そして、この問題に対する答えはNumPyライブラリでした。 NumPyのイデオロギーは多くの点でMatLabに近く、MatLabは科学計算のために広く認識されているツールです。



繰り返し考えるのをやめ、計算のための原子オブジェクトとして行列とベクトルを考え始めます。 また、下位レベルの行列とベクトルを使用したすべての操作は、高性能の線形代数ライブラリIntel MKLまたはATLASによって既に実行されています。



NumPyでの実装は次のようになります。



 def get_R_numpy(p, q): Rx = p[:, 0:1] - q[0:1] Ry = p[:, 1:2] - q[1:2] Rz = p[:, 2:3] - q[2:3] R = 1 / (1 + np.sqrt(Rx * Rx + Ry * Ry + Rz * Rz)) return R
      
      





この実装では、単一のサイクルはまったくありません!



実行時間:839ミリ秒。これは基本実装よりも19倍遅いです。



さらに、NumPyとSciPyには膨大な数の組み込み関数があります。 SciPyでのこのタスクの実装は次のようになります。



 def get_R_scipy(p, q): D = spd.cdist(p, qT) R = 1 / (1 + D) return R
      
      





実行時間:353ミリ秒。これは、基本実装よりも8倍遅いです。



ほとんどのタスクでは、これはすでに許容可能な時間です。 これの代価は考え方を変えることです。今では、線形代数の基本的な操作からコードを収集する必要があります。 時々とても素敵に見えますが、時には別のトリックを考え出す必要があります。



しかし、並列処理はどうでしょうか? ここで彼女は暗黙的です。 低レベルで、行列とベクトルを使用したすべての操作が効率的かつ並列に実装されることを願っています。



しかし、コードが線形代数に適合しない場合、または明示的な並列化が必要な場合はどうなりますか?



Python + Cython



ここにCythonの時間が来ます。 Cythonは、通常のPythonコード内のC-way言語にコードを埋め込むことができる特別な言語です。 次に、Cythonはこのコードを.cファイルに変換し、Pythonモジュールにコンパイルします。 これらのモジュールは、Pythonコードの他の部分で透過的に呼び出すことができます。 Cythonでの実装は次のようになります。



 @cython.wraparound(False) @cython.nonecheck(False) @cython.boundscheck(False) def get_R_cython_mp(py_p, py_q): py_R = np.empty((py_p.shape[0], py_q.shape[1])) cdef int nP = py_p.shape[0] cdef int nQ = py_q.shape[1] cdef double[:, :] p = py_p cdef double[:, :] q = py_q cdef double[:, :] R = py_R cdef int i, j cdef double rx, ry, rz with nogil: for i in prange(nP): for j in xrange(nQ): rx = p[i, 0] - q[0, j] ry = p[i, 1] - q[1, j] rz = p[i, 2] - q[2, j] R[i, j] = 1 / (1 + sqrt(rx * rx + ry * ry + rz * rz)) return py_R
      
      





ここで何が起こっていますか? この関数はpython numpyオブジェクトを入力として受け入れ、それらは型指定されたCython C構造に変換され、次にgilがオフになり、特別な「prange」構造を使用して、外部ループが並列に実行されます。



ランタイム:76ミリ秒。これは、基本実装よりも1.75倍遅いです。



さて、私たちは基本実装にほとんど近づき、明示的な並列コードを書き始めました。 しかし、価格は読みにくいコードであったため、純粋なPythonを残しました。



一般的に、ほとんどの数値計算はそのように書かれています。 それらのほとんどはNumPyにあり、速度に重要ないくつかの場所は別々のモジュールで取り出され、cythonに実装されています。



Python + Numba



私たちは長い道のりを歩んできました。 私たちは純粋なPythonから始めて、マトリックスコンピューティングの魔法の道を進み、特別な拡張言語に突入しました。 始めたところに戻る時が来ました。 したがって、Python + Numbaでの実装:



 @jit(float64[:, :](float64[:, :], float64[:, :]), nopython=True, parallel=True) def get_R_numba_mp(p, q): R = np.empty((p.shape[0], q.shape[1])) for i in prange(p.shape[0]): for j in range(q.shape[1]): rx = p[i, 0] - q[0, j] ry = p[i, 1] - q[1, j] rz = p[i, 2] - q[2, j] R[i, j] = 1 / (1 + math.sqrt(rx * rx + ry * ry + rz * rz)) return R
      
      





実行時間:46ミリ秒。これは基本的な実装とほぼ一致しています。



そして、元の遅いPythonコードを使用してこれを行う必要がありました。





Numbaは非常に興味深いプロジェクトです。 これは、LLVMベースのPython用最適化JITコンパイラです。 使用することは絶対に透過的で、個別のモジュールは必要ありません。 必要なのは、速度が重要なメソッドにいくつかの注釈を追加することだけです。



要約すると、実行時間は次のとおりです。

C ++ Python ナンピー シピー シトン ヌンバ
44ミリ秒 52 861ミリ秒 839ミリ秒 353ミリ秒 76ミリ秒 46ミリ秒



All Articles