Python Memory Management: The Essential Guide

Arwin Lashawn on December 04, 2020

背景

Python は「速い」プログラミング言語として知られていない。 しかし、2020年のStack Overflow Developer Surveyの結果によると、PythonはJavaScriptに次いで2番目に人気のあるプログラミング言語です(ご推察の通りです)。 これは、非常に親しみやすい構文と、あらゆる用途に適用できることが大きな理由です。 Python は最速の言語ではありませんが、他の追随を許さないコミュニティ サポートとライブラリの可用性と相まって、コードで物事を成し遂げるのに極めて魅力的な言語となっています。 それはどのようなものでしょうか。 Python のメモリ管理は、私たちの生活を楽にするような方法で実装されています。 Pythonのメモリーマネージャーを聞いたことがありますか? これはPythonのメモリを管理するマネージャーで、メモリ管理に煩わされることなくコードに集中することができるのです。 しかし、Python はそのシンプルさゆえに、手動でメモリを割り当てたり解放したりできる C++ のような言語とは異なり、メモリ使用の管理における自由度をあまり提供しません。

しかしながら、Python のメモリ管理をよく理解することは、より効率の良いコードを書くことを可能にする素晴らしいスタートとなります。 最終的には、あなたが知っている他のプログラミング言語でも採用できる可能性のある習慣として、それを強制することができます。

では、メモリ効率の良いコードを書くことで何が得られるのでしょうか?

  • それは、処理の高速化とリソース、つまりランダム アクセス メモリ (RAM) 使用量の必要性の低減につながります。 より多くの利用可能な RAM は、一般にキャッシュのためのより多くのスペースを意味し、ディスク アクセスを高速化するのに役立ちます。 メモリ効率の良いコードを書くことの素晴らしい点は、必ずしも多くのコード行を書く必要がないことです。
  • もう 1 つの利点は、プロセスが終了しても RAM 使用量が増え続け、最終的にデバイスのパフォーマンスの低下または障害につながる問題、メモリ リークを防ぐことができる点です。 これは、プロセスの終了後に使用済みメモリを解放しないことが原因です。

テクノロジーの世界では、「完了は完璧に勝る」という言葉を耳にしたことがあるかもしれません。 しかし、同じアプリを開発するために Python を使用し、同じ時間内に完成させた 2 人の開発者がいるとします。 一人はよりメモリ効率の良いコードを書き、より高速なアプリを完成させました。 あなたは、スムーズに動作するアプリと、明らかに動作が遅いアプリのどちらを選びますか?

