概要

M5Stack Core 2 (ESP32)には異なる種類のメモリが存在していて、それぞれの目的に応じて使用されています。メモリの種類によって転送速度にどのくらいの違いがあるのか、また、M5GFXを利用したグラフィックの処理で、スプライトを出力する pushSprite() がどう影響するのかを調べてみました。

2025-06-15 ベンチマークテスト (2-B) 追加しました

きっかけ

大量のスプライトを描画するプログラムを作成していたのですが、どうしても期待するパフォーマンスを得られず、どのくらいの速度が出るのかと気になってきました。createCanvas()やpushSprite()で実験をしていたところ、画像のサイズによって挙動が変わるポイントがあり、メモリのしくみについて調べることにしました。

ESP32のメモリは非常に複雑怪奇で、正直うまく理解できていません。以下のブログがとても詳しく解説しているので、こちらを参考にしてみてください。

ESP32のメモリレイアウトの理解と最適化
https://zenn.dev/paradoia/articles/ce34af18e74392

今回扱うメモリの種類

ESP32のメモリには大きく分けて、内蔵メモリと外部メモリがあります。内蔵メモリはプログラムが動作する領域のほか、データを保存する領域やその両方を兼ねた領域があります。データを保存する領域にはDMA対応の部分とそうでない部分があり、DMAではCPUを介さずに直接メモリ間で転送できるので高速なアクセスが可能です。

外部メモリにはPSRAM (SPI RAM)があり、M5Stack Core2 では8MBが使用可能です。また、プログラム中で const xxx PROGMEM のように変数を記述すると、読み出し専用のFlashメモリの方に保存されるようになります。容量の大きい画像データなどをFlash領域に保存すると、メモリを圧迫せずに済むので便利です。

ベンチマーク内容

今回は以下のテストを実施しました。

(1) 確保したメモリがどの領域に作成されたのか
malloc()で実際にどの領域にメモリを確保したのか、サイズごとに調査しました。

(2) メモリコピーの時間計測
(2-B) メモリコピーの時間計測 ☆追記☆
内部メモリ(DMAあり)、内部メモリ(DMAなし)、PSRAM、FLASH、それぞれの領域間での転送速度を調査しました。またサイズによって違いがあるのかテストしました。

(3) canvas.createSprite()するサイズで使用するメモリの場所が変わるかどうか
元々スプライトの描画パフォーマンスを調べていて今回のテストに至りました。作成するサイズで違いがあるのかをテストしました。

(4) pushSprite()の時間計測
内部メモリ、内部メモリ(PSRAM、FLASH)、ディスプレイ、それぞれの領域間でのスプライトの転送速度を、画像のサイズ別に調査しました。また、pushSprite()で透明色を指定した場合の違いについても調査しました。

今回使用したプログラム

今回ベンチマークに使用したプログラムは GitHub にアップしました。

表に出てくる名称の説明

“Type” について

今回のベンチマーク結果の表で Type とある行は、どのような方法でメモリを確保したかを意味しています。

表中の名称実際の命令
malloc()malloc()
ps_malloc()ps_malloc()
MALLOC_CAP_INTERNALheap_caps_malloc(size, MALLOC_CAP_INTERNAL)
MALLOC_CAP_DMAheap_caps_malloc(size, MALLOC_CAP_DMA)
MALLOC_CAP_SPIRAMheap_caps_malloc(size, MALLOC_CAP_SPIRAM)

malloc()はメモリの種類を指定せずにメモリを確保する関数で、ps_malloc()はPSRAMにメモリを確保します。またheap_caps_malloc()はメモリの種類を指定して確保します。

“From/To/Location” について

今回のベンチマーク結果で From, To, Location とある行は、メモリが実際にどこに確保されたかを意味しています。これはメモリのアドレスを元に、どこに確保されたかを判定しています。たとえば以下のようなものです。

in_SRAM_1_DMA /Data
in_SRAM_2_DMA /Data
in_SRAM_0_none /Inst
ex_SRAM_rw /Data
ex_Flash_r_a /Data

こちらの表の説明を短くしたもので、先頭の in_ は内部メモリ(internal)、ex_ は外部メモリ(external)、最後の /data はデータ用の領域、/inst は命令用の領域を意味しています。間の SRAM_0,1,2 は3つあるSRAMのどれかで、_a,b,c はさらに細かく分かれたエリアを示しています。_r,rwは読み込み専用かR/W可かです。簡潔に表記しようとしたところ、余計にわかりづらくなってしまいました。ほんと意味不明な書き方ですいません。

