ドロイド君ダイエット作戦(ESP32のメモリ管理)

今回はメモリ管理の話なので、興味のない人にはとてもつまらない話です。残念…。

ドロイド君を作り始めの頃は、ESP32のリソースが豊かな事をいいことに、メモリの使い方がもうやりたい放題でした。

でっかいサイズの配列であっても、とりあえず静的に確保したり…。だって、楽ちんなんだもん…。

しかし、今回ある新機能を入れようと思ったらついに…。

ってエラーがでました。まったく!君も、成長したもんだねぇ!(喜と怒)

ってことで、適当に作っていた昔のコードも含めて、ちゃんとメモリ管理に目を向ける必要性が出てきたので、知識をまとめてみました。

でも、この手の類って、必要なんだけど、やっててもつまらないんだよなぁ…。(ぶつぶつ)

メモリの基礎知識

ROM/RAMの違い、ヒープ領域とスタック領域、.bssセクションと.dataセクション等の意味が分からない方は、まず以下のサイトをご覧ください。

コードサイズを聞かれたら(ユークエスト株式会社 様)

とても分かりやすくまとめられているので、以上をベースにして、まとめてみたいと思います。
(このような解説サイトを作るのって、結構な労力が必要なんですよね。とてもありがたいことです)

やりたいこと

今困っていることは、mallocに失敗することです。

なので、やりたいことは「ヒープ領域を確保できるよう、メモリに空きを作る」ことです。

なので、方向性としては「(大きなサイズで)静的に確保されている変数は、使うときだけ動的に確保するように変更」がセオリーです。
(元々そうしとけって突っ込みはなしで…)

esp-idfのメモリ空間

さて、まずは現状でメモリをどれくらい使っているのかを知る必要があります。

makeの際にsizeオプションをつけると、最後にメモリの使用量を表示することができます。

> make size
Total sizes:
 DRAM .data size:   13888 bytes
 DRAM .bss  size:   78624 bytes
Used static DRAM:   92512 bytes (  22688 available, 80.3% used)
Used static IRAM:   76660 bytes (  54412 available, 58.5% used)
      Flash code:  813486 bytes
    Flash rodata:  264792 bytes
Total image size:~1168826 bytes (.bin may be padded larger)

上記で表示されるのは「静的に確保しているメモリ量」のみなので、mallocなどの動的に確保する場合のメモリ量は含まれていません。

なので、この時点でメモリ使用量がギリギリの場合は、動的にメモリを確保する余裕なんてもちろんありません。

「DRAM」と「IRAM」

はて、「DRAM」と「IRAM」という2種類が出てきました。こりゃなんだ?

esp-idfのメモリ空間については、こちらの本家に記載があります。

IRAMは「instruction RAM」、DRAMは「data RAM」の事です。

詳細は上記リンク先を見て頂くとして、今重要なのは「ヒープ領域」がどちらで扱われるか、という点です。

通常のmallocだと、DRAMのほうで確保されるらしいので、今回のダイエットのターゲットは「DRAM」に絞られます。(後述しますが、IRAMに領域を確保するオプションもあるようです)

ダイエットすべきコードの抽出

次に、ダイエットすべき箇所を抽出するため、「(大きなサイズで)静的に確保されている変数」を探します。

コード量がそんなに多くなければ、全て追えば済む話です。

が、ファイル数が多くなると相当な手間だし、見落としも出てきます。

そこで、手っ取り早く静的なメモリ確保の内訳を知る方法として「mapファイルを見てみる」という手があります。

mapファイルの概要については、こちらのサイトにて、とてもわかりやすくまとめられています。

ここでは実作業を解説するため、esp-idfのhello_worldを変更して、以下のようなサンプルコードを考えてみます。
(ファイル名もwtk_main.cに変更)

コード内容に意味はありません。

#include <stdint.h>
#include <stdio.h>

float var1[1024];
uint32_t var2[1024] = { 2 };
const char var3[] = "Hello World!";

void FuncB() {
  static uint32_t var4[1024] = { 0 };
  printf( "%f, %d, %s, %d\n", var1[0], var2[0], var3, var4[0] );
  var4[0]++;
}

void FuncA() {
  for ( int var5 = 0; var5 < var2[0]; var5++ ) {
    FuncB();
  }
}

void app_main() {
  var1[0] = 1.0f;
  FuncA();
  var2[0] = 1;
  FuncA();

  var1[0] = 2.0f;
  FuncA();
}

