2.40. メモリとガベージコレクション¶
MicroPythonはCPythonと同じ方法でメモリを管理します。すべてのオブジェクトは ヒープ 上に存在し、ガベージコレクタ (GC)が、もはや何からも参照されていないオブジェクトを解放します。数百キロバイトのRAMしか持たないデバイスでは、ヒープがどのように使われているかに注意を払うことがときに必要になります。デスクトップのPythonでは、それはめったにありません。
2.40.1. ヒープ¶
ヒープはRAMの領域であり、ほとんどのOpenMVカメラでは実際には複数の物理メモリプールにまたがって分割されており、ランタイムがPythonオブジェクトに割り当てます。Pythonの式が新しいオブジェクト(リスト、文字列、辞書、タプルなど、小さな整数やシングルトンではないあらゆるもの)を生成するたびに、それを保持するためのバイトのブロックがヒープから取り出されます。GCがオブジェクトが到達不能であると認識すると、そのブロックを元の空きプールに返します。
gc モジュールには知っておく価値のある2つの関数があります。
gc.mem_free()-- 現時点でのヒープ上の空きバイト数のおおよその値です。gc.collect()-- ランタイムが収集を起動するのを待つ代わりに、収集サイクルを直ちに実行します。
import gc
print("before:", gc.mem_free())
big = [0] * 10000
print("after :", gc.mem_free())
del big
gc.collect()
print("freed :", gc.mem_free())
正確な数値はビルドに依存します。重要なのは変化の 方向 です。大きなものを割り当てると mem_free が減少し、参照を手放して gc.collect を実行するとヒープが返却されます。
2.40.2. 断片化¶
空きプールは魔法のように連続しているわけではありません。オブジェクトが異なる寿命で生成・消滅するにつれて、空き領域は、その合計サイズがまだ大きい場合でも、どんどん小さなかたまりに分かれていきます。最大の単一の空きかたまりよりも大きなオブジェクトを割り当てようとすると、MemoryError で失敗します。技術的には十分な合計空きRAMがあるにもかかわらずです。
同じ合計空きRAMでも、断片化の程度に応じて大きなバッファを保持できる(左)こともあれば、拒否する(右)こともあります。¶
断片化が主に懸念されるのは、異なるサイズのバッファを割り当て・解放し続ける長時間実行のスクリプトです。最も効果的な緩和策は、寿命の長いバッファをプログラムの開始近くで、多数の短命な割り当てがそれらを散らばらせる機会を持つ前に 事前割り当て することです。
2.40.3. 事前割り当て¶
2つのパターンがヒープを行儀よく振る舞わせます。
反復ごとに新しいリストや
bytearrayを構築する代わりに、固定サイズのバッファを一度だけ割り当てて再利用します。定数やルックアップテーブルを内側のループの外に出して、一度だけ生成されるようにします。
buf = bytearray(64) # one allocation, reused below
def fill(value):
for i in range(len(buf)):
buf[i] = value
fill(0)
fill(255)
ループの内側で新しいbytearrayを生成するバージョンと比べてみてください。各反復が、GCが後でクリーンアップしなければならないゴミを生み出します。事前割り当てしたバージョンはゴミをまったく生み出しません。
2.40.4. gc.collectを呼ぶタイミング¶
gc.collect() は通常は自動的です。割り当てが十分な空きメモリを見つけられないときにランタイムが起動します。手動で呼び出すことが有用なのは2つの状況です。
大量のオブジェクトがスコープから外れた直後に、次の割り当てがそのコストを支払うのを待つ代わりに、それらを直ちに解放するため。
既知の最大量の空きメモリを必要とするセクションの直前に、時間に敏感な操作の途中でGCが起動するのを避けるため。
gc.collect の呼び出しをあちこちにばらまいてもプログラムは速くなりません。収集自体に時間がかかるからです。予定外の収集のコストの方が、意図的に実行した収集のコストよりも悪くなる地点で、意図的に使ってください。