py-spy の優れた新機能の1つは、C、C ++、Cythonなどの言語で記述されたネイティブPython拡張機能をプロファイルする機能です。

 

py-spy の優れた新機能の1つは、C、C ++、Cythonなどの言語で記述されたネイティブPython拡張機能をプロファイルする機能です。

他のほとんどすべてのPythonプロファイラー[1]は、純粋なPythonコードのプログラムアクティビティのみを表示し、代わりにネイティブコードは、ネイティブ関数を呼び出すPythonの行に時間を費やしているように表示されます。perfのようなネイティブプロファイリングツールを使用すると、物事のネイティブ側で何が起こっているのかを知ることができますが、Python関数呼び出しで何が起こっているのかを把握できなくなります。

これに関する大きな問題は、Pythonエコシステムの大部分がネイティブ拡張機能にあることです。プロファイリング後にPythonプログラムの最も遅い部分をCythonやC ++などの言語で書き直すのは一般的な最適化パターンであり、ネイティブコードまたはPythonコードのいずれかをプロファイリングできるだけで、何が起こっているのか半分しかわかりません。 Pythonコードベース。

py-spyの0.2リリースでは、<span style="box-sizing: border-box; color: rgb(199, 37, 78); font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 12.6px; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: nowrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(249, 242, 244); border-radius: 4px;"–<native フラグを使用してネイティブ拡張プロファイリングを有効にすると、PythonコードとC ++ / Cythonコードの両方を含むプロファイルが取得されます。例として、これ私の暗黙の推奨ライブラリlastfmの例プロファイリングすることによって生成されたフレームグラフです

Cython、C、Pythonの両方のコードを示すフレームグラフの例

ネイティブPython拡張機能のプロファイリングに関する問題

私たちが解決しようとしている問題を説明するために、上記のサンプルプログラムをプロファイリングするときのある時点でのPythonコールスタックを見てみましょう。py-spy dumpコマンドを使用すると、Pythonプログラムから単一のスタックトレースを取得できます。これは、メインスレッドでは次のようになります。

Thread 14976 (active)
     fit (implicit/als.py:159)
     calculate_similar_movies (movielens.py:73)
     <module> (movielens.py:112)

ただし、このライブラリはほとんどがCythonコードであり、「fit」呼び出しは最初のCython関数が呼び出される行にあるため、拡張機能の最適化にはまったく役立ちません。

perfなどのネイティブプロファイリングツールを使用すると、すべてのネイティブフレームを表示できますが、コールスタックには、その時点で呼び出されるPython関数の詳細がなく、無関係な情報が散らかっています。例として、同じ時点でのネイティブスタックトレースは次のようになります。

mkl_blas_avx512_xsaxpy(libmkl_avx512.so)
mkl_blas_saxpy(libmkl_intel_thread.so)
saxpy_(libmkl_intel_lp64.so)
__pyx_fuse_0__pyx_f_8implicit_4_als_axpy(暗黙/の_als.cpp:2859)
__pyx_pf_8implicit_4_als_30_least_squares_cg(暗黙/の_als.cpp:15263)
__pyx_fuse_1_0__pyx_pw_8implicit_4_als_31_least_squares_cg(暗黙/の_als.cpp:14740)
__pyx_FusedFunction_call( _reordering.cpython-37m-x86_64-linux-gnu.so)
__Pyx_PyObject_Call(暗黙の/の_als.cpp:40418)
__pyx_pf_8implicit_4_als_4least_squares_cg(暗黙の/の_als.cpp:11553)
__pyx_pw_8implicit_4_als_5least_squares_cg(暗黙の/の_als.cpp:11439)
cfunction_call_varargs(オブジェクト/コール。 c:755)
PyCFunction_Call(Objects / call.c:784)
partial_call_impl.isra.1(Modules / _functoolsmodule.c:187)
partial_call(Modules / _functoolsmodule.c:230)
_PyObject_FastCallKeywords(Objects / call.c:199)
call_function(Python / ceval.c:4619)
_PyEval_EvalFrameDefault(Python / ceval。 c:3139)
_PyEval_EvalCodeWithName(Python / ceval.c:3940)
_PyFunction_FastCallKeywords(Objects / call.c:433)
call_function(Python / ceval.c:4621)
_PyEval_EvalFrameDefault(Python / ceval.c:3110)
_PyEval_EvalCodeWith c:3930)
_PyFunction_FastCallKeywords(Objects / call.c:433)
call_function(Python / ceval.c:4616)
_PyEval_EvalFrameDefault(Python / ceval.c:3139)
_PyEval_EvalCodeWithName(Python / ceval.c:3940)
PyEval_EvalCodeEx(Python / ceval.c:3959)
PyEval_EvalCode(Python / ceval.c:530)
run_mod(Python / pythonrun.c:1036)
PyRun_FileExFlags(Python / pythonrun.c:988)
PyRun_SimpleFileExFlags(Python / pythonrun.c:429)
pymain_run_file(Modules / main.c:428)
pymain_run_filename(Modules / main.c:1627)
pymain_run_python(Modules / main.c:2876)
pymain_main(Modules / main.c:3037)
_Py_UnixMain(Modules / main.c:3073)
__libc_start_main(libc-2.27.so)

