Pythonのパフォーマンス: ネイティブNumPyループ、コールバック、変換コスト
制御されたCPUのみの数値テストでは、CPythonループ、NumPy算術、Pythonコールバックラッパー、コンパイルされたNumbaループを通して同一の値が追跡されます。
制御されたCPUのみの数値テストでは、CPythonループ、NumPy算術、Pythonコールバックラッパー、コンパイルされたNumbaループを通して同一の値が追跡されます。準備されたNumPy入力が大幅にリードしていましたが、Pythonリストからの変換とPythonリストへの戻しにより、その優位性はほぼ同等にまで縮小しました。このエピソードでは、実行境界と、アプリケーションが必要とする結果を測定する方法について説明します。
この動画の要点
- CPythonはソースをバイトコードにコンパイルします。繰り返されるインタプリタ/オブジェクト処理と、型付けされたネイティブな数値ループは、異なる実行方式です。
- 数値的なNumPy算術は、型付けされたバッチをコンパイルされた操作に渡すことができます。算術ループは存在し、ビューは非連続なレイアウトを持つことがあります。
- 変換と必要な出力表現を測定してください。この実行では、Pythonリストを配列に変換し、元に戻すことで、準備済み配列の大きなリードが消えました。
- 小さなワークロードでは、固定の配列呼び出しコストが重要になるため、通常のPythonが有利になることがあります。8値のcomprehensionがこの測定比較で勝ちました。
- np.vectorizeは、連続する要素に対してPythonを呼び出すコンビニエンスラッパーです。バッチインターフェースだけではネイティブな数値実行が確立されるわけではありません。
- コンパイルされたPython構文のループも高速になりえます。呼び出し元にとって重要な場合は、コールドコンパイル/セットアップコストを含めてください。
- ネイティブ実行、CPU SIMD、マルチスレッド、GPUバックエンドは別々の概念です。利用可能なCPU機能は、特定の実行で使用される命令を追跡しません。
翻訳されたトランスクリプト
オリジナルの英語ナレーションから翻訳されています。利用可能なオーディオとキャプションはYouTubeによって管理されています。
なぜ同じCPUなのに早く終わるのか?
0:00 Pythonを速くするには新しい言語が必要だとお考えでしょう。 ループが移動した後、同じCPUがバッチをより速く実行しました。 作業はどこへ行ったのでしょう? 高速化はいつ消えるのでしょうか? どのショートカットがPythonを元に戻すのでしょうか? これはThe Daily Diffの舞台裏です。 そのショートカットに注目してください。 CPythonは標準のPython実装です。
0:17 ソースをバイトコードにコンパイルし、その命令を インタプリタで実行します。NumPyは、宣言されたデータ型を持つ値を保持する配列を持つ数値ライブラリです。 その組み込みの算術演算は、バッチ全体をコンパイル済みの操作に渡すことができます。 100万個の浮動小数点値を取り、それぞれを二乗し、 元の値を追加します。 すべてのバージョンは同じ値を受け取り、一致する結果を生成します。 値は丸め誤差を競争から排除するのに十分小さいです。 このマシンでは、内包表記は約33ミリ秒かかりました。
0:40 NumPyは1ミリ秒未満でした。 どちらもCPUで実行されました。 これはこのワークロードにとって有用な証拠であり、プログラミング言語にとってひどい死亡記事です。通常のループから始めましょう。 Pythonは次の値を取得し、演算を実行し、結果を追加します。 それからそれをもう一度行います。
Pythonループは何の作業を繰り返すのか?
0:53 乗算は安価です。 次の乗算を準備し、その周りのPythonオブジェクトを処理することは、 毎回作業を追加します。 これはテストされた関数の実際の逆アセンブリです。 イテレーション、乗算、加算はバイトコード命令として現れます。 Pythonのバージョンとインタプリタは異なる選択をします。 これらのタイミングは画面上のバージョン、Python 3.10に属します。 Pythonのリストはオブジェクトへの参照を格納するため、異なる種類のものが
1:14 リストを共有できます。 私たちの算術ジョブは全体を通して数値を使用します。 反復的なサービスを要求しながら、一般的な配置に費用を支払っています。 同じコーヒーを100万回注文し、カップごとに新しい用紙に記入するのを 想像してみてください。 コーヒーマシンは問題ありません。 事務処理が独自のキャリアを築いてしまいました。 NumPyは契約を変更します。
1:36 この配列は、1つの宣言された型を持つ固定サイズの浮動小数点値を保持します。
NumPyはループをどこに移動させるのか?
1:38 私たちの配列は連続しているため、その値はメモリ内でまとまって配置されています。 ビューは他のレイアウトを持つことができます。 算術演算は、受け取るデータの種類を知っています。 呼び出しはバッチをネイティブ操作に渡します。 コンパイルされたコード内では、ループは値をまだ訪問します。 その作業全体で型に関する知識を再利用できます。 Pythonは依然としてジョブを引き渡す人です。 Itamar Turner TrauringはPython Speedでこの違いを説明しています。
2:00 バッチインターフェース、ネイティブ実装、プロセッサのベクトル命令は 別々のものです。関数名はほとんど証拠を提供しません。 この結果はCPU上でNumPyを使用しました。 CuPyのようなGPUバックエンドは、配列と操作に異なる場所を選択します。
速いとはGPUや多くのCPUコアを意味するのか?
2:13 GPUを購入しても、この通常のPythonループはすでに実行されていた場所で実行され続けます。 CPUベクトル命令は複数の値をまとめて処理できます。 NumPyにはサポートされているカーネルを選択するためのメカニズムがあります。 私たちのランタイムは利用可能な機能を報告しますが、この式の命令はトレースしません。 測定されたゲイン全体をベクトル命令に与えるのは、証拠を過大評価することになります。 GPUの買い物かごは閉じたままで大丈夫です。 購入をキャンセルするようなパフォーマンス修正には、非常に満足できるものがあります。 残念ながら、ベンチマークは続きます。
2:40 8つの値では、内包表記が勝ちました。 配列機構を呼び出すコストは、ごくわずかなジョブよりも高かったのです。 より大きな準備された配列がリードしました。
驚くべき高速化はいつ消えるのか?
2:49 私たちは繰り返しの試行とウォームアップされたプロセスを使用しました。 新しいPythonビルドはクロスオーバーを変更する可能性があります。 次に、アプリケーションが必要とするものを変更します。 Pythonリストで開始し、新しいPythonリストで終了します。 NumPyは入力を配列に変換し、結果をPythonオブジェクトに戻す必要があります。 アプリケーションがそれらを必要とする場合、それらのステップは請求書に含まれます。 100万個の値の場合、その完全なパスは約28ミリ秒かかりました。 内包表記も約28ミリ秒でした。
3:11 劇的なリードはほぼ同点になりました。 速かった部分は本物でした。 その利点は、データを数値形式のままにしておくことにかかっていました。 データを追跡してください。 次の操作が配列を受け入れる場合、配列を維持します。 リストと配列の間で繰り返し変換する場合、
どの境界をベンチマークに含めるべきか?
3:28 それらの変換を測定してください。 高価な引き渡し前に停止すると、間違った質問に対して正確な数値が 得られます。この配列を乗算すると一時的な結果が生成されます。 元の値を追加すると出力が生成されます。 ネイティブ実行でもメモリが必要で、データを移動します。 一部の操作は出力バッファを受け入れ、コードが安全に再利用できる場合は 割り当てを減らします。 1つのきれいな行で複数の大きなバッファを注文できます。
3:47 Python Speedもこの問題を指摘しています。 アロケーターはあなたの美意識に驚くほど無関心です。 ループ構文を維持し、その実行を変更することもできます。 Numbaは、サポートされている数値Python関数をネイティブコードにコンパイルできます。 これは、コンパイラデコレータ付きのテストからのループです。
コンパイルされたPythonループも速いのか?
4:00 まだ「for」と書かれています。 高価なインタプリタパスは変更されました。 コンパイル後、このループは準備された配列で0.5ミリ秒未満かかりました。 この実行では、コンパイル自体に約1秒かかりました。 そのセットアップコストは繰り返しのタイミングから除外されたため、 小さな一度きりのジョブには別の比較が必要です。 Numbaのガイドは明示的なループを示すことを喜んでいます。 ホットパスを見つける前に言語移行を開始するのは、
4:22 プロファイラーを開くことを避けるための手の込んだ方法です。 まず、何が時間を消費しているのかを見つけてください。 ネットワーク待機、オブジェクト解析、一括演算には異なる修正が必要です。 計算とデータが適合する場合、ネイティブ配列操作が役立ちます。 呼び出し元が実際に必要とする結果を測定してください。 さて、あのショートカットについてです。 Python関数があり、それに配列を受け入れさせたいとします。 vectorizeというラッパーは、足りないパフォーマンスボタンのように聞こえます。
4:43 そのドキュメントには、重要な文が待っています。 NumPyは、vectorizeラッパーが主に便宜のために提供されていると言っています。 その実装は本質的にforループです。 ラッパーはバッチを受け入れますが、連続する要素に対してPython関数を呼び出し続けます。 インターフェースは短くなりました。
np.vectorizeは何を実際に実行するのか?
4:56 実行境界は忙しいままでした。 Pythonコールバックを通して同じ値が戻ってくるのを見てください。 準備された配列の場合、このバージョンは約87ミリ秒かかりました。 それは私たちの内包表記よりも遅かったです。 便利なブロードキャストラッパーは、削除しようとしていた正確な要素ごとの作業を 維持する可能性があります。 バッチインターフェースの背後にある実装を確認し、互換性のあるデータをその 有用な形式に保ち、比較に変換を含めてください。
5:18 ネイティブ、コールバック、コンパイルされたループは、呼び出し元の視点からは似て見えるかもしれません。 vectorizeという名前は、あなたのストップウォッチが決して署名しなかった約束をしました。 便宜が必要な場合はそれを保持してください。 パフォーマンスのために、実行されるループを確認してください。 私の評決は、測定された数値ホットパスのバッチ処理はSHIP ITです。 通常のループがすでにうまく機能している場合は、それを維持してください。 作業がどこで行われているか、そして次の呼び出し元が何を必要としているかを確認してください。 購読し、ベルを鳴らして、もしあなたが違うスタンプを押すならコメントで教えてください。
コードのどこを変更すべきか?
5:41 そして、今日の違いはこれです。 AxrisiのNikoです。 責任を持ってマージしてください。 購読し、ベルを鳴らして、もしあなたが違うスタンプを押すならコメントで教えてください。 そして、今日の違いはこれです。 AxrisiのNikoです。 責任を持ってマージしてください。
情報源
- Universal functions (ufunc) basics — NumPy 2.2NumPy
- The N-dimensional array — NumPy 2.2NumPy
- numpy.vectorize — convenience and performance notesNumPy
- CPU/SIMD optimizations — NumPy 2.2NumPy
- dis — Disassembler for Python bytecodePython Software Foundation
- CPython 3.10.12 list representationCPython contributors
- The limits of Python vectorization as a performance techniqueItamar Turner-Trauring / Python⇒Speed
- Performance TipsNumba
- Basics of CuPyCuPy



