メモリ管理

C/C++のようなプログラミング言語とは異なり、MicroPythonは自動メモリ管理をサポートすることで、メモリ管理の詳細を開発者から隠蔽します。自動メモリ管理は、メモリの割り当てと解放を自動的に管理するためにオペレーティングシステムやアプリケーションが用いる手法です。これにより、オブジェクトに割り当てられたメモリの解放を忘れるといった問題が解消されます。また、自動メモリ管理は、すでに解放されたメモリを使用してしまうという重大な問題も回避します。自動メモリ管理にはさまざまな形態がありますが、そのひとつがガベージコレクション(GC)です。

ガベージコレクターには通常、次の2つの責務があります。

  1. 利用可能なメモリ内に新しいオブジェクトを割り当てる。

  2. 使用されていないメモリを解放する。

GCアルゴリズムは数多く存在しますが、MicroPythonはメモリ管理に マーク・アンド・スイープ 方式を採用しています。このアルゴリズムには、ヒープをたどって生きているすべてのオブジェクトにマークを付けるマークフェーズと、ヒープを走査してマークの付いていないすべてのオブジェクトを回収するスイープフェーズがあります。

MicroPythonにおけるガベージコレクション機能は、組み込みモジュール gc を通じて利用できます。

>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>

gc.disable() が呼び出されている場合でも、 gc.collect() でコレクションをトリガーできます。

Cコードから見たMicroPythonのメモリ

「Pythonヒープ」からメモリを割り当てるCコード(すなわち m_malloc()m_malloc0()m_free() などの関数)を記述する際には、ガベージコレクターを意識する必要があります。

ガベージコレクターのマークフェーズは、次のルートを起点としてヒープメモリへの生きたポインタをスキャンします。

  • メインのPythonランタイム(またはREPL)のスタック。

  • 各「Pythonスレッド」のスタック。ネイティブのオペレーティングシステムのスレッドやタスクの上にPythonスレッドを実装しているポートが対象です。

  • Cコード内でマクロ MP_REGISTER_ROOT_POINTER を使用して定義された「ルートポインタ」。これは、Pythonヒープへの静的スコープのポインタを保持するための推奨される方法です。

  • m_tracked_calloc()m_tracked_reallocm_tracked_free() の各関数で行われたトラッキング済みの割り当て。これらの特別な関数を使うと、ガベージコレクターによって常に生きているとみなされるメモリブロックを割り当てることができます。Cでのメモリ割り当てと同様に、このメモリは m_tracked_free() を呼び出すか、ソフトリセットを行うことでのみ解放されます。トラッキング済みの割り当てごとに、わずかなメモリ使用量と実行時オーバーヘッドが発生します。この機能は、すべてのポートでデフォルトで有効になっているわけではありません。

次にガベージコレクターは、すべてのアドレスを使い果たすまで、ルートポインタが指すすべてのメモリを再帰的にスキャンしてマークを付けます。これにより、MicroPythonランタイムでまだ使用されているすべてのPythonオブジェクトを見つけ出すのに十分です。

ただし、次のメモリはガベージコレクターによってスキャン されず 、早期に解放されてしまう可能性があります。

  • ヒープメモリへのポインタを含む静的またはグローバルなC変数。

  • 割り当てられたバッファの「先頭」(すなわち m_malloc() が返した正確なアドレス)ではなく、割り当てられたバッファ内のアドレス(たとえばネストされた構造体へのポインタ)を指すポインタ。パフォーマンス上の理由から、このような場合、ガベージコレクターは外側のバッファにマークを付けません。

  • Pythonコードを実行していない、または「Pythonスレッド」として手動で登録されていない、任意のスレッドやRTOSタスクのスタック(ネイティブスレッドやタスクをサポートするポートが対象)。

これらのシナリオで解放後使用(use-after-free)を回避する方法は次のとおりです。

  • トラッキング済み割り当てAPIである m_tracked_calloc()m_tracked_realloc()m_tracked_free() を使用する。

  • 静的変数にポインタを格納する代わりに、ルートポインタを登録する(上記参照)。

  • コードを再構成する。たとえば、関連するすべてのポインタを静的変数に持たせる代わりに、それらを保持するシングルトンのPythonオブジェクト(Cで実装)をPythonコードが初期化するAPIを用意します。

注釈

ソフトリセット は常にPythonヒープをクリアし、すべてのメモリを解放します。ソフトリセット後にヒープへのポインタを保持しないことが重要です。それらは解放済みメモリを指すダングリングポインタになってしまうためです。