pythonコールスタックはここで失われ__PyEval_EvalFrameDefault、Python関数名の代わりにおよび他の内部インタープリターメソッドに置き換えられます。また、Cython関数呼び出しはマングルされ、元の.pyxファイルの行ではなく、生成された.cppファイルの行を表示します。

py-spyの最新リリースでは、<span style="box-sizing: border-box; color: rgb(199, 37, 78); font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 12.6px; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: nowrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(249, 242, 244); border-radius: 4px;"–<nativeオプションを指定してコールスタックをダンプすると、Pythonスタックとネイティブスタックがマージされ、すべてのCython呼び出しがデマングルされ、元のpyxファイルの行番号に置き換えられます。

Thread 14976 (active)
    mkl_blas_avx512_xsaxpy (libmkl_avx512.so)
    mkl_blas_saxpy (libmkl_intel_thread.so)
    saxpy_ (libmkl_intel_lp64.so)
    _als_axpy (implicit/_als.pyx:18)
    _least_squares_cg (implicit/_als.pyx:184)
    _least_squares_cg (implicit/_als.pyx:137)
    least_squares_cg (implicit/_als.pyx:132)
    least_squares_cg (implicit/_als.pyx:130)
    fit (implicit/als.py:159)
    calculate_similar_artists (lastfm.py:79)
    <module> (lastfm.py:161)

この投稿の残りの部分では、これを生成する方法の詳細について説明します。他のプロセスのネイティブスタックを巻き戻し、ネイティブスタックとPythonスタックをマージし、Cython拡張機能のプロファイリングを行う方法です。

ネイティブスタックの取得

ネイティブ拡張機能のプロファイルを作成するために必要なのは、ネイティブコールスタックを取得してから、Pythonコールスタックにマージすることだけです。py-spyの最初のバージョンからPythonスタックを取得するためのコードがすでにあるため、ここでの最初の課題は、ネイティブスタックをマージすることです。ネイティブスタックを取得するために、 WindowsではStackWalk64 APIを使用し、 Linuxではlibunwind-ptraceを使用 しました。これらのAPIはどちらも、別のプロセスで実行されているスレッドのスタックを巻き戻し、巻き戻されるすべてのフレームの命令ポインターと他のCPUレジスターを表示します。これらの方法を使用して、各IPがスタックトレースのフレームに対応する各スレッドの命令ポインターのリストを収集しました。

ただし、命令ポインタのリストを用意するだけでは、それだけではそれほど役に立ちません。ソースコードに対応するファイル名、関数、および行を取得するには、これらのポインタを記号化する必要があります。Windows には、シンボリックを実行するためのSymFromAddr およびSymGetLineFromAddr APIが付属ていますが、Linuxの場合、最終的にはgimlicrateによって提供されるシンボリックを使用しました Gimliは、 シンボルを解決するためにaddr2line機能を使用しているだけでなく、C ++リンカーシンボルをデマングルするためにcpp-demangleクレートを使用しているだけでなく、ここで多くの可能性を秘めた素晴らしいプロジェクトです 

完全にシンボリック化されたネイティブスタックフレームを使用すると、あとはpythonコールスタックにマージするだけで、これらの内部pythonインタープリターメソッドを置き換えることができます。

ネイティブスタックとPythonスタックのマージ

残念ながら、ネイティブスタックとPythonスタックをマージするのは少し注意が必要です。

ネイティブスタックトレースからPythonフレーム情報を直接把握することはできません。Pythonインタープリター内では、この情報はPyFrameObject構造体内に保持され、Pythonフレームの関数名、ファイル名、行番号などの詳細が含まれています。PyFrameObjectが最初のパラメーターとして渡される_PyEval_EvalFrameDefaultのようなネイティブ関数呼び出しを見ることができますが、x64_64呼び出し規約では、最初のパラメーターはスタックの巻き戻しから取得できないRDIレジスターに渡されます。Pythonインタープリターのデバッグ情報も保証されていないため、ネイティブスタックを確認するだけでPyFrameObjectを取得する信頼できる方法がないことを意味します。

したがって、代わりに、ネイティブスタックトレースとPythonスタックトレースの両方を互いに独立して生成し、それらを1つのコヒーレントスタックトレースにマージします。スタックをマージするには、ネイティブスタックの_PyEval_EvalFrame *呼び出しをPythonスタックトレースの関数に置き換え、他の無関係な内部pythonインタープリター呼び出しを取り除き、それ以外はすべてネイティブ関数呼び出しを残します。

これを行う際の大きな問題は、PythonスタックトレースとネイティブスタックトレースのスレッドIDが常に同じであるとは限らないため、どのPythonスタックトレースが各ネイティブスタックトレースに対応するかを把握することです。Unixベースのシステムでは、PythonはOSスレッドIDの代わりに内部でpthreadIDを使用します。スレッドIDのこの違いにより、どのネイティブコールスタックが同等のPythonコールスタックと一致するかを判断するのが困難になります-参加する共通のキーがないためです。Python 3.8ではPython内からOSスレッドIDを取得するためのサポートが追加されていますが、これを別のプログラムから呼び出す簡単な方法はありません。また、バージョン2.3までのPythonのプロファイリングもサポートしています。また、実行中のプロセスにコードを挿入することを避けたかった(たとえば、pthread_self py-spyで実行中のプログラムを変更したくないので、ネイティブスレッドで)ルックアップを実行します。

