アプリケーションの開発を行うエンジニアが、開発の過程でよく遭遇する問題の一つであるメモリリーク。
エンジニアの皆さんはご存知の通り、メモリリークとはアプリケーションがメモリを適切に解放せず不要なメモリを占有し続ける現象を指しますが、放置してしまうとアプリケーションのパフォーマンスが低下し、最悪の場合はクラッシュしてしまうことも。
ですので、早めに根本的な原因を特定し解決することが必要ですね。

今回のブログでは、Pythonアプリケーションの開発中にメモリリークが発生した際に行った、メモリ使用量等の測定方法についてご紹介します。
みなさまの参考になれば幸いです。

 

メモリリークとは?

メモリリークとは、プログラムが動的にメモリを割り当てたあと、メモリが適切に開放されない場合に発生します。メモリリークによって、メモリが不足すると、アプリケーションの動作が不安定になったり、クラッシュやフリーズを引き起こすことがあります。

Pythonでは、メモリ管理を自動で行ってくれる仕組みがありますが、メモリリークが発生する場合もあります。

Pythonのガベージコレクションについて

Pythonには、ガベージコレクション(GC)という不要なメモリリソースを自動的に解放する仕組みがあり、プログラマが手動でメモリ管理を行う必要がありません。
Pythonには大きく分けて、2つのガベージコレクションの実行条件があります。

参照カウンタ方式

・オブジェクトごとに参照カウンタが保持され、参照回数が少ないオブジェクトのメモリを、自動的に解放する
・循環参照(オブジェクト間で相互に参照があり、その結果参照カウンタがゼロにならない状況)に対応できない
 例:a=a+1

世代別ガベージコレクション

・メモリの管理を効率化するために、オブジェクトは世代に分けられ最初の世代のオブジェクトが頻繁に収集される
・参照カウンタで拾いきれない部分を補うために使用される

メモリリークが起きやすい例

では、Pythonアプリ開発でメモリリークが起きやすいのは、どのような場合でしょうか。 調べてみると、以下のような状況の時に特に起きやすいようです。

不要なオブジェクトやインスタンスの削除漏れ

プログラムが不要なオブジェクトやクラスのインスタンスへの参照を保持している場合、それらのオブジェクトがメモリ内に残り、メモリリークの原因となる。

循環参照

Pythonでは循環参照が起こりやすく、オブジェクトAとオブジェクトBがお互いに参照し合うと参照を解放できず、メモリリークが発生する。

辞書型の使用

辞書型(dict)は可変オブジェクトであり、大きな辞書を不要なまま保持しておくとメモリリークが生じる。
関数のリターンが辞書型でメモリリークが生じるので、yieldを使用して1行ごと出力する等値を返す。

メモリリークが発生してしまった場合、どのように原因を特定していくのか。
問題解決のための便利なツールをご紹介していきます。

 

メモリ占有率の測定 – memory_profiler

まずはメモリプロファイリングツールを使用して、アプリケーションのどの部分でメモリが使用されているかを測定します。これにより、メモリリークの発生源を特定できます。
今回使用したのは、Pythonのメモリプロファイリングツール「memory_profiler」です。

事前準備

memory_profilerとpsutilのモジュールを、pipを使用してインストールします。

# memory_profilerをインストール
$ pip install -U memory_profiler

# psutilをインストール
$ pip install psutil

開発環境への実装

実行ファイル(test.sh)内のhandle.pyの実行部分を以下に書き換えます。
(memory_profileによる追加は”-m memory_profiler”)

python3 -B -m memory_profiler  ./handler.py $test_root

“-o <出力したいファイル名>.log”のオプション設定により、実行環境のpython以下にプロファイルのlogファイルが生成されます。

計測したい関数の定義位置にて、デコレータ(@profile)を追加します。

@profile
def<計測対象の関数>:

from memory_profiler import profile

LEN = 10000

@profile
def memory_profile_sample():
    # 大きめのリスト
    a = [i for i in range(10000)]
    # 大きめのリストを削除する
    del a
    # 小さめのリスト
    b = [i for i in range(100)]
    # 小さめのリストを削除する
    del b

memory_profile_sample()

実行結果

実行結果は下図の通り、メモリ占有率が表示されます。

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    5     11.5 MiB     11.5 MiB           1    @profile
    6                                          def memory_profile_sample():
    7                                              # 大きめのリスト                                               
    8     67.3 MiB     55.8 MiB       10000        a = [i for i in range(10000)]
    9                                              # 大きめのリストを削除する                                         
   10     11.9 MiB     -55.4 MiB          1        del a
   11                                              # 小さめのリスト                                         
   12     11.9 MiB      0.0 MiB         100        b = [i for i in range(100)]
   13                                              # 小さめのリストを削除する                                        
   14     11.9 MiB      0.0 MiB           1        del b

memory_profilerでの測定の、メリットとデメリット

