CLOVER🍀

That was when it all began.

line_profilerで、Pythonのプログラムを行単位でプロファイリングする

これは、なにをしたくて書いたもの?

Pythonのプログラムを関数の行単位でプロファイリングしてくれるモジュール、line_profilerというものがあるらしいので、
試してみようかなと。

GitHub - rkern/line_profiler: Line-by-line profiling for Python

環境

今回の環境は、こちらです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.2 LTS
Release:    18.04
Codename:   bionic


$ python3 -V
Python 3.6.7

line_profilerを使ってみる

まずは、line_profilerをインストール。

$ python3 -m venv venv
$ . venv/bin/activate
$ pip3 install line_profiler

バージョン。

$ pip3 freeze
...
line-profiler==2.1.2
...

こんなサンプルプログラムを用意。
line_profiler_example.py

import time

def heavy_loop():
    for i in range(5000000):
        loop10()

    for i in range(10000):
        loop10()

    for i in range(10):
        time.sleep(0.1)

    print("executed loop")

def loop10():
    for i in range(10):
        pass

heavy_loop()

実行すると、3秒近くかかるスクリプトです。

$ time python3 line_profiler_example.py 
executed loop

real    0m2.805s
user    0m1.799s
sys 0m0.004s

line_profilerを使うには、プロファイリング対象の関数に「@profile」デコレーターを付与します。

@profile
def heavy_loop():
    for i in range(5000000):
        loop10()

    for i in range(10000):
        loop10()

    for i in range(10):
        time.sleep(0.1)

    print("executed loop")

line_profilerをインストールすると、kernprofというコマンドが使えるようになっています。

$ kernprof -h
Usage: kernprof [-s setupfile] [-o output_file_path] scriptfile [arg] ...

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -l, --line-by-line    Use the line-by-line profiler from the line_profiler
                        module instead of Profile. Implies --builtin.
  -b, --builtin         Put 'profile' in the builtins. Use 'profile.enable()'
                        and 'profile.disable()' in your code to turn it on and
                        off, or '@profile' to decorate a single function, or
                        'with profile:' to profile a single section of code.
  -o OUTFILE, --outfile=OUTFILE
                        Save stats to <outfile>
  -s SETUP, --setup=SETUP
                        Code to execute before the code to profile
  -v, --view            View the results of the profile in addition to saving
                        it.

この「kernprof」コマンドの「-l」オプションを指定して、プログラムを実行します。「-l」は、@profileデコレータを付けた関数に
対して、行単位のプロファイリングを有効にするオプションです。

$ kernprof -l line_profiler_example.py 
executed loop
Wrote profile results to line_profiler_example.py.lprof

ちなみに、実行時間は17秒ほどになりました…。

$ time kernprof -l line_profiler_example.py 
executed loop
Wrote profile results to line_profiler_example.py.lprof

real    0m17.048s
user    0m16.018s
sys 0m0.029s

結果は、実行ディレクトリに「スクリプト名.lprof」というファイルができているので、こちらをline_profilerモジュールで見ると
プロファイリング結果として確認することができます。

$ python3 -m line_profiler line_profiler_example.py.lprof 
Timer unit: 1e-06 s

Total time: 13.1252 s
File: line_profiler_example.py
Function: heavy_loop at line 3

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     3                                           @profile
     4                                           def heavy_loop():
     5   5000001    1557939.0      0.3     11.9      for i in range(5000000):
     6   5000000   10541423.0      2.1     80.3          loop10()
     7                                           
     8     10001       3093.0      0.3      0.0      for i in range(10000):
     9     10000      20840.0      2.1      0.2          loop10()
    10                                           
    11        11         54.0      4.9      0.0      for i in range(10):
    12        10    1001838.0 100183.8      7.6          time.sleep(0.1)
    13                                           
    14         1         59.0     59.0      0.0      print("executed loop")

読み方は、以下です。

  • Line … ファイルの行番号
  • Hits … 対象の行の実行回数
  • Time … 実行に費やされた合計時間。単位は、結果に一緒に表示されている「Timer unit」を確認してください
  • Per Hit … 1回の実行の平均時間
  • % Time … プロファイリングの合計時間に対する、対象の行の実行時間の割合
  • Line Contents … 実際のソースコード。プロファイリングの結果から表示しているわけではなく、現在置かれているソースコードから読み出して表示しています

「% Time」を最初に見て、実行に時間がかかっている割合の大きいところを目安に見ていくとよさそうですね。

ちなみに「-v」オプションを付けると、いきなり結果を表示します(lprofファイルは作成されます)。

$ kernprof -l -v line_profiler_example.py 
executed loop
Wrote profile results to line_profiler_example.py.lprof
Timer unit: 1e-06 s

Total time: 13.1355 s
File: line_profiler_example.py
Function: heavy_loop at line 3

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     3                                           @profile
     4                                           def heavy_loop():
     5   5000001    1533587.0      0.3     11.7      for i in range(5000000):
     6   5000000   10576503.0      2.1     80.5          loop10()
     7                                           
     8     10001       3032.0      0.3      0.0      for i in range(10000):
     9     10000      20567.0      2.1      0.2          loop10()
    10                                           
    11        11         53.0      4.8      0.0      for i in range(10):
    12        10    1001730.0 100173.0      7.6          time.sleep(0.1)
    13                                           
    14         1         48.0     48.0      0.0      print("executed loop")