したがって、代わりに、最上位関数のレジスタを調べて、ネイティブスタックトレースからpthreadIDを検索します。新しいスレッドをフォークした後、pthreadsはpthread IDをネイティブスタックのトップレベルフレームのRBXレジスタに貼り付けます。このレジスタは、巻き戻しから取得できるレジスタの1つです。これはちょっとしたハックですが、かなりうまく機能しているようで、ネイティブスタックとPythonスタックを一緒に結合できます。

ネイティブのthreadidを使用すると、OSにクエリを実行して、スレッドがアイドル状態かどうかを確認することもできます。そのため、新しいバージョンのpy-spyのアイドル検出コードは以前よりもはるかに優れています。以前のヒューリスティックベースの方法がどれほど悪かったかについて人々は文字通りツイッターで笑っていました-したがって、どんな改善も良いものです=)。

Cython拡張機能のプロファイリング

Cython拡張機能のプロファイリングにはいくつかの問題もあります。

最大の問題は、シンボリックが、実際のCythonソースコードを含む元の.pyxファイルではなく、Cythonコンパイラが生成するCまたはC ++ファイルのファイル名と行番号を返すことです。これを克服する最良の方法はemit_linenums=True、cythonize呼び出し中にオプションを使用する ことです。このオプションを設定すると、Cythonは生成されたCまたはC ++ファイルに#lineディレクティブを追加 し、元のCythonファイルの正しい行とファイル名にマップし直します。

このオプションを使用してCython拡張機能を作成していない場合でも、py-spyは、生成されたCファイル自体にCythonが残したコメントから元のファイル名/行番号を取得しようとします。py-spyは、Cythonコンパイラによって生成されたCファイルをロードし、行番号に対応する元のファイル/行番号を持つコメントブロックを見つけて、可能であればそれを使用します。ここでの大きな欠点は、生成されたCファイルが拡張子とともにインストールされないことです。したがって、これは通常、拡張子の開発バージョン(python setup.py developまたはpip install -e .)を実行している場合にのみ機能します。

Cythonのもう1つの問題は、関数名が壊れてしまうことです。これを修正するために、私は基本的なCythonデマングラーを作成しました。これには、おそらく融合関数呼び出しに関する未解決の問題がいくつかありますが、それ以外の点では十分に機能しているようです。

警告

この投稿を締めくくる前に指摘したいネイティブ拡張機能にはいくつかの制限があります。

  • これはx86_64LinuxおよびWindowsでのみ機能します。OSX、FreeBSD、およびARM / i686Linuxはまだサポートされていません

  • 最良の結果を得るには、拡張機能をシンボルでコンパイルする必要があります。シンボル化に失敗し、関数名がエクスポートされない場合は、ポインターアドレスが表示されることがあります。

  • これは、ホストOSからDockerコンテナで実行されているプロファイリングプロセスではまだ機能しません

  • これにより、処理時間が長くなります-特にWindowsの場合

  • Cythonで行番号を検索するには、生成されたC / C ++ファイルを見つける必要があります

最終的な考え

この投稿は、新しいバージョンのpy-spyのリリースのお知らせとしても機能するため、py-spy0.2には他にもたくさんのクールな新機能があることを指摘したいと思います。大きな問題の1つは、Artem Khramovの作業のおかげで、py-spyがFreeBSDで動作するようになったことですpy-spyは、生のプロファイルデータとspeedscope形式のプロファイルの両方を書き出すことで、より多くのファイル形式を書き出すこともできるようになりましたまた、Python 3.8をサポートし、アイドル検出を改善し、ARMプロセッサをサポートし、その他の調整を行います。最後に、このバージョンのpy-spyはMITライセンスの下でリリースされました。

py-spyは私自身はかなり便利だと思いますが、それがどのように機能しているかについてのあなたの考えを聞いてみたいです。見たい機能がある場合は、適切な問題を高く評価するか、 不足している機能を説明する新しい問題を作成してください。賛成票を投票メカニズムとして使用して、py-spyの人々に欠けている機能が最も興味を持っているかどうかを確認します。ネイティブ拡張機能の問題には115の賛成票があり、この機能を完成させる価値があることがわかりました。

脚注1

ここでの注目すべき例外はvmprof-pythonで、ネイティブ拡張機能プロファイリングできることに加えて、pypyインタープリターの下で実行されているpythonプログラムもプロファイリングできます。

https://www.benfrederickson.com/profiling-native-python-extensions-with-py-spy/

コメントを残す

メールアドレスが公開されることはありません。

Next Post

HoudiniHowtos

木 10月 1 , 2020
Youtubeビデオで使用されるHoudiniチュートリアルファイルリポジトリ。