CLOVER🍀

That was when it all began.

Pythonプログラムのメモリの使用状況をトレースする

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

ちょっと、Pythonプログラムで使用しているメモリの状況を追ってみようかなと。

Python標準ライブラリにtracemallocというものがあるので、こちらを利用できます。またobjgraphというものを使えば、
オブジェクトの参照も見れるようです。

このあたりを試してみましょう。

なお、次のようなものもあるようですが、今回はパス。

GitHub - fabianp/memory_profiler: Monitor Memory usage of Python code

GitHub - jmdana/memprof: A memory profiler for Python. As easy as adding a decorator!

GitHub - nvdv/vprof: Visual profiler for Python

環境

今回の環境は、こちら。

$ python3 -V
Python 3.6.7

tracemalloc

Python標準ライブラリに入っている、Pythonが割り当てたメモリをトレースするためのデバッグツールだそうです。

27.7. tracemalloc --- メモリ割り当ての追跡 — Python 3.6.8 ドキュメント

以下の情報が取得できるようです。

  • オブジェクトが割り当てられた場所のトレースバック
  • ファイル名、行ごとに割り当てられたメモリの統計情報
  • スナップショットを取ることができ、その差分の検出

今回は、メモリの統計情報を取得してみます。まあ、ドキュメント通りです。

サンプルプログラムは、こちら。ちょっと大きめのリストを作ったりしています。
heavy-memory.py

import tracemalloc

tracemalloc.start()

a_list = [ x for x in range(1000000) ]
b_list = [ x for x in range(10000) ]

snapshot = tracemalloc.take_snapshot()

top_stats = snapshot.statistics('lineno')

print('[ Top 10 ]')
for stat in top_stats[:10]:
    print(stat)


c_list = [ x for x in range(100000) ]

snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot, 'lineno')

print('[ Top 10 Difference]')
for stat in top_stats[:10]:
    print(stat)

top_stats = snapshot2.statistics('lineno')

print('[ Top 10 ]')
for stat in top_stats[:10]:
    print(stat)

tracemallocは、次の1行でメモリへのトレースを開始します。

tracemalloc.start()

引数に保存するフレーム数を指定することができますが、トレースバックの情報を見たい時以外には増やす意味がないそうな。

次に、適当なところでスナップショットを取ります。スナップショットは、tracemalloc#start以前の割り当てについての情報は
含まれません。

snapshot = tracemalloc.take_snapshot()

このスナップショットからは、統計情報が取得できます。

top_stats = snapshot.statistics('lineno')

引数を指定していますが、指定できるのはこちら。

statistics

他には、「filename」、「traceback」が指定できます。

そして、より多くのメモリを消費しているトップ10を表示します。といっても、割り当てているものはとても少ないですが。

print('[ Top 10 ]')
for stat in top_stats[:10]:
    print(stat)

また、さらにスナップショットを取り、その差分の統計情報を取得することもできます。

snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot, 'lineno')

print('[ Top 10 Difference]')
for stat in top_stats[:10]:
    print(stat)

最後に、2つ目のスナップショットから、再度トップ10を表示してみましょう。

top_stats = snapshot2.statistics('lineno')

print('[ Top 10 ]')
for stat in top_stats[:10]:
    print(stat)

と、このあたりで実際に動かしてみましょう。結果は、こちら。