プログラムを開く メモリ判定ルーチン
String get_memory_region(void* ptr) {
  String region = "";
  uint32_t addr = (uint32_t)ptr;
  // Embeded Memory
  if      (addr >= 0x3FF80000 && addr <= 0x3FF81FFF) region = "RTC_FAST_Memory /Data"; // 8KB
  else if (addr >= 0x3FF90000 && addr <= 0x3FF9FFFF) region = "in_ROM_1 /Data";// 64KB  "Internal ROM 1";
  else if (addr >= 0x3FFAE000 && addr <= 0x3FFDFFFF) region = "in_SRAM_2_DMA /Data";// 200KB  "Internal SRAM 2 DMA";
  else if (addr >= 0x3FFE0000 && addr <= 0x3FFFFFFF) region = "in_SRAM_1_DMA /Data";// 128KB  "Internal SRAM 1 DMA";
  else if (addr >= 0x40000000 && addr <= 0x40007FFF) region = "in_ROM_0_a /Inst";// 32KB "Internal ROM 0";
  else if (addr >= 0x40008000 && addr <= 0x4005FFFF) region = "in_ROM_0_b /Inst";// 352KB "Internal ROM 0";
  else if (addr >= 0x40070000 && addr <= 0x4007FFFF) region = "in_SRAM_0_cache /Inst";// 64KB  "Internal SRAM 0 Cache";
  else if (addr >= 0x40080000 && addr <= 0x4009FFFF) region = "in_SRAM_0_none /Inst";// 128KB  "Internal SRAM 0 -";
  else if (addr >= 0x400A0000 && addr <= 0x400AFFFF) region = "in_SRAM_1_a /Inst";// 64KB  "Internal SRAM 1 (Inst)";
  else if (addr >= 0x400B0000 && addr <= 0x400B7FFF) region = "in_SRAM_1_b /Inst";// 32KB  "Internal SRAM 1 (Inst)";
  else if (addr >= 0x400B8000 && addr <= 0x400BFFFF) region = "in_SRAM_1_c /Inst";// 32KB  "Internal SRAM 1 (Inst)";
  else if (addr >= 0x400C0000 && addr <= 0x400C1FFF) region = "RTC_FAST_Memory /Inst";// 8KB
  else if (addr >= 0x50000000 && addr <= 0x50001FFF) region = "RTC_SLOW_Memory /Data+Isnt";// 8KB
  // External Memory
  else if (addr >= 0x3F400000 && addr <= 0x3F7FFFFF) region = "ex_Flash_r_a /Data";// 4MB  "External Flash";
  else if (addr >= 0x3F800000 && addr <= 0x3FBFFFFF) region = "ex_SRAM_rw /Data";// 4MB  "External RAM (PSRAM)";  
  else if (addr >= 0x400C2000 && addr <= 0x40BFFFFF) region = "ex_Flash_r_b /Inst";// 11512KB  "External Flash";
  else if (addr == 0) region = "Error";
  else region = "Other";
  return region;
}

ベンチマーク結果

ベンチマーク結果(1) 確保したメモリがどの領域に作成されたのか

malloc()で実際にどの領域にメモリを確保したのか、サイズごとに調査しました。結果を見ると、1152バイトの場合は内部メモリに確保していますが、4608バイト以上では外部のPSRAMに確保してますね。空きメモリは余裕があるのに、わざわざPSRAMを使っているのが不思議なところでした。

TypeSizeLocationAddress
malloc()1,152in_SRAM_2_DMA /Data3FFB2524
malloc()4,608ex_SRAM_rw /Data3F800884
malloc()18,432ex_SRAM_rw /Data3F800884
malloc()38,400ex_SRAM_rw /Data3F800884
malloc()64,000ex_SRAM_rw /Data3F800884
malloc()115,200ex_SRAM_rw /Data3F800884
malloc()153,600ex_SRAM_rw /Data3F800884

続いて他の場合も見ていきましょう。「表の続きを見る」をクリックすると表が表示されます。

表の続きを見る
TypeSizeLocationAddress
ps_malloc()1,152ex_SRAM_rw /Data3F800884
ps_malloc()4,608ex_SRAM_rw /Data3F800884
ps_malloc()18,432ex_SRAM_rw /Data3F800884
ps_malloc()38,400ex_SRAM_rw /Data3F800884
ps_malloc()64,000ex_SRAM_rw /Data3F800884
ps_malloc()115,200ex_SRAM_rw /Data3F800884
ps_malloc()153,600ex_SRAM_rw /Data3F800884
MALLOC_CAP_INTERNAL1,152in_SRAM_0_none /Inst40092148
MALLOC_CAP_INTERNAL4,608in_SRAM_0_none /Inst40092148
MALLOC_CAP_INTERNAL18,432in_SRAM_0_none /Inst40092148
MALLOC_CAP_INTERNAL38,400in_SRAM_0_none /Inst40092148
MALLOC_CAP_INTERNAL64,000in_SRAM_2_DMA /Data3FFC6188
MALLOC_CAP_INTERNAL115,200Error0
MALLOC_CAP_INTERNAL153,600Error0
MALLOC_CAP_DMA1,152in_SRAM_2_DMA /Data3FFB2934
MALLOC_CAP_DMA4,608in_SRAM_2_DMA /Data3FFB2964
MALLOC_CAP_DMA18,432in_SRAM_2_DMA /Data3FFC6188
MALLOC_CAP_DMA38,400in_SRAM_2_DMA /Data3FFC6188
MALLOC_CAP_DMA64,000in_SRAM_2_DMA /Data3FFC6188
MALLOC_CAP_DMA115,200Error0
MALLOC_CAP_DMA153,600Error0
MALLOC_CAP_SPIRAM1,152ex_SRAM_rw /Data3F800884
MALLOC_CAP_SPIRAM4,608ex_SRAM_rw /Data3F800884
MALLOC_CAP_SPIRAM18,432ex_SRAM_rw /Data3F800884
MALLOC_CAP_SPIRAM38,400ex_SRAM_rw /Data3F800884
MALLOC_CAP_SPIRAM64,000ex_SRAM_rw /Data3F800884
MALLOC_CAP_SPIRAM115,200ex_SRAM_rw /Data3F800884
MALLOC_CAP_SPIRAM153,600ex_SRAM_rw /Data3F800884
PROGMEM10ex_Flash_r_a /Data3F40C274

