MicroPython的午睡(42) array.array、意外に小さい使える領域

Joseph Halfmoon

別件でラズパイ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の日本語ドキュメントへのリンクを貼り付けました。

array — 数値データの配列

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の両方で走らせて、どこまでメモリ上にデータを格納できるものだか調べてみました。調べた手順は以下です。

    1. スクリプト起動直後、まだ配列確保していない状態でのメモリを確認
    2. シンプルなbytearrayで、byte型の配列をメモリエラーで落ちるまで確保してみる。
    3. array.arrayの符号なしバイト型の配列をコンストラクタにイニシャライザを渡す形でメモリ確保してみる。メモリエラーで落ちたら終了。
    4. array.arrayの符号なしバイト型の配列をコンストラクタ後、1要素づつAppendする形でメモリ確保してみる。メモリエラーで落ちたら終了。
    5. array.arrayの符号なしロング型(4バイト整数)の配列をコンストラクタにイニシャライザを渡す形でメモリ確保してみる。メモリエラーで落ちたら終了。
    6. 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()