これは、なにをしたくて書いたもの?
ちょっと、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')
引数を指定していますが、指定できるのはこちら。
他には、「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)は、このようになります。
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)
デフォルトで、よりメモリを使用するようになったオブジェクトのトップ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
これらのツールを使うと、割と簡単にメモリ使用量の増加傾向などがわかるので、良いですね。
必要に応じて使っていきましょう。