ps_malloc()はPSRAMに確保する命令なので、その通りPSRAMに確保されています。MALLOC_CAP_DMAはDMA転送可能な内部メモリに確保します。115,200バイト以上でエラーになっていますが、これはこのサイズの連続したメモリが確保できなかったからでしょう。以下はプログラム実行時の、空きメモリの量と、確保可能な連続した最大のサイズの結果です。たしかに足らないようです。

MALLOC_CAP_INTERNAL:   318,772 /   110,580
MALLOC_CAP_DMA     :   261,756 /   110,580
MALLOC_CAP_SPIRAM  : 4,192,124 / 4,128,756

不思議なのが MALLOC_CAP_INTERNAL は64,000バイトで別の場所にメモリを確保している点です。heap_caps_malloc()は場所を指定して確保する関数ですが、優先順位があるんでしょうかね。そのほかPSRAM、PROGMEMについては期待通りの結果でした。

ベンチマーク結果(2) メモリコピーの時間計測

内部メモリ(DMAあり)、内部メモリ(DMAなし)、PSRAM、FLASH、それぞれの領域間での転送速度を調査しました。

FromToSizeTime(us)MB/sResultFrom(type)To(type)
dmadma18,43248366.2Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmainternal18,43219391.1Passin_SRAM_2_DMA /Datain_SRAM_0_none /Inst
dmaspiram18,43251344.7Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
internaldma18,43218396.1Passin_SRAM_0_none /Instin_SRAM_2_DMA /Data
internalinternal18,43232853.6Passin_SRAM_0_none /Instin_SRAM_0_none /Inst
internalspiram18,43219192.0Passin_SRAM_0_none /Instex_SRAM_rw /Data
spiramdma18,43249358.7Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiraminternal18,43219888.8Passex_SRAM_rw /Datain_SRAM_0_none /Inst
spiramspiram18,43273024.1Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdma18,43273240.8Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemspiram18,43276231.3Passex_Flash_r_a /Dataex_SRAM_rw /Data

これを見ると違いが明確に現れますね。
DMA転送は350MB/sくらいで、DMAではない内部メモリの転送は56MB/sほど出ています。PSRAMとPROGMEM (Flash)は意外と速いように見えますが、これはサイズが小さいためキャッシュが影響している可能性があります。もう少し大きいサイズの結果も見てみましょう。「表を開く」をクリックすると全ての結果が表示されます。