このコードだと、var1,var2,var4はDRAM上に静的に確保され、使用されていない間もずっと(そこそこ大きなサイズの)メモリを食いっぱなしです。

このような変数を、コードからではなく、mapファイルから見つけ出してみます。

mapファイルからサイズの大きい変数を抽出

esp-idfでmakeした場合、buildフォルダ下に「~.map」というファイルが生成されます。

中身はテキストなので、適当なエディタで開いてみてください。

[Section]       [Addr]         [Size] [File]
.flash.rodata   0x3f400020     0x45c0
 .rodata.var3   0x3f402a4c        0xd ~/build/main\libmain.a(wtk_main.o)
                0x3f402a4c                var3

.dram0.data     0x3ffb0000     0x31e0
 .data.var2     0x3ffb1088     0x1000 ~/build/main\libmain.a(wtk_main.o)
                0x3ffb1088                var2

.dram0.bss      0x3ffb31e0     0x27e8
 .bss.var4$3006
                0x3ffb3714     0x1000 ~/build/main\libmain.a(wtk_main.o)

 COMMON         0x3ffb49c0     0x1000 ~/build/main\libmain.a(wtk_main.o)
                0x3ffb49c0                var1

こんな感じで、各セクションにおける「変数のメモリ確保サイズ」と「使用ファイル名」がズラ~っと並んでいます。
(ここでは、関係あるところだけ抽出しています)

今回のようにDRAMの使用量を減らしたいのであれば、「.dram0」以下に登場する変数の中で、サイズが大きいものを探します。

そうすると、自然とvar1,2,4が候補に挙がります。

そして、その変数が使用されている該当のソースコードとにらめっこし、動的なメモリ確保に変更できないか、という検討をするわけですね。

ちなみに、var5は一時的にスタック内に確保されるだけなので、mapファイルには登場しません。

ここまでの実験結果をまとめておきます。

該当変数   配置先  Section      意味							
  var1    dram0  .bss(common)  初期値を持たないグローバル変数
  var2    dram0  .data         非ゼロで初期化されるグローバル変数
  var3    flash  .rodata       const宣言、文字列リテラル等の定数データ(Read Only DATA)
  var4    dram0  .bss          ゼロ初期化されるグローバル変数(Block Started by Symbol)
 (Code)   flash  .text         プログラムの命令コード

mallocによるメモリ管理

ダイエット対象が決まれば、あとは実装を変更するだけです。

変更部分だけ、簡単に記載します(var1のダイエット例)。

#include <stdlib.h>  // ★追記(malloc, free用)

float var1[1024];
 ⇒ float *var1 = NULL; // ★変更

void app_main() {
  var1 = malloc( sizeof(float)*1024 );  // ★最初に追記
  ...
  free( var1 );  // ★最後に追記
}

これにより、malloc前、およびfree後においては、var1用の領域が解放されます。

このため、malloc前/free後における処理では、今までよりも余分にメモリを使うことができるようになったわけです。

esp-idf特有のmalloc

さて、メモリを動的に確保するには、上記のように普通mallocを使います。

ESP32でも同じように使えますが、こちらの本家の解説を見ると、heap_caps_malloc()という関数で、より細かいオプションを指定可能なようです。

ここでちょっと調べたのが「MALLOC_CAP_8BIT」と「MALLOC_CAP_32BIT」の使い分け。

通常のmallocは、MALLOC_CAP_8BITを指定したのと同じ動きらしいです。では、MALLOC_CAP_32BITのメリットとは?

本家の解説を見ると「32bit単位のみでアクセスするなら(例: int型配列やポインタ)、MALLOC_CAP_32BITを指定することで、IRAM領域も使えるよ。」という記載があります。

意図としては「DRAM領域が足りなくても、IRAM領域を使うことにより、よりメモリを有効活用できる」ということらしいです。
(演算速度も速かったりするのかな?)

はて、じゃあ「float」はMALLOC_CAP_32BITを使えるのかしら?

結論は「使えない」っぽいです。こちらで議論されてました。メモメモ。

ドロイド君のダイエット作戦の結末は…

ということで、以上の方法でダイエット対象を探しだし、メモリを動的確保に変更する、という作業を実施しました。

結果は…。

「あら、メモリ不足って怒られなくなったね。よっ、スリムだよっ!」

おかげで、新機能の仮テスト動作もうまく動くようになりました。

ちなみに、新機能とは「Bluetoothスピーカへ音楽を流す機能」なので、実装が完成したらまたご紹介しようと思います。

ではでは、また次回をお楽しみに。