型指定やメモリ管理をしなくてもなんとなく動いてしまうPythonですが、いざメモリ関連の不調にハマってしまった場合に調べる手段を持っておかないと対処することができません。ここではmemory_profilerを使って実行時間とメモリ使用量のプロットを行う調査方法をメモしておきます。
こんにちは。wat(@watlablog)です。メモリ使用量の改善ができるようにまずは計測方法を覚えてみます!
Pythonでメモリ使用量を調査したい状況
大規模データ処理のアルゴリズムを検討する場合
C言語で当たり前に行うメモリ管理を考えないのはデメリットでもある
C言語ではmallocによりメモリを割り当てたり、自分でメモリを管理しないとまともなプログラミングはできません。
一方、Pythonはガベージコレクション(Garbage Collection)により不要になったメモリ領域を自動的に解放してくれるため、初心者のうちはほとんどメモリリーク等の重大なバグを気にしなくてもプログラミングできてしまいます。
Pythonが初心者でも簡単に使える言語と言われる所以がこのあたりにありますが、参入障壁を低くしている分、普段からメモリを意識しないまま先へ進んでしまうのは大きなデメリットでもあります。
MemoryError対策は必要
特にPythonはデータ分析分野で頻繁に使われる言語ですが、近年ではIoTやWebの発展からビッグデータを当たり前に扱うケースも増えてきていると思います。
また科学技術分野では、非常に大きな行列を相手に多様な線形代数の演算を重ねることがよくあります。
メモリリークの心配をあまりしなくて良いといっても、これら大規模データの処理は一般にメモリを多く必要とし、アルゴリズムによる対策をしなければ指数関数的に増えるメモリで計算できなくなってしまうでしょう。
大規模データに対し「メモリを削減するためのアルゴリズムが適切に機能しているか?」を調べるためにはメモリを監視する術を身につけることが有効です。
さらにMemoryErrorが発生した時は、「確かにコード修正でメモリ増大が改善した」というのを定量的に確認したいものです。
memory_profilerを使う理由
結局は好み
Pythonでメモリ関連の情報を調査する方法として、標準ライブラリのtracemallocやpsutilといった外部ライブラリもあります。
数値で定量的にメモリ使用量を監視できるのであればどれを使っても目的は達成できるため、後は好みかも知れません。
僕が直感的に使いやすかったのがmemory_profilerだったので、今回はこのライブラリを紹介します。
一定時間間隔でメモリ使用量を計測できる(波形表示が楽)
memory_profilerは指定した時間間隔でメモリ使用量を取得できるため、matplotlibと簡単に連携可能です。
他のライブラリも可能と思いますが、この分野初心者の自分が特に迷いもせず使えたので多分一般に簡単なのでしょう。
行単位でメモリ使用量を計測できる
処理時間を行単位で計測できるline_profilerと同様に、memory_profilerも行単位でメモリ使用量を計測可能です。
より詳細な改善策を検討する時はこの機能が大変ありがたいものになるでしょう。
line_profilerについては「Pythonプログラムの処理にかかる時間を計測する方法」をご覧ください。
memory_profilerの使い方
PyPIにmemory_profilerの公式情報があります。まずはこちらをご確認ください。
PyPI:https://pypi.org/project/memory-profiler/
Python環境
このページのコードは以下のPython環境で確認しています。ライブラリはpip install可能です。
Python | Python 3.9.6 |
---|---|
PyCharm (IDE) | PyCharm CE 2020.1 |
Numpy | 1.21.1 |
matplotlib | 3.4.3 |
memory-profiler | 0.60.0 |
一定間隔でメモリ使用量を計測して波形表示するコード
これからmemory_profilerを使って一定の時間間隔でメモリ使用量を計測するコード例を示します。
func()という関数に2つの引数を与え、非常に非効率的な計算をさせています。
時間間隔dtを指定し、memory_usage()に関数funcと引数をタプルで渡してメモリ使用量を計測しています。
memory_usageの結果はリストで得られます。このリストに時間間隔を考慮した時間軸xを作成することで、matplotlibによるプロットが可能になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import numpy as np from matplotlib import pyplot as plt from memory_profiler import memory_usage # 計測する関数 def func(a, b): x = np.arange(0, 100, a) y = np.arange(0, 100, b) matrix = np.zeros([len(x), len(y)]) # 適当で非効率な計算ループ for i in range(len(x)): for j in range(len(y)): matrix[i, j] = x[i] * y[j] # プロットする関数 def plot(x, y): # ここからグラフ描画 # フォントの種類とサイズを設定する。 plt.rcParams['font.size'] = 14 plt.rcParams['font.family'] = 'Times New Roman' # 目盛を内側にする。 plt.rcParams['xtick.direction'] = 'in' plt.rcParams['ytick.direction'] = 'in' # グラフの上下左右に目盛線を付ける。 fig = plt.figure() ax1 = fig.add_subplot(111) ax1.yaxis.set_ticks_position('both') ax1.xaxis.set_ticks_position('both') # 軸のラベルを設定する。 ax1.set_xlabel('Time [s]') ax1.set_ylabel('Memory [MB]') ax1.plot(x, y, lw=1, color='red', label='Memory usage') # レイアウトと凡例の設定 fig.tight_layout() plt.legend() # グラフを表示する。 plt.show() plt.close() return # memory_profilerを使う時はこの形で書かないといけない if __name__ == '__main__': # 引数 a = 1 b = 1 # 一定間隔dtでメモリを計測しリストにする dt = 0.01 memory = memory_usage((func, (a, b)), interval=dt) # matplotlibでプロットする x = np.arange(0, len(memory), 1) * dt plot(x, memory) |
また、メインは正式に「if name == 'main':」と書いています。
当WATLABブログではあまりこの表現を使っていませんでしたが、memory_profilerを使う時にはこのように書かないと以下のエラーが発生します。
今回はエラー回避の意味で書いています。
RuntimeError:
Error
An attempt has been made to start a new process before the
current process has finished its bootstrapping phase.
This probably means that you are not using fork to start your child processes and you have forgotten to use the proper idiom in the main module: if __name__ == '__main__': freeze_support() ... The "freeze_support()" line can be omitted if the program is not going to be frozen to produce an executable.
実行結果を以下に示します。この方法でプロットすると時間の情報とメモリ使用量を一度に確認できます。
行単位でメモリ使用量を計測するコード
@profileというデコレータを関数の手前に付けることで行単位のメモリ使用量を計測することができます。
デコレータについては以下がわかりやすいと思います。
Qiita:Pythonのデコレータについて
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import numpy as np from memory_profiler import profile # 計測する関数 @profile def func(a, b): x = np.arange(0, 100, a) y = np.arange(0, 100, b) matrix = np.zeros([len(x), len(y)]) # 適当で非効率な計算ループ for i in range(len(x)): for j in range(len(y)): matrix[i, j] = x[i] * y[j] # memory_profilerを使う時はこの形で書かないといけない if __name__ == '__main__': # 引数 a = 1 b = 1 # 関数を実行 func(a, b) |
こちらが結果です。行番号やメモリ使用量と共に、各行のコードも書いてありわかりやすいですね。ただし、このコードだとちょっと例としてはよくないかも。
1 2 3 4 5 6 7 8 9 10 11 |
Line # Mem usage Increment Occurrences Line Contents ============================================================= 6 48.8 MiB 48.8 MiB 1 @profile 7 def func(a, b): 8 48.8 MiB 0.0 MiB 1 x = np.arange(0, 100, a) 9 48.8 MiB 0.0 MiB 1 y = np.arange(0, 100, b) 10 48.8 MiB 0.0 MiB 1 matrix = np.zeros([len(x), len(y)]) 11 12 # 適当で非効率な計算ループ 13 48.8 MiB 0.0 MiB 101 for i in range(len(x)): 14 48.8 MiB 0.0 MiB 10100 for j in range(len(y)): |
まとめ
このページではPythonコードのメモリ使用量計測にmemory_profilerを使ってみた例を紹介しました。
一定間隔、行単位で計測する方法はコード改善の定量評価に良さそうです。
ここでは紹介しませんでしたが、公式ページにはmprofコマンドを使って.pyスクリプトごとメモリ調査する方法等、色々な方法が紹介されていますので、是非公式ページもご覧ください。
普段メモリをあまり気にすることがなかったPythonistaの方は是非これを機に使ってみてください。
これでPythonでもメモリに配慮できるようになりました!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!