表を開く
FromToSizeTime(us)MB/sResultFrom(type)To(type)備考
dmadma1,1523366.2Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmadma4,60812366.2Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmadma18,43248366.2Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmadma38,400100366.2Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmadma64,000167365.5Passin_SRAM_2_DMA /Datain_SRAM_1_DMA /Data
dmadma115,2000inf**Fail**ErrorError
dmadma153,6000inf**Fail**ErrorError
dmainternal1,1521291.6Passin_SRAM_2_DMA /Datain_SRAM_0_none /Inst
dmainternal4,6084891.6Passin_SRAM_2_DMA /Datain_SRAM_0_none /Inst
dmainternal18,43219391.1Passin_SRAM_2_DMA /Datain_SRAM_0_none /Inst
dmainternal38,40040291.1Passin_SRAM_2_DMA /Datain_SRAM_0_none /Inst
dmainternal64,000167365.5Passin_SRAM_2_DMA /Datain_SRAM_1_DMA /Data*1
dmainternal115,2000inf**Fail**ErrorError
dmainternal153,6000inf**Fail**ErrorError
dmaspiram1,1523366.2Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
dmaspiram4,60812366.2Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
dmaspiram18,43251344.7Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
dmaspiram38,4001,33127.5Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data*2
dmaspiram64,0004,43613.8Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data*2
dmaspiram115,2000inf**Fail**Errorex_SRAM_rw /Data
dmaspiram153,6000inf**Fail**Errorex_SRAM_rw /Data
internaldma1,1521199.9Passin_SRAM_0_none /Instin_SRAM_2_DMA /Data
internaldma4,6084597.7Passin_SRAM_0_none /Instin_SRAM_2_DMA /Data
internaldma18,43218396.1Passin_SRAM_0_none /Instin_SRAM_2_DMA /Data
internaldma38,40038196.1Passin_SRAM_0_none /Instin_SRAM_2_DMA /Data
internaldma64,000167365.5Passin_SRAM_2_DMA /Datain_SRAM_1_DMA /Data*1
internaldma115,2000inf**Fail**ErrorError
internaldma153,6000inf**Fail**ErrorError
internalinternal1,1522054.9Passin_SRAM_0_none /Instin_SRAM_0_none /Inst
internalinternal4,6088253.6Passin_SRAM_0_none /Instin_SRAM_0_none /Inst
internalinternal18,43232853.6Passin_SRAM_0_none /Instin_SRAM_0_none /Inst
internalinternal38,40038196.1Passin_SRAM_0_none /Instin_SRAM_2_DMA /Data*1
internalinternal64,000167365.5Passin_SRAM_2_DMA /Datain_SRAM_1_DMA /Data*1
internalinternal115,2000inf**Fail**ErrorError
internalinternal153,6000inf**Fail**ErrorError
internalspiram1,1521199.9Passin_SRAM_0_none /Instex_SRAM_rw /Data
internalspiram4,6084597.7Passin_SRAM_0_none /Instex_SRAM_rw /Data
internalspiram18,43219192.0Passin_SRAM_0_none /Instex_SRAM_rw /Data
internalspiram38,4001,53123.9Passin_SRAM_0_none /Instex_SRAM_rw /Data*2
internalspiram64,0004,44013.7Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data*2 *1
internalspiram115,2000inf**Fail**Errorex_SRAM_rw /Data
internalspiram153,6000inf**Fail**Errorex_SRAM_rw /Data
progmemdma1,1524274.7Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemdma4,60818244.1Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemdma18,43273240.8Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemdma38,40078346.8Passex_Flash_r_a /Datain_SRAM_2_DMA /Data*3
progmemdma64,0002,61023.4Passex_Flash_r_a /Datain_SRAM_2_DMA /Data*3
progmemdma115,2000inf**Fail**ex_Flash_r_a /DataError
progmemdma153,6000inf**Fail**ex_Flash_r_a /DataError
progmemspiram1,1524274.7Passex_Flash_r_a /Dataex_SRAM_rw /Data
progmemspiram4,60818244.1Passex_Flash_r_a /Dataex_SRAM_rw /Data
progmemspiram18,43276231.3Passex_Flash_r_a /Dataex_SRAM_rw /Data
progmemspiram38,4002,00018.3Passex_Flash_r_a /Dataex_SRAM_rw /Data*2
progmemspiram64,0006,7989.0Passex_Flash_r_a /Dataex_SRAM_rw /Data*2 (?)
progmemspiram115,20012,2719.0Passex_Flash_r_a /Dataex_SRAM_rw /Data*2 (?)
progmemspiram153,60016,3019.0Passex_Flash_r_a /Dataex_SRAM_rw /Data*2 (?)
spiramdma1,1523366.2Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramdma4,60812366.2Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramdma18,43249358.7Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramdma38,40074349.3Passex_SRAM_rw /Datain_SRAM_2_DMA /Data*2
spiramdma64,0002,43025.1Passex_SRAM_rw /Datain_SRAM_2_DMA /Data*2
spiramdma115,2000inf**Fail**ex_SRAM_rw /DataError
spiramdma153,6000inf**Fail**ex_SRAM_rw /DataError
spiraminternal1,1521291.6Passex_SRAM_rw /Datain_SRAM_0_none /Inst
spiraminternal4,6084891.6Passex_SRAM_rw /Datain_SRAM_0_none /Inst
spiraminternal18,43219888.8Passex_SRAM_rw /Datain_SRAM_0_none /Inst
spiraminternal38,4001,04235.1Passex_SRAM_rw /Datain_SRAM_0_none /Inst*2
spiraminternal64,0002,43125.1Passex_SRAM_rw /Datain_SRAM_2_DMA /Data*2 *1
spiraminternal115,2000inf**Fail**ex_SRAM_rw /DataError
spiraminternal153,6000inf**Fail**ex_SRAM_rw /DataError
spiramspiram1,1523366.2Passex_SRAM_rw /Dataex_SRAM_rw /Data
spiramspiram4,60814313.9Passex_SRAM_rw /Dataex_SRAM_rw /Data
spiramspiram18,43273024.1Passex_SRAM_rw /Dataex_SRAM_rw /Data*2 *4
spiramspiram38,4004,0549.0Passex_SRAM_rw /Dataex_SRAM_rw /Data*2
spiramspiram64,0006,7649.0Passex_SRAM_rw /Dataex_SRAM_rw /Data*2 (?)
spiramspiram115,20012,1679.0Passex_SRAM_rw /Dataex_SRAM_rw /Data*2 (?)
spiramspiram153,60016,2079.0Passex_SRAM_rw /Dataex_SRAM_rw /Data*2 (?)

サイズを大きくしていったときに変化があった部分を見ていきましょう。まずdma→dmaのErrorのところは、確保できるサイズを超えただけなので無視していいです。(以下同様)

備考 *1 のdma→internalで急激に速度が上がってるのは、転送先のメモリがDMA側に確保されてしまったからだと思われます。From(type)が転送元、To(type)が転送先の実際に確保された領域を意味しているのですが、”in_SRAM_0_none /Inst” だったものが “in_SRAM_1_DMA /Data” に変わっています。

