編集履歴一覧に戻る
uchanのアイコン画像

uchan が 2023年03月05日17時41分08秒 に編集

初版

タイトルの変更

+

Arduino Due のメモリ消費量を測定する

タグの変更

+

Arduino

+

Arduino_Due

+

メモリマップ

+

ELF

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

セットアップや使用方法

本文の変更

+

Arduino Due の実行時のメモリ消費量を確認したいと思い、方法を調査しました。スタック領域とヒープ領域の消費量を測定する方法を紹介します。メモリ量が限られる Arduino の別機種(Arduino Uno など)への移植のために、メモリ消費量を削減する場合などに役立つと思います。 この記事では RAM の消費量を対象にします。プログラム本体を格納するためのフラッシュメモリ(ROM)の消費量は Arduino IDE でビルドすれば簡単に分かりますが、RAM の消費量はプログラムの実行のされ方によって異なるため、ビルドしただけでは正確に分かりません(静的解析)。この記事で紹介する方法はメモリ消費量を動的に測るため(動的解析)、個別の実行に関して正確な値が出ます。しかし、あらゆる実行で消費されるメモリの最大値を見積もるようなことはできません。 ## メモリの種別 RAM の使われ方は主に 3 つあります。静的領域、スタック領域、ヒープ領域です。静的領域にはグローバル変数や関数内 `static` 変数などが置かれます。スタック領域には主にローカル変数が置かれます。ヒープ領域には動的に確保された変数が置かれます。 Arduino には [String](https://www.arduino.cc/reference/en/language/variables/data-types/stringobject/) という文字列を格納するための型があります。これをローカル変数、あるいは関数の引数として使うと、スタック領域とヒープ領域のどちらも使用されます。文字列の長さを表す変数や、文字列そのものを指すポインタ変数など、管理用変数群(コンパイル時に大きさが確定する変数)がスタックに置かれます。文字列そのものは可変長データであり、ヒープに置かれます。そのため、RAM 使用量を調べるにはスタックだけではだめで、ヒープも調べる必要があるのです。 ## メモリ消費量の測定 メモリ消費量の測定方法は CPU の種類や使用するライブラリによって異なります。Arduino IDE では、CPU の種類を特定すればライブラリは自ずと一種類に決まると思いますが。 今回の対象は Arduino Due です。AVR を採用する Arduino Uno と異なり、Arcuino Due の CPU は Arm です。「Arduino メモリ消費量」などと調べて出てくる情報は `RAMEND`(End of RAM)マクロを使った AVR 向けのものが多いですが、今回はその方法が使えません。スタック領域の測定は、プログラム実行開始直後と測定点でのスタックポインタの差を見る方法を用いました。ヒープ領域の測定は、`sbrk` 関数の戻り値を調べる方法を使いました。 ```c uintptr_t getSP() { int i; return (uintptr_t)&i; } extern "C" char* sbrk(int incr); uintptr_t getHeapBottom() { return (uintptr_t)sbrk(0); } uintptr_t init_sp, init_heap; void showMemoryUsage() { uintptr_t sp = getSP(); uintptr_t heap = getHeapBottom(); Serial.print("SP = 0x"); Serial.print(sp, HEX); Serial.print(", Consumption = 0x"); Serial.print(init_sp - sp, HEX); Serial.print(" ("); Serial.print(init_sp - sp, DEC); Serial.println(")"); Serial.print("Heap = 0x"); Serial.print(heap, HEX); Serial.print(", Consumption = 0x"); Serial.print(heap - init_heap, HEX); Serial.print(" ("); Serial.print(heap - init_heap, DEC); Serial.println(")"); } void setup() { init_sp = getSP(); init_heap = getHeapBottom(); } ``` これが測定用プログラムです。`showMemoryUsage()` をメモリ消費量を調べたいところで呼び出すと、次のように測定結果がシリアルモニタに表示されます。 SP = 0x20087BA4, Consumption = 0x440 (1088) Heap = 0x20072000, Consumption = 0xBC8 (3016) SP = で表示されるアドレスはスタックポインタの値であり、スタックの先頭です。スタックが消費されるにつれて、この値は小さくなっていきます。 Heap = で表示されるアドレスは `sbrk(0)` が返す値であり、ヒープ領域の末尾です。ヒープが消費されるにつれて、この値は大きくなっていきます。 上記の出力結果は、今開発しているプログラムのメモリ消費量を調べたものです。`loop()` から `showMemoryUsage()` にいたる途中で、`String` 型の関数引数に対し 700 バイト超の文字列を渡す場面があります。そのためヒープ領域が大量に消費されていると推測されます。`String` の代わりに `const char *` を使うようにしたところ、次のような結果に変わりました。 SP = 0x20087BB4, Consumption = 0x430 (1072) Heap = 0x20071434, Consumption = 0x0 (0) `String` の管理用変数群の分だけスタックが 16 バイトだけ小さくなりました。また、文字列を置くための領域が不要になったためヒープが 3016 バイトも削減されました。とても大きな改善だと思います。`String` を使うバージョンでは RAM が 2KB しかない Arduino Uno では動かせませんが、新しいバージョンでは何とか動かせそうです。 ## 測定プログラムの注意点 上記の測定プログラムで表示されるのは `setup()` から目的の関数までに消費されたスタックとヒープです。`setup()` を実行するまでの間に消費された分は考慮されません。 Arduino Due のメモリマップをちょっと調べてみると、どうやら 0x20070000 から 0x20088000 までが RAM のようです[^arduino-memory-map]。前半 64KiB が SRAM0、後半 32KiB が SRAM1 だそうです。この情報に基づき、`setup()` の中で `init_sp` に `0x20088000` を設定すれば、すべてのスタック消費量を取得できるはずです。 [^arduino-memory-map]: https://forum.arduino.cc/t/due-sram-access-and-maximum-limit-of-size/454781/2 ヒープ領域についても考えてみます。0x20071434 というアドレスは何者でしょうか。ヒープ領域は静的データ領域の直後に置かれるはずですので、静的データ領域の大きさを知る必要があります。先ほどのプログラムについて、elf ファイルのセクション構造を確認したところ、次のようになっていました。 Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al 《中略》 [ 3] .relocate REL 20070000 018000 0008d8 08 WAX 0 0 8 [ 4] .bss NOBITS 200708d8 0188d8 000b60 00 WA 0 0 4 .bss が静的データ領域のことです。0x200708d8 から始まる 0xb60 バイトの領域ということは、それらを足せば終了アドレスは 0x20071438 と求まります。 0x20071438 はまさに `showMemoryUsage()` が表示したヒープ領域の末尾のアドレスと一致します。すなわち、`setup()` に到達するまでに一切ヒープが使われないか、使われていたとしても `setup()` に到達するまでにすべて解放されるということが分かりました。