ESP32シリーズの性能を比較してみる (M5Stack Core2 vs CoreS3 - PSRAMアクセス性能編)
こんにちは。ハードウェアチームの今井 @lovyan03 です。
M5Stackにも使用されているプロセッサ ESP32 シリーズですが、S2 , S3 , C3 など新しいラインナップが増えて来ました。それぞれ、高性能を目指しているもの、省電力を目指しているものなど、特徴があります。
各プロセッサの比較記事などもネットでよく見かけますが、性能差について詳しく言及しているものはあまり見受けられないように思います。
そこで、ESP32シリーズの性能比較を独自にやってみようと思います。比較できそうなネタが色々ありますので、複数回に分けて少しずつまとめて行きます。
今回は M5Stackユーザの皆さんが特に興味を持っていると思われる、M5Stackシリーズの Core2 と CoreS3 を用いて、基本性能の比較としてメモリアクセス速度のベンチマークを行います。
ESP32 と ESP32-S3 は、いずれもコア部分にTensilica社のXtensa LXシリーズを搭載しています。動作クロックはどちらも上限 240MHz となっていますが、ESP32にはLX6、ESP32-S3にはLX7が搭載されており、S3の方が世代がひとつ新しくなっています。この違いがどう結果に影響するでしょうか。
気になる結果の概要から
早速ですが、詳細説明は少々長くなりますので、先に結果の概要をお伝えします。
ESP32 = M5Stack Core2 / ESP32-S3 = M5Stack CoreS3
棒グラフの値は処理に要した時間を表しており、値が小さいほど高速であることを示しています。
PSRAMを使用した場合はCore2 (ESP32)よりも CoreS3 (ESP32-S3)の方が3割~4割ほど速い結果が得られたほか、SRAMのみを対象とした場合も、PSRAMほどの差ではないもののESP32-S3の方が速い結果が得られました。
検証の方法
それでは具体的な検証方法の詳細説明に移ります。
まず「ふたつのuint8_t配列の各要素それぞれを足し合わせ、結果を片方の配列に戻す」関数を用意します。この関数を呼び出す前後でのタイマー情報を取得しておき、関数の実行に要した時間を求めて表を作成します。比較対象の関数の内容は以下のようなものです。
void bench_add_u8(uint32_t len, void* mem0, void* mem1)
{
auto dst = (uint8_t*)mem0;
auto src = (uint8_t*)mem1;
for (uint32_t i = 0; i < len; ++i) {
dst[i] += src[i];
}
}
コンパイル時のビルドフラグには -Os を指定します。CPUの動作クロックは240MHz、FlashおよびPSRAMとの通信クロックは80MHzとします。
検証結果
配列がどちらもSRAMにある場合
まずはsrc、dstどちらの配列もSRAMにある場合を比較してみます。結果は以下の通りです。
20 kByte | 40 kByte | 60 kByte | 80 kByte | 100 kByte | |
---|---|---|---|---|---|
ESP32 | 1118 | 2234 | 3351 | 4468 | 5585 |
ESP32-S3 | 941 | 1886 | 2832 | 3778 | 4723 |
縦軸が結果の値になります。単位はマイクロ秒です。
横軸がデータのサイズになります。単位はキロバイト (kByte)です。
ループ1回につき1Byte進む処理をしますから、100kByteの 結果の値 × CPUクロックのMHz値 ÷ 100kByte を求めることで、ループ1回あたりに要したCPUサイクル数が判ります。
-
(ESP32) = 13 Cycle ( 5585 × 240 ÷ 102400 )
-
(ESP32-S3) = 11Cycle ( 4723 × 240 ÷ 102400 )
CPUの動作クロックが同じであれば、単純にサイクル数の差が処理速度の差となります。この差が生じた理由については次回以降の記事で詳しく掘り下げたいと思います。
dst:PSRAM src:SRAMの場合
20 kByte | 40 kByte | 60 kByte | 80 kByte | 100 kByte | |
---|---|---|---|---|---|
ESP32 | 1124 | 3920 | 7445 | 9946 | 12438 |
ESP32-S3 | 940 | 2831 | 4252 | 5669 | 7090 |
配列がどちらもPSRAMの場合
20 kByte | 40 kByte | 60 kByte | 80 kByte | 100 kByte | |
---|---|---|---|---|---|
ESP32 | 2652 | 6931 | 10172 | 13369 | 16583 |
ESP32-S3 | 1951 | 4857 | 7020 | 9178 | 11336 |
dst:SRAM src:PSRAMの場合
20 kByte | 40 kByte | 60 kByte | 80 kByte | 100 kByte | |
---|---|---|---|---|---|
ESP32 | 1150 | 3644 | 6539 | 8383 | 10225 |
ESP32-S3 | 940 | 2762 | 4007 | 5246 | 6486 |
PSRAMを対象に含めた場合、SRAMのみの場合と比べて処理時間の差が大きくなることがグラフから読み取れます。また、データサイズが小さいうちはESP32とESP32-S3の差は小さいですが、データサイズが一定サイズを超えると処理時間の差が大きく開きます。これはESP32とESP32-S3のメモリ管理ユニット (MMU = Memory Management Unit) のキャッシュ処理性能の差が表れていると推測できます。
MMUのキャッシュ容量は32kByteですから、配列のサイズが32kByte以下ならばすべてキャッシュに収まるため性能低下がないこと、32kByteを超えると性能が低下し始めていること、配列がどちらもPSRAMにある場合はキャッシュの取り合いになるため16kByteを超えると性能が低下し始めていること、などがグラフから読み取れます。
各プロセッサのSRAM対PSRAMでの比較
ここまで同一条件でのプロセッサ性能比較を見てきました。このデータを、同一プロセッサでの使用メモリの違いによる性能比較グラフにしてみます。
ESP32
ESP32-S3
グラフの開きが大きいほど、PSRAM使用時の性能低下が大きいと言えます。PSRAMを使用してもESP32-S3の方が性能低下が少ないことが読み取れます。
まとめ
今回行った性能比較では、PSRAMで32kByteを超える大きさのデータを扱う場合、性能差が大きく出る結果となりました。基本的にPSRAMを使用するとSRAMのみを使用した場合より処理速度が遅くなりますが、ESP32-S3ではかなり改善されているようです。
大きなデータを扱う際に有利であることは、例えば画像データのフレームバッファや、各種センサから高いサンプリングレートで得たデータの蓄積・集計、Webサーバ・クライアントとしての動作など、メモリを多く必要とする場面で特にESP32-S3を選ぶメリットがあると言えそうです。
これまでESP32で性能上の問題からPSRAMの使用を躊躇っていたケースでも、ESP32-S3ならばPSRAMの使用を検討してみても良いかも知れません。
次回はまた別の観点での比較記事をお届けしたいと思います。
最後に、比較に使用したプログラムを以下に置いておきます。
※ なおプログラムは次回以降にも使用するため、今回の記事で対象にしていない S2・C3 およびESP8266にも対応するように記述されています。
platformio.ini
[env]
platform_packages = platformio/tool-esptoolpy@^1.40501.0
platform = espressif32
framework = arduino
upload_speed = 1500000
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
board_build.f_flash = 80000000L
board_build.f_cpu = 240000000L
board_build.flash_mode = qio
build_type = release
build_flags = -Os -DCORE_DEBUG_LEVEL=5 -DBOARD_HAS_PSRAM
[env:esp32]
board = m5stick-c
[env:esp32s3]
board = esp32-s3-devkitc-1
board_build.arduino.memory_type = qio_qspi
build_flags = ${env.build_flags}
-DARDUINO_USB_CDC_ON_BOOT=1
-DCONFIG_TINYUSB_ENABLED=1
-DCONFIG_TINYUSB_CDC_ENABLED=1
bench_001.cpp
#if defined (ARDUINO)
#include <Arduino.h>
#else
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <rom/ets_sys.h>
#include <esp_timer.h>
#include <stdio.h>
#include <initializer_list>
void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
uint32_t micros(void) { return esp_timer_get_time(); }
#endif
#if __has_include(<soc/rtc.h>)
#include <soc/rtc.h>
#endif
// 検証に使用するメモリのポインタ setup内で準備する。[0]と[1]はSRAM, [2]と[3]はPSRAMを確保する
static void* memory_buf[4];
// 検証に使用するメモリのサイズ
static constexpr const size_t sram_len = 1024 * 100;
static constexpr const size_t psram_len = 1024 * 1000;
void prepare_add_u8(uint32_t len, void* mem0, void* mem1)
{
auto dst = (uint8_t*)mem0;
auto src = (uint8_t*)mem1;
for (uint32_t i = 0; i < len; ++i) {
dst[i] = i;
src[i] = i;
}
}
void bench_add_u8(uint32_t len, void* mem0, void* mem1)
{
auto dst = (uint8_t*)mem0;
auto src = (uint8_t*)mem1;
for (uint32_t i = 0; i < len; ++i) {
dst[i] += src[i];
}
}
uint32_t verify_add_u8(uint32_t len, void* mem0, void* mem1)
{
auto dst = (uint8_t*)mem0;
for (uint32_t i = 0; i < len; ++i) {
if ((uint8_t)(i*2) != dst[i]) { return i; }
}
return len;
}
struct test_set_t
{
void (*prepare)(uint32_t len, void* mem0, void* mem1);
void (*bench)(uint32_t len, void* mem0, void* mem1);
uint32_t (*verify)(uint32_t len, void* mem0, void* mem1);
uint16_t mem0_idx;
uint16_t mem1_idx;
const char* title;
};
static constexpr const test_set_t test_set_tbl[] = {
// 計測前処理の関数, 計測対象の関数,ベリファイ用の関数,メモリ配列番号,メモリ配列番号,処理内容の表示名称
{ prepare_add_u8, bench_add_u8, verify_add_u8, 0, 1, "dst:SRAM src:SRAM uint8_t dst[i] += src[i];" } ,
{ prepare_add_u8, bench_add_u8, verify_add_u8, 1, 2, "dst:SRAM src:PSRAM uint8_t dst[i] += src[i];" } ,
{ prepare_add_u8, bench_add_u8, verify_add_u8, 2, 3, "dst:PSRAM src:PSRAM uint8_t dst[i] += src[i];" } ,
{ prepare_add_u8, bench_add_u8, verify_add_u8, 3, 0, "dst:PSRAM src:SRAM uint8_t dst[i] += src[i];" } ,
};
static constexpr const uint8_t len_kib_tbl[] = { 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100 };
static constexpr const size_t test_number = sizeof(test_set_tbl) / sizeof(test_set_tbl[0]);
const char* target_name_table[] = { " SRAM", "PSRAM" };
#if defined (ESP8266)
static constexpr const char* cpu_name = "ESP8266 ";
#elif defined (CONFIG_IDF_TARGET_ESP32S3)
static constexpr const char* cpu_name = "ESP32-S3";
#elif defined (CONFIG_IDF_TARGET_ESP32S2)
static constexpr const char* cpu_name = "ESP32-S2";
#elif defined (CONFIG_IDF_TARGET_ESP32C3)
static constexpr const char* cpu_name = "ESP32-C3";
#else
static constexpr const char* cpu_name = "ESP32 ";
#endif
#if defined (CONFIG_SPIRAM_MODE_OCT)
static constexpr const char* bus_name = " OPI";
#else
static constexpr const char* bus_name = "QSPI";
#endif
#if defined ( CONFIG_SPIRAM_SPEED_120M )
static constexpr const int mem_freq = 120;
#elif defined ( CONFIG_SPIRAM_SPEED_80M )
static constexpr const int mem_freq = 80;
#elif defined ( CONFIG_SPIRAM_SPEED_40M )
static constexpr const int mem_freq = 40;
#elif defined ( CONFIG_SPIRAM_SPEED_20M )
static constexpr const int mem_freq = 20;
#else
static constexpr const int mem_freq = 0;
#endif
void try_test(uint8_t test_id, void* mem0, void* mem1)
{
auto prepare = test_set_tbl[test_id].prepare;
auto bench = test_set_tbl[test_id].bench;
auto verify = test_set_tbl[test_id].verify;
uint8_t dummy[32];
char log_buf[256];
uint32_t remain = 255;
auto b = log_buf;
for (auto len_kib : len_kib_tbl)
{
size_t len = len_kib * 1024;
if (len > sram_len) { break; }
unsigned long best = UINT32_MAX;
{
uint32_t c0, c1;
for (int i = 0; i < 4; ++i) {
if (prepare) { prepare(len, mem0, mem1); }
bench(16, dummy, dummy);
delay(10);
c0 = micros();
bench(len, mem0, mem1);
c1 = micros();
uint32_t result = c1 - c0;
if (best > result) { best = result; }
unsigned long v = verify ? verify(len, mem0, mem1) : len;
if (v != len) {
printf("verify error %lu / %u\n", v, len);
return;
}
}
}
auto l = snprintf(b, remain, ",%6lu", best);
b += l; remain -= l;
if (remain < 2) { break; }
}
#if defined (ESP8266)
int cpu_freq = esp_get_cpu_freq_mhz();
#else
int cpu_freq = ets_get_cpu_frequency();
#endif
printf("Test %d,%s:%s,%3u MHz,%3u MHz,%s,%s%s\n"
, test_id+1
, target_name_table[test_set_tbl[test_id].mem0_idx >> 1]
, target_name_table[test_set_tbl[test_id].mem1_idx >> 1]
, cpu_freq, mem_freq,bus_name,cpu_name, log_buf);
fflush(stdout);
delay(30);
};
void loop(void)
{
char log_buf[256];
for (size_t test_id = 0; test_id < test_number; ++test_id) {
auto mem0 = memory_buf[test_set_tbl[test_id].mem0_idx];
auto mem1 = memory_buf[test_set_tbl[test_id].mem1_idx];
if (mem0 == nullptr || mem1 == nullptr) { continue; }
printf("//------------------------------------------------\n");
printf("Test %d : %s\n", test_id+1, test_set_tbl[test_id].title );
int remain = 255;
auto b = log_buf;
for (auto len : len_kib_tbl) {
auto l = snprintf(b, remain, ",%3ukiB", len);
b += l; remain -= l;
}
printf("TestNo, Memory ,CPUfreq,Busfreq, Bus,CPU Name%s\n", log_buf);
fflush(stdout);
delay(40);
for (auto freq : { 80, 160, 240 })
{
#if defined (ESP8266)
if (esp_get_cpu_freq_mhz() != freq) { continue; }
#else
rtc_cpu_freq_config_t cfg;
rtc_clk_cpu_freq_mhz_to_config(freq, &cfg);
rtc_clk_cpu_freq_set_config_fast(&cfg);
if (ets_get_cpu_frequency() != freq) { continue; }
#endif
delay(20);
try_test(test_id, mem0, mem1);
}
}
delay(4096);
}
void setup()
{
delay(2048);
#if defined (ESP8266)
memory_buf[0] = malloc(sram_len);
memory_buf[1] = malloc(sram_len);
memory_buf[2] = nullptr;
memory_buf[3] = nullptr;
#else
memory_buf[0] = heap_caps_aligned_alloc(16, sram_len, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
memory_buf[1] = heap_caps_aligned_alloc(16, sram_len, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
memory_buf[2] = heap_caps_aligned_alloc(16, psram_len, MALLOC_CAP_SPIRAM);
memory_buf[3] = heap_caps_aligned_alloc(16, psram_len, MALLOC_CAP_SPIRAM);
#endif
for (int i = 0; i < 4; ++i) {
printf("mem%d addr:%08x \n", i, (uintptr_t)memory_buf[i]);
}
}