備考 *2 の→spiramへの転送では、38,400バイト以降で急激に速度が低下しています。これはおそらく速い方がキャッシュの影響で、遅い方が本来のPSRAMの速度なのではないかと思ってます。そう考えると25MB/sあたりがPSRAMの速度なのでしょう。備考 *3 もおそらく同じ理由です。

spiram→spiramの結果をみると9MB/sほどで、読み書き両方で同じバスが使用されるためにさらに遅くなっているのだと思われます。PSRAM同士でコピーするような使い方はやめた方がいいですね。
備考 *4 のPSRAM同士の場合、速度低下が18,432バイトの段階で起こっています。もしかすると読み書き両方でキャッシュを使ってしまったからなのかなという気がしています。

最後にPROGMEM (Flash)の結果ですが、一番高速なはずのprogmem→dmaでキャッシュの影響が少ない64,000バイトの結果を見ると、23MB/sになっています。この辺がFlashの転送速度だと思われます。

まとめ

・DMA転送 350MB/s くらい
・DMAではない内部メモリ 56MB/s くらい
・PSRAM 25MB/s くらい
・PROGMEM (Flash) 23MB/s くらい
・PSRAMやFlashはキャッシュの恩恵により小サイズならもっと速い

ベンチマーク結果(2-B) メモリコピーの時間計測 詳細

内蔵メモリ(DMA)と外部メモリ(PSRAM)の組み合わせ別に、データサイズを更に細かくして測定してみました。使用したプログラムは こちら です。縦軸は転送スピードで上に行くほど高速です。横軸は転送したデータのバイト数です。

これを見ると16KBと32KBのあたりでキャッシュが作用している様子がよくわかりますね。青い線は内蔵メモリ(DMA)→内蔵メモリ(DMA)なので、サイズに関係なく高速です。最初が特に高いところは測定誤差でしょう。オレンジ色と灰色の線はメモリ(DMA)←→外部メモリ(PSRAM)の結果です。キャッシュの範囲を超えたあたりから本当の速度になっていきます。黄色はPSRAM同士です。32KBではなく16KBに変化点があるのが興味深いですね。WriteとReadでキャッシュが別々になってるってことなんですかね。全てのデータは以下の通りです。

全ての結果を表示する
Sizedma->dmadma->spiramspiram->dmaspiram->spiram
512488.3488.3488.3488.3
1,024488.3488.3488.3325.5
1,536366.2366.2366.2366.2
2,048390.6390.6390.6325.5
2,560406.9406.9406.9305.2
3,072366.2366.2366.2325.5
3,584379.8379.8379.8310.7
4,096390.6390.6390.6300.5
4,608366.2366.2366.2313.9
5,120375.6375.6375.6305.2
5,632383.7383.7383.7316.0
6,144366.2366.2366.2308.4
6,656373.4373.4373.4302.3
7,168379.8379.8379.8310.7
7,680366.2366.2366.2305.2
8,192372.0372.0372.0312.5
8,704377.3360.9377.3307.4
9,216366.2366.2366.2303.1
9,728371.1371.1371.1299.3
10,240375.6361.7361.7295.9
10,752366.2366.2366.2301.6
11,264370.4370.4370.4298.4
11,776374.4362.3362.3295.5
12,288366.2366.2366.2293.0
12,800369.9369.9369.9297.7
13,312373.4362.7362.7295.2
13,824366.2366.2366.2293.0
14,336369.5369.5369.5297.2
14,848372.6363.1363.1295.0
15,360366.2366.2366.2293.0
15,872369.2369.2369.2280.3
16,384372.0363.4363.4289.4
16,896366.2366.2366.273.6
17,408368.9368.9368.943.6
17,920371.5363.6363.631.2
18,432366.2351.6358.724.4
18,944368.7347.4354.220.2
19,456363.8350.1356.817.5
19,968366.2352.7359.315.6
20,480368.5348.8355.114.2
20,992364.0351.2357.513.1
21,504366.2347.6353.612.1
22,016368.4349.9355.911.4
22,528364.1352.2358.110.7
23,040366.2348.8354.410.2
23,552368.2351.0356.59.7
24,064364.3347.7358.69.4
24,576366.2349.8355.19.0
25,088368.1351.9357.19.0
25,600364.4348.8353.89.0
26,112366.2350.7355.89.0
26,624368.0347.8357.69.0
27,136364.5349.7354.59.0
27,648366.2351.6356.39.1
28,160367.9348.8353.49.1
28,672364.6350.6355.19.1
29,184366.2347.9356.89.1
29,696367.8349.6354.09.1
30,208364.7351.3355.79.1
30,720366.2344.7353.09.1
31,232367.7338.5350.49.1
31,744364.7325.5340.29.1
32,256366.2327.3341.89.0
32,768367.7322.2339.79.0
33,280364.8149.0208.89.0
33,792366.297.7152.79.0
34,304367.673.2121.29.0
34,816364.960.9102.59.0
35,328366.251.488.49.0
35,840367.544.677.99.0
36,352364.939.469.89.0
36,864366.235.463.39.0
37,376367.532.758.29.0
37,888365.030.453.99.0
38,400366.228.450.29.0
38,912367.426.547.29.0
39,424365.025.144.99.0
39,936366.223.642.49.0
40,448367.422.540.49.0
40,960365.121.539.09.0
41,472366.220.637.29.0
41,984367.319.835.79.0
42,496365.119.034.59.0
43,008366.218.433.49.0
43,520367.317.832.29.0
44,032365.217.231.49.0
44,544366.216.730.39.0
45,056367.316.329.59.0
45,568365.215.828.79.0
46,080366.215.428.09.0
46,592367.215.127.49.0
47,104365.214.726.89.0
47,616366.214.526.39.0
48,128367.214.326.09.0
48,640365.314.025.59.0
49,152366.213.725.19.0