一部のポートは「Cヒープ」もサポートしています( C の動的メモリ割り当て を参照)。その場合、標準のC関数 malloc などを呼び出すことで、ソフトリセットをまたいで有効であり続けるメモリを割り当てることができます。

オブジェクトモデル

すべてのMicroPythonオブジェクトは mp_obj_t というデータ型で参照されます。これは通常ワードサイズ(すなわちターゲットアーキテクチャ上のポインタと同じサイズ)であり、一般的に32ビット(STM32、RP2、nRF、Unix x86)または64ビット(Unix x64)です。特定のオブジェクト表現ではワードサイズより大きくなることもあり、たとえば OBJ_REPR_D では32ビットアーキテクチャ上で64ビットサイズの mp_obj_t を持ちます。

mp_obj_t はMicroPythonオブジェクトを表します。たとえば整数、浮動小数点数、型、辞書、クラスインスタンスなどです。ブール値や小さな整数のような一部のオブジェクトは、その値が mp_obj_t の値に直接格納され、追加のメモリを必要としません。その他のオブジェクトは値をメモリ内の別の場所(たとえばガベージコレクション対象のヒープ上)に格納し、その mp_obj_t はそのメモリへのポインタを含みます。 mp_obj_t の一部はタグであり、それがどの種類のオブジェクトであるかを示します。

利用可能な表現の具体的な詳細については py/mpconfig.h を参照してください。

ポインタタギング

ポインタはワード境界に整列されているため、 mp_obj_t に格納される際、このオブジェクトハンドルの下位ビットはゼロになります。たとえば32ビットアーキテクチャでは、下位2ビットがゼロになります。

********|********|********|******00

これらのビットはタグを格納する目的で予約されています。タグは、オブジェクト内にその情報を格納する新しいフィールドを導入する(非効率になる可能性があります)のではなく、追加情報を格納します。MicroPythonでは、タグは扱っているものが小さな整数、インターン化された(小さな)文字列、または具体的なオブジェクトのいずれであるかを示し、それぞれに異なるセマンティクスが適用されます。

小さな整数の場合、マッピングは次のとおりです。

********|********|********|*******1

アスタリスクが実際の整数値を保持します。インターン化された文字列や即値オブジェクト(たとえば True )の場合、 mp_obj_t の値のレイアウトはそれぞれ次のようになります。

********|********|********|*****010

********|********|********|*****110

一方、上記のいずれにも該当しない具体的なオブジェクトは次の形式をとります。

********|********|********|******00

ここでの星印は、メモリ内の具体的なオブジェクトのアドレスに対応します。

オブジェクトの割り当て

小さな整数の値は mp_obj_t に直接格納され、ヒープや他の場所ではなくその場で割り当てられます。そのため、小さな整数の生成はヒープに影響しません。すでにテキストデータが別の場所に格納されているインターン化された文字列や、 NoneFalseTrue のような即値についても同様です。

具体的なオブジェクトであるその他すべてはヒープ上に割り当てられ、そのオブジェクト構造は、オブジェクトの型を格納するためのフィールドがオブジェクトヘッダ内に予約されるようになっています。

+++++++++++
+         +
+ type    + object header
+         +
+++++++++++
+         + object items
+         +
+         +
+++++++++++

ヒープの最小の割り当て単位はブロックであり、そのサイズは4マシンワードです(32ビットマシンでは16バイト、64ビットマシンでは32バイト)。各ブロックにおけるオブジェクトの割り当て状況は、ヒープ上に割り当てられる別の構造体が追跡します。この構造体は ビットマップ と呼ばれます。

../_images/bitmap.png

ビットマップは、ブロックが「空き」か「使用中」かを追跡し、各ブロックのこの状態を追跡するために2ビットを使用します。

マーク・アンド・スイープ方式のガベージコレクターは、ヒープ上に割り当てられたオブジェクトを管理し、またビットマップを利用してまだ使用中のオブジェクトにマークを付けます。これらの詳細の完全な実装については py/gc.c を参照してください。

割り当て: ヒープレイアウト

ヒープは、プール内のブロックで構成されるように配置されています。ブロックはさまざまなプロパティを持つことができます。

  • ATB(allocation table byte、割り当てテーブルバイト): セットされている場合、そのブロックは通常のブロックです。

  • FREE: 空きブロック

  • HEAD: ブロックチェーンの先頭

  • TAIL: ブロックチェーンの末尾

  • MARK : マークされた先頭ブロック

  • FTB(finaliser table byte、ファイナライザーテーブルバイト): セットされている場合、そのブロックはファイナライザーを持ちます。