今回はMicroPythonのメモリの使い方の話を若干。C言語などではconst扱いのオブジェクトをFlash上に配置して貴重なRAMをセーブするということがあります。馬鹿なのでMicroPythonも同じようなもんかい、と思っていたら違いました。Flashに置くことはできるのですがそれはまた別の話みたいっす。
※「MicroPython的午睡」投稿順 Indexはこちら
参照すべきドキュメントの数々
いつもドキュメントをよく読みもしないで始めて、あとで泥縄で読んでます。いつもお世話になっておりますMicroPython公式の以下の日本語ドキュメントは、今回読んでおくべきものかと。知らんけど。
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スクリプトが以下に。
-
- 大域変数xyzを、micropython.const()を使用して宣言
- main()関数冒頭で、ガベージコレクタ発動、ヒープにアロケートされているメモリ量など確認。
- main内のローカル変数abcを普通に作成
- abcを削除した前後でメモリ量の変化を確認(適宜GC発動)
- 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したときのメモリ量が変化することを観察します。
つづいて、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を開放したときのメモリ量と同じだあ。
最後に xyzの宣言そのものを取り除いたらどうよ、という話。修正は以下のようです。
#xyz = micropython.const([123, 456, 789, 111, 222, 333, 444, 555, 666, 777]) #~途中略~ # del globals()['xyz']
大域変数1個と文が2行無くなったことで大分メモリ減りました。単純大域変数1個のときと比べて64バイト減。大域変数分をふくめたら128バイト減。
なお、64バイト減とか80バイト減とか16の倍数単位で変化するのは、考察するに(ドキュメントに書いてあろうが)、ヒープの管理が16バイト単位で割り当てているためであろうと。
ちょっとはMicroPythonのメモリの使い方が見えてきた。ホントか?