次にキャッシュの影響を受けないようにコピー前にキャッシュをクリアし、データもランダムな値にして同様の実験をしてみました。

Sizedma->dmadma->spiramspiram->dmaspiram->spiram
4,096355.124.724.612.7
8,192355.124.724.712.3
12,288366.225.125.29.9
16,384363.424.024.08.9
20,480361.720.821.09.0
24,576366.219.419.59.0
28,672364.618.318.49.0
32,768363.417.517.79.0
36,864362.417.118.39.0
40,960365.116.618.99.0

内部メモリ(DMA)同士は 360MB/s くらい、外部メモリ(PSRAM)同士は 9MB/s くらい、内部と外部間は 24MB/s くらいのようです。

(3) canvas.createSprite()するサイズで使用するメモリの場所が変わるかどうか

canvas.createSprite() で作成するサイズで、確保されるメモリの場所に違いがあるのかをテストしました。usePsram(false)でPSRAMは使用しない設定にしています。

SizeWidthHeightResultAddressRegion
11522424Pass3FFB2524in_SRAM_2_DMA /Data
46084848Pass3FFB2524in_SRAM_2_DMA /Data
184329696Pass3FFC6188in_SRAM_2_DMA /Data
38400160120Pass3FFC6188in_SRAM_2_DMA /Data
64000200160Pass3FFC6188in_SRAM_2_DMA /Data
115200240240**Fail**0error
153600320240**Fail**0error

この結果、全て内部メモリの高速なDMA可能なエリアに作成されていることがわかりました。M5Stack Core 2の320×240ピクセルの画面全てが収まるキャンバスは大きすぎて内部メモリには作成できないので、大きなサイズはPSRAMに作成するしかありません。

ベンチマーク結果(4) pushSprite()の時間計測

最後にスプライトをキャンバスまたはLCDに出力する pushSprite() の速度を計測してみました。画像サイズ 96×96ピクセル、16ビットカラーでの結果です。

FromToColorWidthHeightSizeTimeMB/sResultFrom(used)To(used)
dmadisplay16969618,43236954.8Passin_SRAM_2_DMA /DataDisplay
dmadma16969618,43250351.6Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram16969618,43299717.6Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay16969618,43238474.6Passex_SRAM_rw /DataDisplay
spiramdma16969618,43252338.0Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram16969618,432159211.0Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdisplay16969618,43239004.5Passex_Flash_r_a /DataDisplay
progmemdma16969618,432100175.8Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemspiram16969618,432103217.0Passex_Flash_r_a /Dataex_SRAM_rw /Data

結果は先ほどのメモリコピーの場合と同じ傾向がみられました。DMA同士では爆速、PSRAMは低速な転送速度になりました。また、ディスプレイ(&M5.Display)への出力は転送元にかかわらず 4.5MB/s にとどまっています。

PROGMEMからの転送速度はpushSprite()ではなくpushImage()でテストしています。

続いて、以下は透明色を指定して pushSprite() をしたときの結果です。透明色を指定していない場合は単純にメモリをコピーするだけなので(きっと)高速ですが、透明色を指定した場合は各ピクセル毎に演算が必要になり、速度低下が予想されます。

FromToColorWidthHeightSizeTimeMB/sResultFrom(used)To(used)
dmadisplay16969618,43240704.3Passin_SRAM_2_DMA /DataDisplay
dmadma16969618,43295418.4Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram16969618,43299717.6Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay16969618,43240714.3Passex_SRAM_rw /DataDisplay
spiramdma16969618,43297018.1Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram16969618,432164910.7Passex_SRAM_rw /Dataex_SRAM_rw /Data

テストの結果、出力先がディスプレイの場合は影響は軽微なものの、DMAでの転送の場合は 18MB/s と大幅な低下がみられました。必要がなければ透明色は指定しない方がいいですね。残念ながら私がやりたかったのはスプライトの透過処理だったので、他に高速化する方法を考えなくてはなりません。

参考までに全てのサイズの結果を以下に示します。

