0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0201(2025/07/25)Python memory_profilerガイド

Posted at

Python memory_profiler徹底ガイド

memory_profilerは Python コードの行単位でメモリ使用量を可視化し、リーク箇所を素早く特定できる軽量ツールです。

1. memory_profiler とは?

  • 行単位の RSS(常駐メモリ) を計測し、どの行がどれだけメモリを確保・解放したかを一覧表示
  • 付属 CLI mprof で、スクリプト全体のメモリ推移をグラフ化
  • psutil をバックエンドに使用するシンプル構成で、追加ビルド不要

類似ツールとの比較

観点 memory_profiler tracemalloc objgraph
測定粒度 行単位(RSS) Python ヒープ、スナップショット単位 オブジェクト参照グラフ
リーク特定 ★★★速い ★★★詳細 ★★可視化寄り
C拡張メモリ △ (計測不可)
導入コスト pip1行 標準 pip1行

2. インストールと準備

pip install -U memory-profiler psutil   # 必須
pip install matplotlib                  # mprof plot 用(任意)

最小サンプル

from memory_profiler import profile

@profile
def heavy_job():
    a = [i for i in range(10_000_000)]
    b = [i**2 for i in range(10_000_000)]
    del b
    return a

if __name__ == "__main__":
    heavy_job()

実行:

python -m memory_profiler sample.py

結果:

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
     3     49.7 MiB     49.7 MiB           1   @profile
     4                                         def heavy_job():
     5     52.7 MiB -350195603.5 MiB    10000001       a = [i for i in range(10_000_000)]
     6     34.7 MiB -60282161.9 MiB    10000001       b = [i**2 for i in range(10_000_000)]
     7      6.4 MiB    -28.3 MiB           1       del b
     8      6.4 MiB      0.1 MiB           1       return a

3. ワークフロー概観

  1. まず mprof run で全体傾向をつかむ
  2. スパイク地点の関数に @profile を付けて再計測
  3. 余分な確保行を修正 → 効果確認

4. mprof によるグラフ化

mprof run --interval 0.1 python sample.py   # ログ取得
mprof plot                                   # PNG ウィンドウ表示
mprof peak                                   # 最大値だけ知る
  • --include-children でサブプロセスも追跡
  • .dat は CSV 互換; Excel や pandas で二次解析可能

5. メモリリークデバッグ実践

5.1 再現コード

from memory_profiler import profile

leaks = []

@profile
def leaky(n=1000):
    for _ in range(n):
        leaks.append(bytearray(10**6))  # 1 MB 確保しっぱなし

a = leaky(300)

実行結果サンプル(抜粋):

Line #    Mem usage    Increment  Occurrences   Line Contents
-------------------------------------------------------------
     7     56.0 MiB     56.0 MiB           1   @profile
     8                                         def leaky(n=1000):
     9    294.2 MiB    238.2 MiB         300       leaks.append(bytearray(10**6))

5.2 改善策

  • グローバル→ローカルへ変更
  • del + ガベコレクション強制
  • memory_profiler.show_results(timestamp=True) で実行タイミングを確認

修正版:

import gc
from memory_profiler import profile

@profile
def leaky_fixed(n=1000):
    for _ in range(n):
        buf = bytearray(10**6)
        # 使い終わったら参照を切るだけで OK
    gc.collect()

実行結果サンプル(抜粋):

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    27     49.3 MiB     49.3 MiB           1   @profile
    28                                         def main(n=1000):
    29     53.2 MiB      0.0 MiB         301       for _ in range(n):
    30     53.4 MiB      4.1 MiB         300           buf = bytearray(10**6)
    32     55.2 MiB      0.0 MiB           1       gc.collect()  # RSS がほぼ初期値に戻る

再計測→インクリメントがほぼゼロであることを確認。

5.3 実行結果比較と考察

以下は リークあり修正版 をそれぞれ memory_profiler で計測した生ログ抜粋です。

# リークあり
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    15    275.0 MiB    275.0 MiB           1   @profile
    16                                         def main(n=1000):
    17    379.1 MiB  -1743.0 MiB         301       for _ in range(n):
    18    379.2 MiB  -1608.4 MiB         300           leaks.append(bytearray(10**6))  # 1 MB 確保しっぱなし

# 修正版
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    27     49.3 MiB     49.3 MiB           1   @profile
    28                                         def main(n=1000):
    29     53.2 MiB      0.0 MiB         301       for _ in range(n):
    30     53.2 MiB      3.9 MiB         300           buf = bytearray(10**6)
    32     53.2 MiB      0.0 MiB           1       gc.collect()

読み取りポイント

  • ピークRSS: リークありは約 379 MiB、修正版は約 53 MiB — 約 7× の差。
  • 増加の持続: リーク版ではループ後も 100+ MiB が残留、修正版では元に戻る。
  • 巨大な負の Increment: OS が未使用ページを回収した瞬間にサンプリングが当たると発生。Mem usage が下がっていなければ無視してよい。
  • Occurrences × Increment で概算すると、修正版は 1 回につき 13 KB 程度のみ残存 ─ Python オブジェクトのメタデータ分だけで本体は解放されていることがわかる。

結論: グローバルリストへの保持がリークの原因。ローカル変数化 & gc.collect() で一気に解消できた。

6. Tips & Best Practices

  • 対象を絞る: 高頻度ループ全行に @profile は非推奨
  • gc.disable() で GC 影響を排除し、本質的な増加のみを見る
  • 環境を揃える: 仮想環境・OS のメモリ管理差異で値が変動
  • CI 連携: mprof run pytest で回帰テストに最大 RSS を記録

7. まとめ

  • memory_profiler は「どこでどれだけ確保したか」を 秒で 把握できる
  • mprof → 行単位 → 修正 の三段構えが王道
  • C 拡張や巨大 NumPy 配列は別途 tracemallocline_profiler と併用しよう

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?