How is Memory Managed in Python?

  • Python Garbage Collection
  • Monitoring Python Memory Issue
  • Best Practices for Improving Python Code Performance
  • How is Memory Managed in Python?

    According the Python documentation for memory management (3.9.0) is involves a private heap that is used to store your program objects and data structures.Python memory managementは、プログラムのオブジェクトとデータ構造を格納するために使用されるプライベートヒープを含んでいます。 また、メモリ管理に関連するほとんどの汚い仕事を処理するのは Python のメモリ マネージャーであり、あなたはコードに集中できることを覚えておいてください。 これらのオブジェクトが有用であるためには、アクセスするためにメモリに格納される必要があります。 それらがメモリに格納される前に、メモリのチャンクは最初にそれらのそれぞれに対して割り当てられるか、または割り当てられなければなりません。

    最も低いレベルでは、Python の生のメモリ割り当て機能は、最初にこれらのオブジェクトを格納するためにプライベート ヒープに利用可能な領域があることを確認します。 これは、オペレーティングシステムのメモリマネージャと対話することによって行います。 Python プログラムがオペレーティングシステムにメモリの塊を要求していると考えてください。

    次のレベルでは、いくつかのオブジェクト固有のアロケータが同じヒープで動作し、オブジェクトの種類によって異なる管理ポリシーを実装します。 すでにご存知かもしれませんが、オブジェクト型の例として、文字列と整数があります。 文字列と整数は、私たちが認識し記憶するのにかかる時間を考えると、それほど違いはないかもしれませんが、コンピュータの中では非常に異なる扱いを受けます。

    Python のヒープがどのように管理されるかについて知っておくべき最後のことは、あなたはそれに対してゼロコントロールを持っているということです。 Python のメモリ管理をほとんど制御できないなら、どうやってメモリ効率の良いコードを書けばいいのでしょうか? その前に、メモリ管理に関するいくつかの重要な用語をさらに理解する必要があります。

    Static vs. Dynamic Memory Allocation

    メモリ割り当てとは何かを理解したところで、2 種類のメモリ割り当て、すなわち静的と動的について理解し、両者を区別する必要があります。

    静的メモリ割り当て:

    • 「静的」という言葉が示すように、静的に割り当てられた変数は永久的であり、事前に割り当てる必要があり、プログラムが実行される限り持続します。
    • メモリはコンパイル時、またはプログラムの実行前に割り当てられる。
    • スタック データ構造を使って実装され、変数はスタック メモリに格納されることを意味します。
    • 割り当てられたメモリは再利用できないため、メモリの再利用性はない。

    動的メモリ割り当て:

    • 「動的」という言葉が示すように、動的に割り当てられた変数は永久ではなく、プログラムの実行中に割り当てることが可能である。
    • ヒープデータ構造を使用して実装され、変数がヒープメモリに格納されることを意味する。
    • 割り当てられたメモリは解放して再利用できる。

    Python における動的メモリ割り当ての利点は、プログラムにどれだけのメモリを必要とするかを事前に気にする必要がないことである。

    ただし、動的メモリ確保はプログラム実行中に行われるため、プログラムの完了までに多くの時間を消費します。 また、割り当てられたメモリは、使用後に解放する必要がある。

    私たちは上記で、ヒープ メモリとスタック メモリという 2 種類のメモリ構造に出会いました。 4380>

    スタック メモリ

    すべてのメソッドとその変数は、スタック メモリに格納されます。 スタックメモリは、コンパイル時に割り当てられることを覚えていますか? これは事実上、このタイプのメモリへのアクセスが非常に高速であることを意味します。

    Python でメソッドが呼び出されるとき、スタック フレームが割り振られます。 このスタックフレームは、メソッドのすべての変数を処理します。 メソッドが返された後、スタックフレームは自動的に破棄される。

    スタックフレームはメソッドの変数にスコープを設定する役割もあることに注意する。

    ヒープメモリ

    すべてのオブジェクトとインスタンス変数はヒープメモリに格納される。 Python で変数が作成されるとき、それはプライベートヒープに格納され、その後、割り当てと解放が可能になる。

    ヒープ メモリは、これらの変数がプログラムのすべてのメソッドによってグローバルにアクセスされることを可能にします。 変数が返された後、Python のガベージコレクタが動作し始めますが、その動作については後で説明します。

    • アリーナ
    • プール
    • ブロック

    まずは、最も大きなアリーナから見ていきましょう。 机の上は、ヒープに割り当てられた256KiBの固定サイズを持つ1つのアリーナを表しています(KiBはKBとは異なりますが、この説明では同じと仮定してください)。

    より具体的には、アリーナは Python のアロケータである pymalloc が使用するメモリ マッピングであり、これは小さなオブジェクト (512 バイト以下) に対して最適化されたものです。 アリーナはメモリの割り当てを担当するため、後続の構造体はもうそれを行う必要がありません。

    このアリーナは、次に大きなメモリ構造である 64 のプールにさらに分割することが可能です。

    プール

    机の例に戻ると、本は 1 つのアリーナ内のすべてのプールを表します。

    各プールは通常 4Kb の固定サイズを持ち、次の 3 つの状態を持つ可能性があります。 プールは空であり、割り当てが可能です。

  • 使用中。 プールにオブジェクトが含まれているため、空でも満杯でもない状態。
  • Full: プールが満杯で、これ以上割り当てることができません。
  • プールのサイズは、オペレーティング システムのデフォルトのメモリ ページ サイズに対応する必要があることに注意してください。

    プールは、最小のメモリ構造である多くのブロックに分割されます。 ブロックのサイズは 8 から 512 バイトの範囲で、8の倍数でなければなりません。

    各ブロックは、特定のサイズの 1 つの Python オブジェクトのみを格納でき、次の 3 つの可能な状態があります:

    • Untouched: 割り当てられていない
    • Free: 割り当てられたが、解放されて割り当て可能になった
    • Allocated: 割り当て済み

    上で説明したメモリ構造の3つのレベル(アリーナ、プール、ブロック)は、特に小さい Python オブジェクトのためのものであることに注意してください。

    Python ガーベッジコレクション

    ガーベッジコレクションは、もはや使用されていないオブジェクトのために以前に割り当てられたメモリを解放するためにプログラムによって実行される処理です。 ガベージ コレクションをメモリのリサイクルまたは再利用と考えることができます。

    昔は、プログラマはメモリを手動で割り当てたり解除したりする必要がありました。 メモリの割り当て解除を忘れると、メモリ リークが発生し、実行パフォーマンスの低下を招きます。 さらに悪いことに、手動でのメモリの割り当てと解放は、誤ってメモリを上書きしてしまう可能性さえあり、プログラムが完全にクラッシュしてしまうこともあります。 具体的には、Python は参照カウントと世代別ガベージコレクションを組み合わせて使用し、未使用のメモリを解放しています。 Pythonでは参照カウントだけでは不十分なのは、ぶら下がった循環参照を効果的にクリーンアップできないからです。

    世代別ガベージコレクションサイクルには次のステップがあります –

    1. Python は未使用オブジェクトの「廃棄リスト」を初期化します。
    2. 参照サイクルを検出するためにアルゴリズムが実行されます。
    3. オブジェクトが外部参照を失っている場合、それは廃棄リストに挿入されます。
    4. 廃棄リスト内のオブジェクトのためにメモリ割り当てを解放する。

    Python のガベージコレクションについてもっと学ぶには、 Python Garbage Collection を参照してください。

    Monitoring Python Memory Issues

    誰もが Python を愛していますが、Python はメモリの問題を避けては通れません。 多くの考えられる理由があります。

    Python (3.9.0) のメモリ管理のドキュメントによると、Python のメモリ マネージャは、必ずしもメモリをオペレーティング システムに解放して戻すわけではありません。 特定の状況下では、Python のメモリマネージャーはガベージコレクション、メモリコンパクション、または他の予防措置のような適切なアクションをトリガーしないかもしれない”

    結果として、人は Python で明示的にメモリを解放する必要があるかもしれない、とドキュメントに記述されています。 これを行う 1 つの方法は、gc モジュールを使用することにより、Python ガーベッジコレクタに未使用メモリを解放するように強制することです。 そのためには、gc.collect()を実行するだけでよいのです。

    Python ガーベッジコレクタの時々誤った性質とは別に、特に大きなデータセットを扱うとき、いくつかの Python ライブラリもメモリ リークを引き起こすことが知られています。 たとえば、Pandas は、レーダー上のそのようなツールの 1 つです。 pandas の公式 GitHub リポジトリにあるすべてのメモリ関連の問題を見てみることを検討してください!

    コード査読者の鋭い目さえもすり抜けるかもしれない明白な理由の1つは、コード内に解放されていない長引く大きなオブジェクトがあることです。 同じノートで、無限に成長するデータ構造も懸念の原因です。 たとえば、固定サイズ制限のない成長する辞書データ構造です。

    成長するデータ構造を解決する 1 つの方法は、可能であれば辞書をリストに変換し、リストに最大サイズを設定することです。 そうでなければ、単に辞書のサイズに制限を設定し、制限に達したときにそれをクリアします。

    さて、そもそもメモリの問題をどのように検出すればよいのだろうかと疑問に思われるかもしれません。 1 つのオプションは、アプリケーション パフォーマンス監視 (APM) ツールを利用することです。 さらに、多くの有用な Python モジュールが、メモリの問題の追跡とトレースを支援します。 APM ツールから始めて、選択肢を見てみましょう。

    アプリケーション パフォーマンス監視 (APM) ツール

    では、アプリケーション パフォーマンス監視とは厳密に何でしょうか、そして、メモリの問題を追跡するのに役立つのでしょうか。 APM ツールを使用すると、プログラムのパフォーマンス指標をリアルタイムで観察でき、パフォーマンスを制限している問題を発見しながら継続的に最適化することが可能になります。 リアルタイムのパフォーマンス指標を受信および監視できるため、観察された問題に対して直ちに行動を起こすことができます。 メモリ問題の犯人である可能性のあるプログラムの領域を絞り込んでから、コードに飛び込み、他のコード貢献者と議論して、修正する必要のある特定のコード行をさらに決定することが可能です。 コードを本当に理解する必要があるため、それを修正するのはまた別の悪夢です。 このような場合、ScoutAPMは、アプリケーションのパフォーマンスを建設的に分析し、最適化することができる熟練したAPMツールであるため、もう探す必要はありません。 ScoutAPM はリアルタイムの洞察を提供するので、クライアントが問題を発見する前に、素早く問題を特定し、解決することができます。

    プロファイル モジュール

    メモリ問題、それがメモリリークやメモリの過剰使用によるプログラムのクラッシュであっても、それを解決できる便利な Python モジュールが数多く存在します。 その中でもお勧めのものは以下の通りです:

    1. tracemalloc
    2. memory-profiler

    注意: tracemalloc モジュールだけが組み込みなので、使用したい場合は他のモジュールを最初にインストールするようにしてください。

    tracemalloc

    Python (3.9.0) の tracemalloc のドキュメントによると、このモジュールを使用すると、次の情報を提供することができます。

  • ファイル名および行番号ごとの割り当て済みメモリ ブロックの統計: 割り当て済みメモリ ブロックの合計サイズ、数、および平均サイズ。
  • メモリ リークを検出するために 2 つのスナップショット間の差を計算します。 これは、ドキュメントに示されている最初のコード例を使用して簡単に行うことができます。

    しかし、これは、少量のメモリを割り当てたファイルが、将来メモリ リークを引き起こすために無限に成長しないことを意味しません。

    memory_profiler

    このモジュールは楽しいものです。 これを使用して作業したことがありますが、調査したい任意の関数に @profile デコレーターを単純に追加するオプションを提供するため、個人的にお気に入りです。 結果として得られる出力も非常にわかりやすいです。

    これを個人的にお気に入りにしたもうひとつの理由は、このモジュールで時間ベースのメモリ使用量のグラフを描画できることです。 時には、メモリ使用量が無限に増加し続けるかどうか、簡単にチェックする必要があります。 これは、それを確認するために行ごとのメモリプロファイリングを行う必要がないため、そのための完璧なソリューションです。 プロファイラを一定時間実行した後、プロットされたグラフを観察するだけでよいのです。 以下は出力されたグラフの例です –

    memory-profiler documentation の説明によると、この Python モジュールはプロセスのメモリ消費を監視し、また Python プログラムについて同じものを一行ごとに分析するためのものだそうです。 psutil ライブラリに依存する純粋な Python モジュールです。

    Memory-profiler がどのように使用されるかをさらに調べるには、この Medium ブログを読むことをお勧めします。 また、別の Python モジュールである muppy (最新のものは muppy3) の使用方法も紹介されています。

    Best Practices for Improving Python Code Performance

    メモリ管理に関するすべての詳細については、もう十分だと思われます。

    Pythonのライブラリと組み込み関数を活用する

    はい、これはかなり頻繁に見落とされるかもしれない良い習慣です。 Python には他の追随を許さないコミュニティ サポートがあり、これは API 呼び出しからデータ サイエンスまで、あらゆる目的で利用できる豊富な Python ライブラリに反映されています。

    すでに実装したものと同じことを実行できる Python ライブラリがある場合、できることは、カスタム コードの使用時と比較して、ライブラリを使用時のコード パフォーマンスを比較することです。 Pythonライブラリ(特に人気のあるもの)は、コミュニティのフィードバックに基づいて継続的に改善されているため、あなたのコードよりもメモリ効率が良い可能性があります。

    Python ライブラリは、コードの多くの行を節約することができます。 したがって、”+” 演算子で文字列に要素を追加するたびに、Python は新しい文字列を作成し、新しいメモリ割り当てを行わなければなりません。 より長い文字列では、コードのメモリ効率の悪さはより顕著になるでしょう。

    効率的なループのための itertools の使用

    ループは、ものを自動化する上で不可欠な部分です。 ループをより多く使用し続けると、最終的には、実行時の複雑性が高いために非効率であることが知られているネストされたループを使用しなければならないことに気づきます。 Python の itertools ドキュメントによると、「このモジュールは、それ自体または組み合わせで有用な、高速でメモリ効率の良いツールのコアセットを標準化します。 一緒に使うと、純粋な Python で特化したツールを簡潔かつ効率的に構築することが可能になる」

    つまり、itertools モジュールは不要なループを取り除くことにより、メモリ効率の良いループ処理を可能にします。 興味深いことに、itertools モジュールは無数の問題に対するエレガントなソリューションを構成できるため、gem と呼ばれています。

    次のコードで少なくとも 1 つのループを扱うことは間違いないので、itertools を実装してみてください!

    総括と最後の考察

    良い Python メモリ管理習慣を応用することはカジュアルプログラマーのためのものではありません。 もしあなたが普段から簡単なスクリプトで済ませているなら、メモリ関連の問題には全く遭遇しないはずです。 これを読んでいる間にも急速に進歩し続けているハードウェアとソフトウェアのおかげで、そこにあるほとんどすべてのデバイスの基本モデルは、そのブランドに関係なく、日常のプログラムを問題なく実行できるはずです。 メモリ効率の良いコードの必要性は、大きなコードベース、特にパフォーマンスが重要な実稼働環境での作業を開始したときに初めて明らかになります。

    しかしながら、これは Python のメモリ管理が把握しにくい概念であることを意味しませんし、重要でないことを意味しません。 なぜなら、アプリケーションのパフォーマンスに対する比重は日に日に高まっているからです。 ある日、それはもう単なる「できた」という問題ではなくなるでしょう。 その代わりに、開発者は、顧客のニーズをうまく解決できるだけでなく、非常に速いスピードと最小限のリソースでそれを行うソリューションを提供するために競うことになるでしょう。