表を表示する pushSprite() 透明色指定なし
FromToColorWidthHeightSizeTimeMB/sResultFrom(used)To(used)
dmadisplay1624241,1522394.6Passin_SRAM_2_DMA /DataDisplay
dmadma1624241,1525219.7Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram1624241,1526616.6Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay1624241,1522484.4Passex_SRAM_rw /DataDisplay
spiramdma1624241,1525219.7Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram1624241,1526616.6Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdisplay1624241,1522484.4Passex_Flash_r_a /DataDisplay
progmemdma1624241,1526183.1Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemspiram1624241,1526616.6Passex_Flash_r_a /Dataex_SRAM_rw /Data
dmadisplay1648484,6089304.7Passin_SRAM_2_DMA /DataDisplay
dmadma1648484,60814313.9Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram1648484,60824617.9Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay1648484,6089674.5Passex_SRAM_rw /DataDisplay
spiramdma1648484,60814313.9Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram1648484,60825017.6Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdisplay1648484,6089864.5Passex_Flash_r_a /DataDisplay
progmemdma1648484,60822199.8Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemspiram1648484,60824817.7Passex_Flash_r_a /Dataex_SRAM_rw /Data
dmadisplay16969618,43236954.8Passin_SRAM_2_DMA /DataDisplay
dmadma16969618,43250351.6Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram16969618,43299717.6Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay16969618,43238474.6Passex_SRAM_rw /DataDisplay
spiramdma16969618,43252338.0Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram16969618,432159211.0Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdisplay16969618,43239004.5Passex_Flash_r_a /DataDisplay
progmemdma16969618,432100175.8Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemspiram16969618,432103217.0Passex_Flash_r_a /Dataex_SRAM_rw /Data
dmadisplay1616012038,40076904.8Passin_SRAM_2_DMA /DataDisplay
dmadma1616012038,400102359.0Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram1616012038,400316711.6Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay1616012038,40080134.6Passex_SRAM_rw /DataDisplay
spiramdma1616012038,40073649.8Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram1616012038,40059146.2Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdisplay1616012038,40081804.5Passex_Flash_r_a /DataDisplay
progmemdma1616012038,40092439.6Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemspiram1616012038,40039629.2Passex_Flash_r_a /Dataex_SRAM_rw /Data
dmadisplay1620016064,000128124.8Passin_SRAM_2_DMA /DataDisplay
dmadma1620016064,000169361.2Passin_SRAM_2_DMA /Datain_SRAM_1_DMA /Data
dmaspiram1620016064,00075498.1Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay1620016064,000132734.6Passex_SRAM_rw /DataDisplay
spiramdma1620016064,000245924.8Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram1620016064,00098456.2Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdisplay1620016064,000135494.5Passex_Flash_r_a /DataDisplay
progmemdma1620016064,000266522.9Passex_Flash_r_a /Datain_SRAM_2_DMA /Data
progmemspiram1620016064,000100016.1Passex_Flash_r_a /Dataex_SRAM_rw /Data
dmadisplay16240240115,2000inf**Fail**ErrorDisplay
dmadma16240240115,2000inf**Fail**ErrorError
dmaspiram16240240115,2000inf**Fail**Errorex_SRAM_rw /Data
spiramdisplay16240240115,200238814.6Passex_SRAM_rw /DataDisplay
spiramdma16240240115,2000inf**Fail**ex_SRAM_rw /DataError
spiramspiram16240240115,200177056.2Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdisplay16240240115,200242864.5Passex_Flash_r_a /DataDisplay
progmemdma16240240115,2000inf**Fail**ex_Flash_r_a /DataError
progmemspiram16240240115,200179426.1Passex_Flash_r_a /Dataex_SRAM_rw /Data
dmadisplay16320240153,6000inf**Fail**ErrorDisplay
dmadma16320240153,6000inf**Fail**ErrorError
dmaspiram16320240153,6000inf**Fail**Errorex_SRAM_rw /Data
spiramdisplay16320240153,600320814.6Passex_SRAM_rw /DataDisplay
spiramdma16320240153,6000inf**Fail**ex_SRAM_rw /DataError
spiramspiram16320240153,600235736.2Passex_SRAM_rw /Dataex_SRAM_rw /Data
progmemdisplay16320240153,600323384.5Passex_Flash_r_a /DataDisplay
progmemdma16320240153,6000inf**Fail**ex_Flash_r_a /DataError
progmemspiram16320240153,600238836.1Passex_Flash_r_a /Dataex_SRAM_rw /Data
表を表示する pushSprite() 透明色指定あり
FromToColorWidthHeightSizeTimeMB/sResultFrom(used)To(used)
dmadisplay1624241,1523123.5Passin_SRAM_2_DMA /DataDisplay
dmadma1624241,1526616.6Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram1624241,1526616.6Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay1624241,1523123.5Passex_SRAM_rw /DataDisplay
spiramdma1624241,1526616.6Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram1624241,1526616.6Passex_SRAM_rw /Dataex_SRAM_rw /Data
dmadisplay1648484,60811163.9Passin_SRAM_2_DMA /DataDisplay
dmadma1648484,60824617.9Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram1648484,60824617.9Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay1648484,60811163.9Passex_SRAM_rw /DataDisplay
spiramdma1648484,60824617.9Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram1648484,60824617.9Passex_SRAM_rw /Dataex_SRAM_rw /Data
dmadisplay16969618,43240704.3Passin_SRAM_2_DMA /DataDisplay
dmadma16969618,43295418.4Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram16969618,43299717.6Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay16969618,43240714.3Passex_SRAM_rw /DataDisplay
spiramdma16969618,43297018.1Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram16969618,432164910.7Passex_SRAM_rw /Dataex_SRAM_rw /Data
dmadisplay1616012038,40081624.5Passin_SRAM_2_DMA /DataDisplay
dmadma1616012038,400196318.7Passin_SRAM_2_DMA /Datain_SRAM_2_DMA /Data
dmaspiram1616012038,400316811.6Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay1616012038,40081774.5Passex_SRAM_rw /DataDisplay
spiramdma1616012038,400261114.0Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram1616012038,40059036.2Passex_SRAM_rw /Dataex_SRAM_rw /Data
dmadisplay1620016064,000134414.5Passin_SRAM_2_DMA /DataDisplay
dmadma1620016064,000326018.7Passin_SRAM_2_DMA /Datain_SRAM_1_DMA /Data
dmaspiram1620016064,00075498.1Passin_SRAM_2_DMA /Dataex_SRAM_rw /Data
spiramdisplay1620016064,000134594.5Passex_SRAM_rw /DataDisplay
spiramdma1620016064,000554911.0Passex_SRAM_rw /Datain_SRAM_2_DMA /Data
spiramspiram1620016064,00098396.2Passex_SRAM_rw /Dataex_SRAM_rw /Data
dmadisplay16240240115,2000inf**Fail**ErrorDisplay
dmadma16240240115,2000inf**Fail**ErrorError
dmaspiram16240240115,2000inf**Fail**Errorex_SRAM_rw /Data
spiramdisplay16240240115,200240154.6Passex_SRAM_rw /DataDisplay
spiramdma16240240115,2000inf**Fail**ex_SRAM_rw /DataError
spiramspiram16240240115,200177056.2Passex_SRAM_rw /Dataex_SRAM_rw /Data
dmadisplay16320240153,6000inf**Fail**ErrorDisplay
dmadma16320240153,6000inf**Fail**ErrorError
dmaspiram16320240153,6000inf**Fail**Errorex_SRAM_rw /Data
spiramdisplay16320240153,600317194.6Passex_SRAM_rw /DataDisplay
spiramdma16320240153,6000inf**Fail**ex_SRAM_rw /DataError
spiramspiram16320240153,600235756.2Passex_SRAM_rw /Dataex_SRAM_rw /Data

