ESP32-S3 のメモリ性能を本気で測る: PSRAM / GDMA / Cache prefetch の実測値
はじめに
ESP32-S3 の性能上限は CPU クロックだけでなく PSRAM / MSPI 帯域にも左右されることがあります。Octal DDR 120 MHz は理論 240 MB/s ですが、実用上限は 55% 程度にとどまるケースが多く、datasheet の理論値だけで設計すると見積もりを外しやすい印象です。
この記事では レイテンシ / 帯域 / Cache 活用 の観点で、ESP32-S3 のメモリサブシステムを実測します。ターゲットは ESP32-S3-WROOM-1 (PSRAM 8 MB)、ESP-IDF 5.3、PSRAM Octal 120 MHz、Cache line 64 B。
TL;DR
CPU からの PSRAM read
| 条件 | 帯域 [MB/s] |
|---|---|
| L1 D-cache (32 KB) に収まる seq read | 239 |
| cache を超える seq read | 124.8 (MSPI 律速) |
| ランダムアクセス (4 MB) | 8.2 (レイテンシ律速) |
DMA (PSRAM → 内蔵 DRAM, M2M)
| 方法 | 起動コスト | 256 KB スループット [MB/s] |
|---|---|---|
esp_async_memcpy |
約 12 ms | ~17 |
| raw GDMA (descriptor 再利用) | 約 4 us | 79.8 |
固定オーバヘッドが乗るので、esp_async_memcpy は小転送には不向き。1 KB を運ぶと実効 0.08 MB/s 程度にとどまり、raw GDMA なら ~60 MB/s 出る、というオーダ感。
MSPI バスの競合
| シナリオ | CPU 帯域 [MB/s] | DMA 帯域 [MB/s] | 合計 [MB/s] |
|---|---|---|---|
| CPUのみ | 124.8 | — | 124.8 |
| DMAのみ 1ch | — | 79.7 | 79.7 |
| CPU + DMA 1ch (並走) | 67.4 (-46%) | 65.8 (-17%) | 133.2 |
| CPU + DMA 2ch (並走) | 67.3 | 66.0 | 133.3 |
- 並走時は DMA が優先される挙動。CPU の cache fill 帯域は半分程度に落ちる傾向で、DMA 側の落ち込みは小さい。
- MSPI 実用上限はおおむね ~133 MB/s 付近。本ベンチでは 2ch にしても大きくは超えなかった。
設計指針
- 重い streaming は
MALLOC_CAP_INTERNAL | MALLOC_CAP_DMAの内蔵 DRAM に逃がす (cache バイパス、両コア coherent、msync 不要) - L1 D-cache (32 KB) は別用途に温存
- PSRAM seq read は cache に収まる単位 (≤32 KB) でブロック化すると、本ベンチでは 2 倍程度の差が出た
環境
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_120M=y
CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y
CONFIG_ESP32S3_DATA_CACHE_SIZE=0x8000 # 32 KB shared L1 D-cache
計測は esp_timer_get_time() (1 us 解像度)、各サイズ 8 回平均。
実験 1: PSRAM の素の帯域とレイテンシ
for (i=0;i<n;i++) s += buf[i]; で測定。
| アクセスパターン | サイズ | 帯域 [MB/s] |
|---|---|---|
| seq read (cache 内) | 16 KB | 239 |
| seq read (MSPI 律速) | 4 MB | 124.8 |
| memcpy PSRAM→DRAM | 256 KB | 126 |
| random read | 4 MB | 8.2 |
- cache hit vs miss で約 2x。
- random read は 1 read あたり ~120 ns の MSPI レイテンシが律速で 8 MB/s。
- 理論 240 MB/s に対し cache miss seq でも 52% (transaction overhead)。
Cache line 32 B → 64 B のトレードオフ
デフォルトの 32 B cache line では seq read が 83 MB/s 止まり。CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y に変えると 124 MB/s まで伸びました (+49%)。
ただし cache line が大きくなる分、ランダムアクセス時の miss 1 回あたりに転送されるバイト数が増えるため、参照されない領域も含めて MSPI を引いてしまうケースが増えます。シーケンシャル read 中心のワークロードなら 64 B が有利、まばらに散ったランダム read が支配的なら 32 B のほうが MSPI の無駄が少なくなる可能性があります。ワークロードの性格に合わせて選ぶのが妥当そうです。
実験 2: esp_async_memcpy の罠
PSRAM → 内蔵 SRAM の M2M 転送を計測:
| 転送サイズ |
esp_async_memcpy 所要時間 [us] |
raw GDMA 所要時間 [us] |
|---|---|---|
| 64 B | 12,500 | 4 |
| 1 KB | 12,500 | 4 |
| 256 KB | ~15,000 | 3,280 |
転送サイズに依らず ~12 ms の固定オーバヘッド。原因は esp_async_memcpy が転送ごとに以下を実行しているため:
-
heap_caps_aligned_calloc()で descriptor 動的確保 -
esp_cache_msync()で cache flush / invalidate - ISR でコールバック発火、heap 解放
256 KB を 1 発で運ぶ用途なら ~17 MB/s で「使えなくもない」程度。「たまに大きな塊を運ぶ」用途専用と考えるのが安全。
実験 3: Raw GDMA を直接叩く
gdma_* API で descriptor を事前確保・再利用する。ポイント:
-
gdma_transfer_ability_t.psram_trans_align = 64で cache line と揃える (MSPI burst が割れず効率最大) - DMA は完了時に
owner=0を書き戻すので、CPU 側でowner=1への再武装が必要 - 起動は
gdma_start(rx)→gdma_start(tx)→ EOF 割り込み待ち、で submit overhead 4 us
Raw GDMA 帯域 (PSRAM → internal RAM)
| 転送サイズ | DMA 帯域 [MB/s] | CPU memcpy 帯域 [MB/s] |
|---|---|---|
| 1 KB | 49.2 | 357 |
| 16 KB | 76.9 | 370 |
| 64 KB | 79.2 | 126 |
| 256 KB | 79.8 | 126 |
- DMA は plateau が ~80 MB/s 付近 (MSPI 124 MB/s の 64% 程度)
- CPU memcpy は cache hit するサイズだと 5 倍程度速いケースもあった
- このベンチの範囲では DMA が CPU memcpy を上回る場面は見られず、価値は速度より「CPU と並列に動ける」点に出やすそうです
実験 4: CPU vs DMA の MSPI bus 競合
PRO_CPU で seq read、APP_CPU で DMA flooder を並走:
| シナリオ | CPU 帯域 [MB/s] | DMA 帯域 [MB/s] | 合計 [MB/s] |
|---|---|---|---|
| CPUのみ | 124.8 | 0 | 124.8 |
| DMAのみ 1ch | 0 | 79.7 | 79.7 |
| CPU + DMA 1ch (並走) | 67.4 | 65.8 | 133.2 |
単独時で正規化すると CPU は 54%、DMA は 83% 残る計算で、MSPI アービタは DMA 寄りに優先する挙動と読めます。
→ DMA を走らせている間、CPU 側の PSRAM cache miss は latency が増える傾向。LCD DMA を回しながら framebuffer を CPU で書き換える構成などで影響が出やすそうです。
実験 5: 2ch DMA を同時に走らせる
| シナリオ | DMA 帯域 [MB/s] | 合計 [MB/s] |
|---|---|---|
| DMAのみ 1ch | 79.4 | — |
| DMAのみ 2ch | 94.6 (+19%) | — |
| CPU + DMA 2ch (並走) | 66.0 | 133.3 |
合計帯域という観点では、2ch にしても +19% で 2 倍にはならず、CPU 並走時の合計も 1ch / 2ch ともに 133 MB/s 付近に収束しました (本ベンチ範囲)。MSPI が共有資源である以上、帯域だけを目的に 2ch にしても伸びは限定的、という見え方です。
ただし 2ch DMA にはそれぞれを独立に制御できるメリットがあります。例えば「LCD への streaming 用」と「センサバッファの取り込み用」を別々のタイミング・サイズで並走させたい、といった用途では 2ch 構成自体に価値があります。帯域目的か、独立制御目的かで採否が分かれそうです。
実験 6: Cache precharge (参考値)
cache を先に温めておく precharge は、precharge 中に他の処理を走らせて時間を隠蔽できれば効果が見込める手法。今回の測定は precharge 直後に同じデータを直列で read する構成で、隠蔽対象の処理を挟んでいないため、precharge そのもののコストがそのまま時間に乗る形になっています。参考値として載せておきます (隠蔽できる構成や working set が cache に収まる前提なら結果の見え方は変わりそう):
戦略 (全て同一コア):
- cold: invalidate してから読む
- touch: invalidate → CPU で 64 B stride で touch read → 本番 read
- preload: invalidate → ROM の
Cache_Start_DCache_Preload→ 本番 read
| サイズ | cold のみ [us] | touch + warm read [us] | 備考 |
|---|---|---|---|
| 16 KB (cache 内) | 132 | 194 | warm read は 237 MB/s で cache hit している |
| 64 KB (cache 外) | 516 | 1030 | working set が cache を超え precharge 中に evict |
- HW preload (
Cache_Start_DCache_Preload) も CPU touch とほぼ同等の所要時間。 - 直列実行の参考値であり、precharge が有効な構成 (PSRAM を触らない計算と並走、別コアでの touch、非同期 preload と本処理のオーバーラップなど) では別の結果になり得ます。
まとめ
経路別の実測帯域
| 経路 | 実測 [MB/s] |
|---|---|
| L1 D-cache hit (PSRAM, ≤32 KB) | 239 |
| PSRAM cache miss seq | 124.8 |
| PSRAM random read | 8.2 |
| 1ch GDMA PSRAM→DRAM | 79.8 |
| MSPI 実用上限 | ~133 |
| Internal DRAM | ~1000 |
設計ルール
-
esp_async_memcpyは小転送に使わない (12 ms 固定オーバヘッド) - Cache line サイズはワークロード次第。seq read 中心なら 64 B (32 B 比 +49%)、ランダムが多いなら miss あたりの転送量が小さい 32 B も検討
- DMA は MSPI 上で CPU より優先される挙動。並走時 CPU は半分程度に落ちる傾向 (本ベンチ観測)
- 2ch DMA は帯域目的だとスケールが鈍い (本ベンチ +19%)。一方で 2ch を独立に制御したい用途では別の価値あり
- Cache precharge は隠蔽前提の手法。precharge 時間を別タスクで隠せる構成でないと、直列で測ると単に 1 pass 分余計に見える
- 重い streaming は内蔵 DRAM (
MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA) に置く
補足: メモリマップ
| 領域 | アドレス | 実体 | access path | レイテンシ |
|---|---|---|---|---|
| internal IRAM | 0x4037_0000 |
内蔵 SRAM (命令属性) | 直接 | 数 ns (~1 GB/s) |
| internal DRAM | 0x3FC8_0000 |
内蔵 SRAM (データ属性) | 直接 | 数 ns (~1 GB/s) |
| PSRAM | 0x3C00_0000 |
外部 Octal SPI RAM | L1 D-cache 経由 | hit: ns オーダ / miss: hundreds of ns オーダ |
| Flash XIP | 0x4200_0000 |
外部 SPI Flash | L1 I-cache 経由 | hit: ns オーダ / miss: us オーダ |
- IRAM と DRAM は同じ物理 SRAM の別ビュー
- 内蔵 SRAM は CPU と DMA が同じ物理メモリを直接見るので msync 不要
- L1 D-cache は 両コア共有 32 KB (per-core ではない)
参考
一部AIで生成しています。チェックはしていますが悪しからず...