メリット

調査範囲が広い時に有効。ソースコードの変更量が少ない。

デメリット

繰り返し呼び出されると測定箇所が不明瞭になる。処理速度が遅くなる。

時系列のグラフの出力

matplotlibモジュールをインストールしてあれば
以下のような時系列のグラフを出力することも可能です。

 •mprof  run <スクリプト>
 •mprof plot

 

仮想メモリ情報の取得 – virtual_memory()

virtual_memory()関数は、Pythonのpsutilモジュールで提供されている関数の一つです。
Bashのfreeコマンドと同様、システムの仮想メモリの合計サイズ、使用量、空き容量、仮想メモリのページング統計などの情報を取得でき、アプリケーションがシステムのメモリリソースを効果的に利用しているかどうかを確認できます。

使用方法

動作環境に、psutilのモジュールがインストールされている必要があります。
使用したいスクリプトにpsutilをimportして、確認したい箇所でpsutil.virtual_memory()を呼び出し、ログに出力します。
引数を指定することで、使用量、空き容量等の指定が可能です。

import psutil

mem = psutil.virtual_memory() 

print(f"全体量:{mem.total}")
print(f"使用率:{mem.percent}")
print(f"使用量:{mem.used}")
print(f"空き容量:{mem.free}")

実行結果

下図の通り、メモリの使用量等が出力されます。

全体量:8589934592
使用率:41.5
使用量:3567390138
空き容量:5022544454

virtual_memory()での測定の、メリットとデメリット

メリット

繰り返し呼ぶオブジェクトにも有効。処理も軽い。

デメリット

ソースコードの変更量が増える。ソースコード自体のメモリ使用量が不明瞭。

 

オブジェクトサイズの測定 – __size_of_()、getsizeof()

__size_of_()やgetsizeof()関数を用いるとオブジェクトのサイズを調べることができ、メモリリークの個別のオブジェクトを特定するのに役立ちます。取得されるサイズの単位はバイトになります。

getsizeof()を使用する場合は、sysをimportする必要があります。

from memory_profiler import profile
import sys

LEN = 10000

@profile
def memory_profile_sample():
    # 大きめのリスト
    a = [i for i in range(100000)]
    # 大きめのリストのサイズを出力
    print(f"大きめのリストのサイズは:{a.__sizeof__()}")
    # 大きめのリストを削除する
    del a
    # 小さめのリスト
    b = [i for i in range(100)]
    # 小さめのリストのサイズを出力
    print(f"小さめのリストのサイズは:{sys.getsizeof(b)}")
        # 小さめのリストを削除する
    del b

memory_profile_sample()

実行結果

下図の通り、オブジェクトのサイズが表示されます。

大きめのリストのサイズは:8000412
小さめのリストのサイズは:8067
Filename: /home/user/sample/memory_profile_sample.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    6     22.9 MiB     22.9 MiB           1     @profile
    7                                           def memory_profile_sample():
    8                                               # 大きめのリスト                                              
    9     26.8 MiB      3.9 MiB      100000         a = [i for i in range(100000)]
   10                                               # 大きめのリストのサイズを出力                                         
   11     26.8 MiB      0.0 MiB           1         print(f"大きめのリストのサイズは:{a.__sizeof__()}") 
   12                                               # 大きめのリストを削除する                                         
   13     23.4 MiB     -3.4 MiB           1         del a
   14                                               # 小さめのリスト                                         
   15     23.4 MiB      0.0 MiB         100         b = [i for i in range(100)]
   16                                               # 小さめのリストのサイズを出力                                         
   17     23.4 MiB      0.0 MiB           1         print(f"小さめのリストのサイズは:{sys.getsizeof(b)}")
   18                                               # 小さめのリストを削除する                                        
   19     23.4 MiB      0.0 MiB           1         del b
 

まとめ

いかがでしょうか。
Pythonアプリケーションを開発する上で、メモリリークは面倒な問題です。
でもアプリケーションの安定性と性能に大きな影響を及ぼす問題ですので、
今回ご紹介した手法などを使って、早めに原因の特定を行って問題を解決していきたいですね!

ハートランド・データといえば動的解析ツール【DT+】

実機を動かして、「ナマの挙動」を解析する動的解析。 ソフトの実行経路や実行時間から、ハード側の挙動まで!全部ひと手間で解析できます。

DT+の詳細はこちら

【 無料セミナー 】
レガシーコードをいじる前に知っておいてほしい「動的テスト」の3つの使い方

レガシーコードやOSSなどの資産を使ったソフトウェア開発では、ベースコードへの理解は最重要。
しっかり理解せずに開発してしまうと、あとで不具合発生の温床になってしまうことも。
そんなときこそ、ソフトウェアの実挙動を解析する「動的テスト」の出番。
はじめましてのソースコード相手でも短時間でかなり深く理解できる仕組みをご紹介します。

お申し込みはこちら!