全画面表示をした場合、320×240 (16bit) をPSRAM→Displayに転送すると 32ms 程度かかります。FPSに換算するとだいたい 31fps くらいですね。遅いPSRAMを使っても 30fps は確保できそう。速いDMAメモリを分割して出力してやれば速いかなとも思ったのですが、結局はディスプレイがボトルネックになっているので、どちらを使っても関係なさそうです。逆に複数のスプライトを重ね合わせるような処理をする場合は、DMA上で処理しておく方がよさそうですね。

まとめ

・ディスプレイ(LCD)への出力 4.5MB/s くらい
・320×240ピクセルで 31fps くらいが限界(?)
・PSRAMが20MB/sくらいでDMAなら350MB/s以上出る
・しかし最終的にはディスプレイがボトルネックなのでどっちでもいい

余談1: インスタンスの作成方法の違いでハマった件

以下のプログラムは一見同じ動作をするように見えますが、実際に createCanves() で 320×240 のキャンバスを作成してみると挙動が違うことがわかります。

M5Canvas canvas(&M5.Display);
setup() {
  canvas.createSprite(320, 240);
}
loop() {
  canvas.pushSprite(0, 0);
}
M5Canvas canvas;
setup() {
  canvas.createSprite(320, 240);
}
loop() {
  canvas.pushSprite(&M5.Display, 0, 0);
}

前者の場合はメモリ不足により createSprite() は失敗します。ですが後者は成功します。違いが何かというと、後者ではPSRAMにメモリを確保しているため成功していました。PSRAMを使用するかどうかは usePsram() で指定できますが、未指定の場合のデフォルト動作が変わるようです。最初原因がわからず、しばらく何が違うんだー!?と混乱してました。

余談2: TablePressプラグインを使ってみた

今回初めて TablePress というWordPressのプラグインを使ってみました。WordPress標準の表はとても使いにくく、大量のデータがある場合でもスプレッドシートから一気にコピーすることはできません。それならMarkDownで出力すれば表は簡単に作れそうです。しかし今回はソート機能を付けたいのです。調べてみるとTablePressというのがあったのでインストールしてみました。

表の作成は記事の編集画面ではなく、専用の画面で作成します。表を作成した後に記事で読み込むイメージです。この編集画面のすごいところは、なんとExcelからコピペができるところ。期待に100%応えてくれました!

と、関心していたのですが、数値を右寄せしようとして気づきました。セル内の位置を変更する機能が無い!? こんなに良くできたプラグインなのに、こんな基本的な機能がなぜないのか。調べてみるとCSSを追加するらしいです。あーこれは技術的な理由よりも思想的なやつだな…。

CSSの追加方法をネットで調べてみると難しいことをしている記事がたくさん出てきました。面倒だったので「カスタムHTML」でこう書いてみました。

<style>
.tablepress-id-1 .column-2 { text-align: right; }
.tablepress-id-1 .column-3 { text-align: center; }
</style>

右寄せヨシ!

LINEで送る
Pocket