MicroPython的午睡(114) ESP32版、ヒープ・メモリの使用状況

Joseph Halfmoon

今回はMicroPythonのメモリの使い方の話を若干。C言語などではconst扱いのオブジェクトをFlash上に配置して貴重なRAMをセーブするということがあります。馬鹿なのでMicroPythonも同じようなもんかい、と思っていたら違いました。Flashに置くことはできるのですがそれはまた別の話みたいっす。

※「MicroPython的午睡」投稿順 Indexはこちら

参照すべきドキュメントの数々

いつもドキュメントをよく読みもしないで始めて、あとで泥縄で読んでます。いつもお世話になっておりますMicroPython公式の以下の日本語ドキュメントは、今回読んでおくべきものかと。知らんけど。

メモリー管理

マイクロコントローラ上の MicroPython

gc — ガベージコレクションの制御

micropython — MicroPython 内部のアクセスと制御

ESP32版のMicroPythonが使用できるSRAM容量

今回使用しているのはESP32-WROOM-32D搭載のESP32-DevkitCであります。公式には以下の容量のメモリを搭載の模様。

    • SPI-Flash 4MB
    • SRAM 520kB

ここでSPI-Flash側には、各種のファームウエアと一緒にMicroPython「処理系」(インタプリタというべきか、それともVM、仮想マシンというべきか)が書きこまれているはず。そしてFlashの一部領域はMicroPythonからストレージ・デバイスとしても見えると。

一方SRAMは 520kBと潤沢にあるように見えますが、実際にはWiFiやBTのファームウエアなど組み込まれている各種ファームウエア群も使うので、MicroPython上のアプリが使える分量というのはせいぜい120kB程度みたいです。まあ、メモリ搭載量が非常に限られたマイコン上のMicroPythonに比べたら十分多いですが。

SRAMを使うもの

まずは、ローカルな記憶としてのスタック領域がとられます。今回観察したところでは、15kバイトくらい確保されてるみたいです。

残りのメモリからヒープが確保され、ヒープの中に各種のオブジェクトが格納されることになるみたいです。フリー領域は110kバイトくらいあるみたいで、ここから必要量が切り出されて使われます。

MicroPythonで書かれたソースをコンパイルした結果の「バイトコード」はRAM上に置かれるのがキホンみたいです。つまり上記のフリー領域を消費するものどもの一人みたいです。変数などの各種データもみなRAMを消費します。通常は

mp_obj_t型

という型の変数でポイントされる先のヒープにデータが詰まっているみたいですが、整数などの小さいデータは、mp_obj_t型の中にデータそのものが詰められているみたいです。ポインタか、データなのかはLSB側の2ビットにコードされているのね。

なお、バイトコードなどをFlashに配置する方法はありますが、「別ルートで」その処置を行わないとならないようです。今回はやりませぬ。また今度ね。

実験したソース

所要メモリ量の変化を観察するために、作成したMicroPythonスクリプトが以下に。

    1. 大域変数xyzを、micropython.const()を使用して宣言
    2. main()関数冒頭で、ガベージコレクタ発動、ヒープにアロケートされているメモリ量など確認。
    3. main内のローカル変数abcを普通に作成
    4. abcを削除した前後でメモリ量の変化を確認(適宜GC発動)
    5. xyzを削除した前後でメモリ量の変化を確認(適宜GC発動)

こんな感じです。

import micropython, gc

xyz = micropython.const([123, 456, 789, 111, 222, 333, 444, 555, 666, 777])

def main():
    testSTR = "ESP32, memory usage."
    print(testSTR)
    gc.collect()
    print("mem_alloc:", gc.mem_alloc())
    print("mem_free:", gc.mem_free())
    print("mem_info:")
    print(micropython.mem_info())
    abc = [123, 456, 789, 111, 222, 333, 444, 555, 666, 777]
    print("mem_info(abc):")
    print(micropython.mem_info())
    del abc
    gc.collect()
    print("mem_info(del abc):")
    print(micropython.mem_info())
    del globals()['xyz']
    gc.collect()
    print("mem_info(del global xyz):")
    print(micropython.mem_info())
    
    print("qstr_info:")
    print(micropython.qstr_info())
            
if __name__ == "__main__":
    main()
実行結果

黄色のマーカ部をみると、gc.mem_alloc()関数で報告されるバイト数とmicropython.mem_info()関数で used: として報告されるバイト数は同じであることが分かります。これがヒープに割り当てられているSRAM量かと。

一方、緑のマーカ部をみると、gc.mem_free()関数で報告されるバイト数とmicropython.mem_info()関数で free: として報告されるバイト数は同じであり、これが未割り当てのSARM量であることが分かります。

黄色のマーカと水色のマーカの差は64バイトあり、これがabcの名で確保された10要素のLIST構造を反映していることが分かります。変数abcをdel(したのちにGC)することによってこの64バイトが開放されていることが確認できます。

一方、constで宣言した大域変数 xyz を del globals()[‘xyz’] として消去してもピンクのマーカ部のようにメモリ量は変化しないことが分かります。一方、次のステップでは const宣言していない 大域変数 xyz をdelしたときのメモリ量が変化することを観察します。

ConstGlobal

 

つづいて、micropython.const()で宣言していた大域変数 xyz を、通常の大域変数にしてみます。修正箇所はこんな感じ。

xyz = [123, 456, 789, 111, 222, 333, 444, 555, 666, 777]

実行結果が以下に。

まずちょっと驚くのが、micropython.const()を使わなくなっただけで80バイトもRAM使用量が減っていること。結構オーバヘッドがあるのね。

もひとつ驚くのが、先ほどのmicropython.const()で宣言したときには、

del globals()[‘xyz’]

のようにしてもメモリ量は復活しなかったのに、今回はしっかり「開放」されていること。その量64バイト。ローカル変数abcを開放したときのメモリ量と同じだあ。noConstGlobal

最後に xyzの宣言そのものを取り除いたらどうよ、という話。修正は以下のようです。

#xyz = micropython.const([123, 456, 789, 111, 222, 333, 444, 555, 666, 777])
#~途中略~
# del globals()['xyz']

大域変数1個と文が2行無くなったことで大分メモリ減りました。単純大域変数1個のときと比べて64バイト減。大域変数分をふくめたら128バイト減。

noXYZ

なお、64バイト減とか80バイト減とか16の倍数単位で変化するのは、考察するに(ドキュメントに書いてあろうが)、ヒープの管理が16バイト単位で割り当てているためであろうと。

ちょっとはMicroPythonのメモリの使い方が見えてきた。ホントか?

MicroPython的午睡(113) ESP32版、hashlib使ってsha256を計算 に戻る

MicroPython的午睡(115) ESP32版、mpy-crossの利用 へ進む