$ python3 heavy-memory.py 
[ Top 10 ]
heavy-memory.py:5: size=35.0 MiB, count=999745, average=37 B
heavy-memory.py:6: size=352 KiB, count=9744, average=37 B
[ Top 10 Difference]
heavy-memory.py:17: size=3533 KiB (+3533 KiB), count=99745 (+99745), average=36 B
/usr/lib/python3.6/tracemalloc.py:207: size=960 B (+960 B), count=3 (+3), average=320 B
/usr/lib/python3.6/tracemalloc.py:165: size=864 B (+864 B), count=2 (+2), average=432 B
/usr/lib/python3.6/tracemalloc.py:387: size=728 B (+728 B), count=6 (+6), average=121 B
/usr/lib/python3.6/tracemalloc.py:497: size=680 B (+680 B), count=1 (+1), average=680 B
/usr/lib/python3.6/tracemalloc.py:469: size=656 B (+656 B), count=4 (+4), average=164 B
/usr/lib/python3.6/tracemalloc.py:524: size=632 B (+632 B), count=4 (+4), average=158 B
/usr/lib/python3.6/tracemalloc.py:462: size=536 B (+536 B), count=3 (+3), average=179 B
heavy-memory.py:14: size=536 B (+536 B), count=2 (+2), average=268 B
/usr/lib/python3.6/tracemalloc.py:192: size=504 B (+504 B), count=2 (+2), average=252 B
[ Top 10 ]
heavy-memory.py:5: size=35.0 MiB, count=999745, average=37 B
heavy-memory.py:17: size=3533 KiB, count=99745, average=36 B
heavy-memory.py:6: size=352 KiB, count=9744, average=37 B
/usr/lib/python3.6/tracemalloc.py:207: size=960 B, count=3, average=320 B
/usr/lib/python3.6/tracemalloc.py:165: size=864 B, count=2, average=432 B
/usr/lib/python3.6/tracemalloc.py:387: size=728 B, count=6, average=121 B
/usr/lib/python3.6/tracemalloc.py:497: size=680 B, count=1, average=680 B
/usr/lib/python3.6/tracemalloc.py:469: size=656 B, count=4, average=164 B
/usr/lib/python3.6/tracemalloc.py:524: size=632 B, count=4, average=158 B
/usr/lib/python3.6/tracemalloc.py:462: size=536 B, count=3, average=179 B

最初のトップ10、

[ Top 10 ]
heavy-memory.py:5: size=35.0 MiB, count=999745, average=37 B
heavy-memory.py:6: size=352 KiB, count=9744, average=37 B

差分、

[ Top 10 Difference]
heavy-memory.py:17: size=3533 KiB (+3533 KiB), count=99745 (+99745), average=36 B
/usr/lib/python3.6/tracemalloc.py:207: size=960 B (+960 B), count=3 (+3), average=320 B
/usr/lib/python3.6/tracemalloc.py:165: size=864 B (+864 B), count=2 (+2), average=432 B
/usr/lib/python3.6/tracemalloc.py:387: size=728 B (+728 B), count=6 (+6), average=121 B
/usr/lib/python3.6/tracemalloc.py:497: size=680 B (+680 B), count=1 (+1), average=680 B
/usr/lib/python3.6/tracemalloc.py:469: size=656 B (+656 B), count=4 (+4), average=164 B
/usr/lib/python3.6/tracemalloc.py:524: size=632 B (+632 B), count=4 (+4), average=158 B
/usr/lib/python3.6/tracemalloc.py:462: size=536 B (+536 B), count=3 (+3), average=179 B
heavy-memory.py:14: size=536 B (+536 B), count=2 (+2), average=268 B
/usr/lib/python3.6/tracemalloc.py:192: size=504 B (+504 B), count=2 (+2), average=252 B

最終的なトップ10。

[ Top 10 ]
heavy-memory.py:5: size=35.0 MiB, count=999745, average=37 B
heavy-memory.py:17: size=3533 KiB, count=99745, average=36 B
heavy-memory.py:6: size=352 KiB, count=9744, average=37 B
/usr/lib/python3.6/tracemalloc.py:207: size=960 B, count=3, average=320 B
/usr/lib/python3.6/tracemalloc.py:165: size=864 B, count=2, average=432 B
/usr/lib/python3.6/tracemalloc.py:387: size=728 B, count=6, average=121 B
/usr/lib/python3.6/tracemalloc.py:497: size=680 B, count=1, average=680 B
/usr/lib/python3.6/tracemalloc.py:469: size=656 B, count=4, average=164 B
/usr/lib/python3.6/tracemalloc.py:524: size=632 B, count=4, average=158 B
/usr/lib/python3.6/tracemalloc.py:462: size=536 B, count=3, average=179 B

