別件でラズパイPico上のMicroPythonを使用して、arrayモジュールを使ってみたところ、バイト要素が高々16000個以下でエラーで落ちました。他のプログラム要素の使用分もあるので一概には言えないですが、ラズパイPico搭載のSRAM量からしたらかなり小さくて予想外。どういう書き方したらどのくらいの容量が使えるのだか気になって調べてみました。対象はラズパイPicoとM5ATOM LiteのMicroPythonです。
※「MicroPython的午睡」投稿順 Indexはこちら
(末尾に実験に使用したMicroPythonのコード全文を掲げました。ラズパイPicoのMicroPythonと、M5ATOM Liteに書き込んだESP32用MicroPython “generic” ポートの両方で同一コードが実行できます。)
arrayモジュール(MicroPythonでは uarray とも呼ばれる)は「効率良く」同型の数値を蓄えられる構造です。ホスト上では単にデータを蓄えるだけではない NumPyとかPandasとか超強力なモジュールがありますが、メモリの限られるMicroPythonではこれがファーストチョイスだと思います。以下にMicroPythonの日本語ドキュメントへのリンクを貼り付けました。
MicroPythonの各実装によりドキュメント記載のどこまでが本当に実装されているのか分からない(特にarrayについてはCPythonの記述を見よ、という感じ)なので、例によって現物で調べておきます。対象はラズパイPicoのMicroPythonです。
>>> import uarray >>> help(uarray) object <module 'uarray'> is of type module __name__ -- uarray array -- <class 'array'> >>> help(uarray.array) object <class 'array'> is of type type append -- <function> extend -- <function> decode -- <function>
ホスト上のCPython上の array モジュールに比べると、ほんと必要最小限、という感じの実装であることが分かります。
ハードウエア上のメモリ量
組み込みマイコンでは仮想記憶が使えるケースはほとんどないので、記憶できる容量は物理的なメモリ量に制限されます。以下にラズパイPicoとM5ATOM Liteの搭載メモリ量とCPUについての表を掲げます。
Raspberry Pi Pico | M5ATOM Lite | |
---|---|---|
SRAM | 264KB | 520KB |
Flash | 2MB | 4MB |
CPU | Dual Cortex M0+ | Dual Tensilica LX6 |
SPEED | 133MHz | 240MHz |
どちらも切のよい数字とちょっと異なるのが気になりますがそれはまた別の機会に調べます。
こうしてみるとM5 ATOMLiteの方がほぼ倍くらいの物理メモリを搭載していることが分かります。しかし表面上の数字だけでは比べられません。M5 ATOMLiteは無線IF(WiFiとBLE)を搭載しています。当然ネットワークのプロトコルスタックなども搭載している筈。なんとなればラズパイPicoは2個のCPUをユーザーの仕事に動員できますが、M5ATOM LiteのCPUの片方は「裏の仕事(通信)」に専従状態みたいで表に出てきません。裏で使われているメモリ量もM5ATOM Liteの方が格段に大きい筈。上記の数字は単純搭載量ということで。
実験に使用したスクリプト
同一のスクリプトをラズパイPicoとM5ATOM Liteの両方で走らせて、どこまでメモリ上にデータを格納できるものだか調べてみました。調べた手順は以下です。
-
- スクリプト起動直後、まだ配列確保していない状態でのメモリを確認
- シンプルなbytearrayで、byte型の配列をメモリエラーで落ちるまで確保してみる。
- array.arrayの符号なしバイト型の配列をコンストラクタにイニシャライザを渡す形でメモリ確保してみる。メモリエラーで落ちたら終了。
- array.arrayの符号なしバイト型の配列をコンストラクタ後、1要素づつAppendする形でメモリ確保してみる。メモリエラーで落ちたら終了。
- array.arrayの符号なしロング型(4バイト整数)の配列をコンストラクタにイニシャライザを渡す形でメモリ確保してみる。メモリエラーで落ちたら終了。
- array.arrayの符号なしロング型(4バイト整数)の配列をコンストラクタ後、1要素づつAppendする形でメモリ確保してみる。メモリエラーで落ちたら終了。
配列確保する試行後、配列変数を解放した後に毎回ギャベージコレクタを強制的に起動してから次の試験をやってみています。
なお、細かい点ですが、ラズパイPicoとM5ATOM Liteで以下の微妙な差異があることにも気づきました。
-
- ラズパイPico、ギャベージコレクタはgcを明示的にimportしないと利用できない。
- M5ATOM Lite、 gcを明示的にimportしない状態でギャベージコレクタを使用できる。
M5ATOM Liteの方はどこか「裏」のコードのために設定済ということかもしれません。なお、末尾のソースは両方で走るように gcを明示的にimportしています。
mem info
スクリプトを起動した直後のメモリの状態を観察したものです。
-
- RPi
フリーメモリが186Kバイト以上あります。なお、mem_info()で出力されるマップの読み方は「マイクロコントローラ上のMicroPython」の下の方に書かれています。
stack: 732 out of 7936 GC: total: 192064, used: 5616, free: 186448 No. of 1-blocks: 52, 2-blocks: 19, max blk sz: 64, max free sz: 11292 GC memory layout; from 20007af0: 00000: h=Mhh.Dhh....D.DBh.h===BBh=Dh====B=BBBBBB.B=B.B=BBB.B=.B.B=Bh=== 00400: DB=h===========h===================BBBBBB..h=====h============== 00800: ===h============================================================ 00c00: ===h============================================================ 01000: ===............................................................. 01400: ...............h=======h=====h=BB..hh...h...........h=====...... 01800: ................hhh........S........hh...hh...h=................ 01c00: .........h=..........h=......................................... 02000: ..........Sh=................Sh=..........Sh=..................S 02400: h=...................Sh=............Sh=...................Sh=... 02800: ............................h=====h=====h======h======h========= 02c00: ========........................................................ (175 lines all free) 2ec00: ....................................
-
- M5ATOM Lite
物理メモリの搭載量はラズパイPicoより多いM5ATOM Liteですが、フリーメモリは約109KバイトとPicoより大分少ないです。反面スタックはPicoの倍ちかく確保してあるみたい。
stack: 1008 out of 15360 GC: total: 111168, used: 2496, free: 108672 No. of 1-blocks: 30, 2-blocks: 14, max blk sz: 18, max free sz: 6504 GC memory layout; from 3ffe4db0: 00000: h=.hh.MBBD.h=h=.hh=================hh=======h=======h=h......... 00400: .......BBB.hh...h.........h=====......................hhh....... 00800: .Sh=======h=====........hh...hh...h=.........................h=. 00c00: .........h=...................................................Sh 01000: =................Sh=..........Sh=..................Sh=.......... 01400: .........Sh=............Sh=...................Sh=............... 01800: ................h=====h=====h======h======h=================.... (101 lines all free) 1b000: ....................................
BYTE Array
最初は クラスarray.arrayでなく、組み込み型のbytearrayです。書き換え可能なbytes型。格納可能な数値は「バイト」のみ。1000バイトづつ確保する量を増やしながらエラーになるまで繰り返しコンストラクトしてみます。
-
- RPi
180Kバイトまでは確保でき、181Kバイトを確保しようとしてMemoryErrorで落ちました。先ほど確認したフリーメモリの上限にかなり近いところまで利用できている感じです。
SIZE: 1000 SIZE: 2000 ~途中略~ SIZE: 179000 SIZE: 180000 Traceback (most recent call last): File "<stdin>", line 5, in dutByteArray MemoryError: memory allocation failed, allocating 181000 bytes stack: 916 out of 7936 GC: total: 192064, used: 5856, free: 186208 No. of 1-blocks: 57, 2-blocks: 19, max blk sz: 64, max free sz: 11292
-
- M5ATOM Lite
こちも104Kバイトまで確保、105Kでコケました。やはりフリーメモリの上限に迫る勢い。
SIZE: 1000 SIZE: 2000 ~途中略~ SIZE: 103000 SIZE: 104000 Traceback (most recent call last): File "<stdin>", line 5, in dutByteArray MemoryError: memory allocation failed, allocating 105000 bytes stack: 1248 out of 15360 GC: total: 111168, used: 2752, free: 108416 No. of 1-blocks: 36, 2-blocks: 14, max blk sz: 18, max free sz: 6504
BYTE Initializer
こんどは、クラス array.array で符号なしバイト整数の配列を作った場合です。コンストラクタにイニシャライザ(イテレータ)を渡して配列を初期化しながら確保します。やはり1000バイト毎に確保量を増やしながらエラーになるまでやってみています。
-
- RPi
32Kバイトは確保できましたが、33Kバイトにトライして落ちたようです。しかし、MemoryErrorのメッセージを読むと、ほぼほぼ物理メモリの上限の262Kバイトをアロケートしようとしてフェイルとあります。推測するにイニシャライザが生成する「リスト」が使ってしまっているメモリ量が多いのではないか?と。
TYPE: B SIZE: 1000 TYPE: B SIZE: 2000 ~途中略~ TYPE: B SIZE: 31000 TYPE: B SIZE: 32000 Traceback (most recent call last): File "<stdin>", line 15, in dutInitializer File "<stdin>", line 15, in <listcomp> MemoryError: memory allocation failed, allocating 262144 bytes stack: 916 out of 7936 GC: total: 192064, used: 136992, free: 55072 No. of 1-blocks: 58, 2-blocks: 20, max blk sz: 8192, max free sz: 3100
-
- M5ATOM Lite
こちらも16Kバイトは確保できましたが、17Kバイトでダメでした。
TYPE: B SIZE: 1000 TYPE: B SIZE: 2000 ~途中略~ TYPE: B SIZE: 15000 TYPE: B SIZE: 16000 Traceback (most recent call last): File "<stdin>", line 15, in dutInitializer File "<stdin>", line 15, in <listcomp> MemoryError: memory allocation failed, allocating 131072 bytes stack: 1248 out of 15360 GC: total: 111168, used: 68336, free: 42832 No. of 1-blocks: 36, 2-blocks: 15, max blk sz: 4096, max free sz: 2408
コンストラクタにイニシャライザを渡せば、本格処理前に初期化済の配列を確保できますが、かなり気をつけないとエラーで落ちることが分かりました。
BYTE Append
次は、array.array クラス利用ですが、コンストラクタでは配列の型のみ決めて、容量は決めず、後から append で動的に「伸ばして」みました。単位は1バイト。イニシャライザで一気に確保するのに比べると、実行時間は相当かかっている感じ(ちゃんと時間計ってませんが、しばらく待ちます。)
-
- RPi
180673バイト目を確保しようとしてエラーになってました。先ほどのbytearray型の利用可能量に近いです。
Traceback (most recent call last): File "<stdin>", line 28, in dutAppend MemoryError: memory allocation failed, allocating 180680 bytes stack: 916 out of 7936 GC: total: 192064, used: 186544, free: 5520 No. of 1-blocks: 57, 2-blocks: 19, max blk sz: 11292, max free sz: 65 None Memory Error at x: 180673
-
- M5ATOM Lite
104065バイト目を確保しようとしてエラーです。やはりbytearray型に匹敵。
Traceback (most recent call last): File "<stdin>", line 28, in dutAppend MemoryError: memory allocation failed, allocating 104072 bytes stack: 1248 out of 15360 GC: total: 111168, used: 106976, free: 4192 No. of 1-blocks: 38, 2-blocks: 15, max blk sz: 6504, max free sz: 51 None Memory Error at x: 104065
この方法であれば bytearray型とほぼ同等の容量を確保できそうですが、人間が待つくらいの処理時間が問題かも。
LONG Initializer
続いて LONG型(符号なし4バイト整数)を array.arrayのイニシャライザでやってみました。
-
- RPi
16000要素=64Kバイト分を確保できました。先ほどのバイト型より容量そのものは多いです。これまたイニシャライザのマジック?
TYPE: L SIZE: 1000 TYPE: L SIZE: 2000 ~途中略~ TYPE: L SIZE: 15000 TYPE: L SIZE: 16000 Traceback (most recent call last): File "<stdin>", line 15, in dutInitializer MemoryError: memory allocation failed, allocating 68000 bytes stack: 916 out of 7936 GC: total: 192064, used: 136976, free: 55088 No. of 1-blocks: 59, 2-blocks: 19, max blk sz: 8192, max free sz: 3100
-
- M5ATOM Lite
8000要素=32Kバイト確保成功。この傾向はラズパイPicoと同じ。
TYPE: L SIZE: 1000 TYPE: L SIZE: 2000 ~途中略~ TYPE: L SIZE: 7000 TYPE: L SIZE: 8000 Traceback (most recent call last): File "<stdin>", line 15, in dutInitializer File "<stdin>", line 15, in <listcomp> MemoryError: memory allocation failed, allocating 65536 bytes stack: 1248 out of 15360 GC: total: 111168, used: 67600, free: 43568 No. of 1-blocks: 36, 2-blocks: 16, max blk sz: 2048, max free sz: 1750
ビット数の多い要素型であると、イニシャライザ方式もだんだん分が良くなるみたいです。
LONG Append
今度は LONG型で動的にAppendです。
-
- RPi
45158要素=180632バイト確保できました。先ほどのBYTE Appendとほぼ同等。
Traceback (most recent call last): File "<stdin>", line 28, in dutAppend MemoryError: memory allocation failed, allocating 180704 bytes stack: 916 out of 7936 GC: total: 192064, used: 186544, free: 5520 No. of 1-blocks: 57, 2-blocks: 19, max blk sz: 11292, max free sz: 65 None Memory Error at x: 45169
-
- M5ATOM Lite
26017要素=103068バイト確保。やはりBYTE Appendとほぼ同等
Traceback (most recent call last): File "<stdin>", line 28, in dutAppend MemoryError: memory allocation failed, allocating 104096 bytes stack: 1248 out of 15360 GC: total: 111168, used: 106976, free: 4192 No. of 1-blocks: 38, 2-blocks: 15, max blk sz: 6504, max free sz: 51 None Memory Error at x: 26017
ザックリしたサマリ
-
- バイトデータなら、組み込み型bytearrayが容量限界まで使える上、多分速い
- LONG型データ(多分バイト以外)なら、array.arrayクラスを使うしかない
- 速さならイニシャライザ方式(ちゃんと速度測らんかったケド。)
- 限界まで容量を使いたいなら動的Append方式
長々と書いた割には大した結論ではないな。すみません。
MicroPython的午睡(41) MQTTでPublish、M5STOM Lite へ戻る
MicroPython的午睡(43) MQTTでSubscribe、M5ATOM Lite へ進む
実験に使ったMicroPythonスクリプト全文
import uarray, usys, micropython, gc def dutByteArray(siz): try: bufArray = bytearray(siz) print("SIZE: {0}".format(siz)) return False except MemoryError as exc: usys.print_exception(exc) print(micropython.mem_info()) return True def dutInitializer(arrayT, siz): try: bufArray = uarray.array(arrayT, [0 for x in range(siz)]) print("TYPE: {0} SIZE: {1}".format(arrayT, siz)) return False except MemoryError as exc: usys.print_exception(exc) print(micropython.mem_info()) return True def dutAppend(arrayT): bufArray = uarray.array(arrayT) x = 1 try: while True: bufArray.append(0) x += 1 except MemoryError as exc: usys.print_exception(exc) print(micropython.mem_info()) print("Memory Error at x: ", x) def main(): gc.collect() print("---- mem_info ---") print(micropython.mem_info('verbose')) print("--- BYTE Array ---") for siz in range(1000, 256000, 1000): if dutByteArray(siz): break gc.collect() print("--- BYTE Initializer ---") for siz in range(1000, 256000, 1000): if dutInitializer('B', siz): break gc.collect() print("--- BYTE Append ---") dutAppend('B') gc.collect() print("--- LONG Initializer ---") for siz in range(1000, 256000, 1000): if dutInitializer('L', siz): break gc.collect() print("--- LONG Append ---") dutAppend('L') gc.collect() if __name__ == "__main__": main()