CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: memory_profiler でプログラムのメモリ使用量を調べる

今回は memory_profiler というモジュールを使ってプログラムのメモリ使用量を調べる方法について紹介する。

pypi.python.org

このブログでは、以前に Python のプロファイラとして profile/cProfile や line_profiler について書いたことがある。 これまでに紹介したこれらのプロファイラは、主に時間計算量の調査が目的となる。 それに対して memory_profiler では、調べる対象は空間計算量となる。

blog.amedama.jp

blog.amedama.jp

使った環境は以下の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1212
$ python --version
Python 3.6.4

下準備

まずは memory_profiler をインストールする。

$ pip install memory_profiler

スクリプトから memory_profiler を使う

まずは最も基本的な、スクリプトから memory_profiler を使う方法について。

memory_profiler では特定の関数のメモリ使用量をプロファイリングするのに @profile デコレータが使える。 例えば次のサンプルコードでは my_func() 関数を @profile デコレータでプロファイル対象としてマークしている。 関数の内容は変数の入った大きなリストを作って、それを del 文で削除するというものになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from memory_profiler import profile


@profile
def my_func():
    # 整数の入った大きなリストを用意する
    a = [0] * (2 * 10 ** 7)
    # 変数を削除する
    del a
    # 先ほどより少し小さなリストを用意する
    b = [0] * (10 ** 6)
    # 変数を削除する
    del b


def main():
    my_func()


if __name__ == '__main__':
    main()

上記に適当な名前をつけたら、あとは普通に Python のスクリプトとして実行するだけ。 すると、標準出力にプロファイル結果が出力される。 出力内容は、左から「プログラムの行数、その行が評価された時点でのメモリ使用量、その行が評価されたことによる使用量の増減、対応するコード」となっている。

$ python example.py 2>/dev/null
Filename: example.py

Line #    Mem usage    Increment   Line Contents
================================================
     7     36.1 MiB     36.1 MiB   @profile
     8                             def my_func():
     9                                 # 整数の入った大きなリストを用意する
    10    188.7 MiB    152.6 MiB       a = [0] * (2 * 10 ** 7)
    11                                 # 変数を削除する
    12     36.1 MiB   -152.6 MiB       del a
    13                                 # 先ほどより少し小さなリストを用意する
    14     43.8 MiB      7.6 MiB       b = [0] * (10 ** 6)
    15                                 # 変数を削除する
    16     43.8 MiB      0.0 MiB       del b

上記を見ると興味深いことが分かる。 最初の変数 a は del 文を発行することで GC が実行されたのか、メモリ使用量は減っている。 それに対し変数 b では del 文を発行してもメモリ使用量は変化していない。

IPython から memory_profiler を使う

続いては IPython からアドホックに memory_profiler を使ってみる。 おそらく、実際のプロファイリングではこの方法を使うことが多いだろう。

まずは IPython をインストールしておく。

$ pip install ipython

先ほどと、ほぼ同じ内容のサンプルコードを用意する。 違いは my_func() 関数に @profile デコレータが付与されていないところだ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def my_func():
    a = [0] * (2 * 10 ** 7)
    del a
    b = [0] * (10 ** 6)
    del b


def main():
    my_func()


if __name__ == '__main__':
    main()

上記で用意した example.py を IPython に読み込ませながら起動する。

$ ipython -i example.py

ちなみに、上記で起動と同時にモジュールを読み込ませているのは手順を省くためだけ。 単独で IPython を起動した後に my_func 関数をインポートしても、もちろん構わない。

$ ipython
...
In [1]: from example import my_func

memory_profiler の IPython 拡張を読み込む。

In [1]: %load_ext memory_profiler

あとは %mprun マジックコマンド経由で my_func() 関数を実行する。 これで、先ほどスクリプトから実行したのと同じ内容が得られる。

In [2]: %mprun -f my_func my_func()
Filename: /Users/amedama/Documents/temporary/example.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     49.2 MiB     49.2 MiB   def my_func():
     6    201.8 MiB    152.6 MiB       a = [0] * (2 * 10 ** 7)
     7     49.2 MiB   -152.6 MiB       del a
     8     49.2 MiB      0.0 MiB       b = [0] * (10 ** 6)
     9     49.2 MiB      0.0 MiB       del b

処理内容が一行で収まるときは %memit マジックコマンドも便利だ。

In [3]: %memit list(range(10 ** 6))
peak memory: 86.92 MiB, increment: 27.92 MiB

In [4]: %memit list(range(10 ** 7))
peak memory: 437.59 MiB, increment: 378.59 MiB

プロファイル結果を matplotlib で折れ線グラフにプロットする

memory_profiler は matplotlib と連携してプロファイル結果をプロットする機能もある。

そのために、まずは matplotlib をインストールしておこう。

$ pip install matplotlib

サンプルコードを mprof run コマンド経由で実行する。

$ mprof run example.py
mprof: Sampling memory every 0.1s
running as a Python program...

実行が完了したら mprof plot コマンドでプロットされた結果を確認する。

$ mprof plot
Using last profile data.

こんな感じで結果が確認できる。 f:id:momijiame:20180204001410p:plain

ちなみにプロファイル結果は履歴が残る。 履歴は mprof list コマンドで確認できる。

$ mprof list
0 mprofile_20180203230956.dat 23:09:56 03/02/2018

インデックスを指定すれば過去の実行結果のプロットが確認できる。

$ mprof plot 0

履歴を削除するにはインデックスを指定して mprof rm コマンドを実行する。

$ mprof rm 0

あるいは、全ての履歴を削除したいなら mprof clean コマンドを使っても構わない。

$ mprof clean

めでたしめでたし。