あまり対象がないのと、差分比較の時にはtracemalloc自身も含まれるようになっています…。

とはいえ、割と簡単に取得できて良いですね。

ところで、オーバーヘッドは?と。

ソースコードは省略しますが、tracemallocの部分を全部コメントアウトして、実行。

$ time python3 heavy-memory.py 

real    0m0.077s
user    0m0.073s
sys 0m0.004s

次に、tracemallocを有効にして再度測定。

$ time python3 heavy-memory.py 

〜省略〜

real    0m4.084s
user    0m4.000s
sys 0m0.084s

あ、だいぶ遅くなりましたね…。

APIの使い方自体はそう難しくないですが、オーバーヘッドはやはり気になるのと、ソースコードを変更しなくては
いけないところがややネックでしょうか。

標準で使えるところは、ポイントかなーと思います。

objgraph

objgraphは、Pythonのオブジェクトのメモリ参照を可視化してくれたり、メモリリークの検出に役立つツールです。

GitHub - mgedmin/objgraph: Visually explore Python object graphs

ドキュメントは、こちら。

Python Object Graphs — objgraph 3.4.0 documentation

まずはインストール。

$ pip3 install objgraph

バージョン。

$ pip3 freeze
...
objgraph==3.4.0

簡単に、サンプルを。
memory-object-graph.py

import objgraph

a_list = [ x for x in range(1000000) ]

dict = { 'list': a_list, 'string': 'Hello Python!' }

b_list = [ x for x in range(10000) ]

objgraph.show_refs([ a_list, dict, b_list ], filename = 'sample.png')

print('[ show most common type ]')
objgraph.show_most_common_types()

print('[ show growth, top 10 ]')
objgraph.show_growth(limit = 10)

c_list = [ x for x in range(100000) ]

print('[ show growth, top 10 ]')
objgraph.show_growth(limit = 10)

実行した結果を見つつ、簡単に解説。

$ python3 memory-object-graph.py

objgraph#show_refsで、指定したオブジェクトの参照グラフを得ることができます。

objgraph.show_refs([ a_list, dict, b_list ], filename = 'sample.png')

今回は、こんなオブジェクトを指定しているので

a_list = [ x for x in range(1000000) ]

dict = { 'list': a_list, 'string': 'Hello Python!' }

b_list = [ x for x in range(10000) ]

出力されたファイル(sample.png)は、このようになります。

f:id:Kazuhira:20190419002127p:plain

objgraph#show_most_common_typesでは、メモリ内のオブジェクトの概況を簡単に得ることができます。

print('[ show most common type ]')
objgraph.show_most_common_types()

objgraph.show_most_common_types

なお、引数limitで出力する数を制御することができます(デフォルトでは10)。

[ show most common type ]
function                   1990
dict                       1089
wrapper_descriptor         998
tuple                      874
weakref                    773
method_descriptor          732
builtin_function_or_method 695
getset_descriptor          378
set                        344
list                       302

続いて、objgraph.show_growthでは、オブジェクトの増加を見ることができます。

print('[ show growth, top 10 ]')
objgraph.show_growth(limit = 10)

objgraph.show_growth

デフォルトで、よりメモリを使用するようになったオブジェクトのトップ10を表示します。今回は、limitを明示しています。

[ show growth, top 10 ]
function                       1990     +1990
dict                           1089     +1089
wrapper_descriptor              998      +998
tuple                           852      +852
weakref                         773      +773
method_descriptor               732      +732
builtin_function_or_method      695      +695
getset_descriptor               378      +378
set                             344      +344
list                            302      +302

リストを作成して、もう1度objgraph.show_growthを実行すると

c_list = [ x for x in range(100000) ]

print('[ show growth, top 10 ]')
objgraph.show_growth(limit = 10)

増えた差分が結果として得られます。

[ show growth, top 10 ]
list      303        +1

これらのツールを使うと、割と簡単にメモリ使用量の増加傾向などがわかるので、良いですね。

必要に応じて使っていきましょう。