始める前に
みなさま閲覧ありがとうございます。数日前に記事を投稿して、その後いくつか修正を加えました。内容は変わっていないのですが、コードのミスや説明不足なところを補足しました。結果の傾向が大きく変わったわけではありませんが、より正確な比較ができるようになったと思います。
特にATMega32U4の結果が信じがたいものだったので、コードを見直しました。そもそもintで扱える範囲が他のCPUとは違っています。これをちゃんと考慮しないと他のCPUとの比較が妥当ではありません。
また、ATMega32U4の結果が速すぎるのはなぜか?という点についても、コードを見直しました。違う関数を書いてみましたので、参考にしてください。
はじめに
サイン波を再生するプログラムを書いていたのです。そこで、PCMデータが1秒間に何サンプル必要化を求めようと思って、いくつか考えてみました。
- 周波数も時間もfloatとして考える
- 時間はミリ秒で考えて整数として考える
- 周波数も整数にしてしまう
ちょっと考えるだけでもこれだけあって、実際に動かしてみたらどうなるのだろう…、と考えてみました。
なんとなく知ってること
- floatって遅いんじゃないの?
- 整数使うと誤差が発生するんじゃないの?
- ちょっと考えておかないとオーバーフローしちゃうよね
これらを踏まえて、以下の3つを考えてみました。
実装
普通に実装
unsigned float_normal(float f, float d) {
float x = f * d;
return static_cast<unsigned>(x);
}
できるだけ浮動小数点を使って計算する。
整数を使って実装
unsigned integer_normal(unsigned f, unsigned d) {
unsigned x = (f / 1000) * d;
return x;
}
1ミリ秒が何サンプルになるかを考えて、それに時間をかけ合わせる。
誤差を減らすようにしたらこれ
unsigned integer_underflow_fix(unsigned f, unsigned d) {
unsigned x = (uint32_t(f) * d) / 1000;
return x;
}
dの大きさにもよるけれど、誤差は減らせるはず。ただ、(f*d)がATMEGA32U4のunsigned intを超えるので、uint32_tにキャストしてから計算する。これは結果を見るとわかるが大きな影響を受けている。
ATMega32U4がやたらに速い?
比較用に、1ずつ足す関数を用意した。
unsigned float_add() {
volatile float a = 0.0;
for (uint32_t i = 0; i < 50000; ++i) {
a += 1.0;
}
return static_cast<unsigned>(a);
}
unsigned int_add() {
volatile unsigned a = 0;
for (uint32_t i = 0; i < 50000; ++i) {
a += 1;
}
return a;
}
結果はいかに
呼び出し側のコード
様々な最適化をコンパイラは行う。このようなテストを行うときはどうにかしてコンパイラの最適化を抑制する必要がある。
コードは以下のようになる。
void measurement() {
constexpr unsigned repeated = 1000;
volatile unsigned x = 0;
volatile unsigned input_int = 500;
volatile float input_float = 0.5;
volatile unsigned freq_int = 22050;
volatile float freq_float = 22050.0;
auto start = micros();
// 繰り返し回数がATMEGA32U4のuintの範囲に収まるように、1000 * 1000で行う
for (unsigned i = 0; i < repeated; ++i) {
for (unsigned j = 0; j < repeated; ++j) {
x = float_normal(freq_float, input_float);
}
}
auto end = micros();
auto duration = end - start;
Serial.println(String("float_normal() Execution time: ") + String(duration) +
String(" microseconds"));
start = micros();
for (unsigned i = 0; i < repeated; ++i) {
for (unsigned j = 0; j < repeated; ++j) {
x = integer_normal(freq_int, input_int);
}
}
end = micros();
duration = end - start;
Serial.println(String("integer_normal() Execution time: ") +
String(duration) + String(" microseconds"));
start = micros();
for (unsigned i = 0; i < repeated; ++i) {
for (unsigned j = 0; j < repeated; ++j) {
x = integer_underflow_fix(freq_int, input_int);
}
}
end = micros();
duration = end - start;
Serial.println(String("integer_underflow_fix() Execution time: ") +
String(duration) + String(" microseconds"));
start = micros();
for (unsigned i = 0; i < 20; ++i) {
x = float_add();
}
end = micros();
duration = end - start;
Serial.println(String("float_add() Execution time: ") + String(duration) +
String(" microseconds"));
start = micros();
for (unsigned i = 0; i < 20; ++i) {
x = int_add();
}
end = micros();
duration = end - start;
Serial.println(String("int_add() Execution time: ") + String(duration) +
String(" microseconds"));
}
platformio.iniは下記のようになる。
[env:seeed_xiao_rp2040]
platform = raspberrypi
board = seeed_xiao_rp2040
framework = arduino
build_flags = -O0
build_unflags = -flto
まだまだ考えなければならないことはあるとは思うけれど、傾向は見えるようになったと思うので、これで各種マイコンでやってみよう。
エントリーしたマイコンたち
- XIAO RP2040
- Raspberry Pi Pico2 RP2350
- XIAO ESP32C3
- XIAO ESP32C6
- XIAO ESP32S3
- XIAO nRF52840
- Arduino Leonardo
- Intel Core I9 10900K
LeonardoとRP2040にはFPUが搭載されていないらしい。これを踏まえて、どんな結果になるか楽しみである。では上からやってみよう。
RP2040
HARDWARE: RP2040 133MHz, 256KB RAM, 2MB Flash
float_normal() Execution time: 915007 microseconds
integer_normal() Execution time: 264396 microseconds
integer_underflow_fix() Execution time: 264375 microseconds
float_add() Execution time: 664611 microseconds
int_add() Execution time: 68009 microseconds
なるほど、float使うと4倍くらい遅いらしい。こりゃ整数にするかもね。
RP2350
HARDWARE: RP2350 150MHz, 512KB RAM, 4MB Flash
float_normal() Execution time: 60226 microseconds
integer_normal() Execution time: 87030 microseconds
integer_underflow_fix() Execution time: 93708 microseconds
float_add() Execution time: 46852 microseconds
int_add() Execution time: 33477 microseconds
RP2040との速度差にもびっくりなんだけど、FPUの効果が絶大。こりゃfloatを避ける意味なんてない。
ESP32C3
HARDWARE: ESP32C3 160MHz, 320KB RAM, 4MB Flash
float_normal() Execution time: 1037713 microseconds
integer_normal() Execution time: 207578 microseconds
integer_underflow_fix() Execution time: 207578 microseconds
float_add() Execution time: 566049 microseconds
int_add() Execution time: 44049 microseconds
FPU載ってるらしいんだけどなあ。RP2040とあまり大きな差はないかも?
ESP32C6
HARDWARE: ESP32C6 160MHz, 320KB RAM, 4MB Flash
float_normal() Execution time: 478412 microseconds
integer_normal() Execution time: 100751 microseconds
integer_underflow_fix() Execution time: 100753 microseconds
float_add() Execution time: 352502 microseconds
int_add() Execution time: 31494 microseconds
C3に比べると随分速くなっている。だけど、傾向は変わらない。floatの計算を整数計算に置き換えてやるメリットはある。
ESP32S3
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
float_normal() Execution time: 58617 microseconds
integer_normal() Execution time: 54433 microseconds
integer_underflow_fix() Execution time: 62805 microseconds
float_add() Execution time: 50232 microseconds
int_add() Execution time: 37683 microseconds
さすが、速い。floatを整数にする意味はもはやない。
nRF52840
HARDWARE: NRF52840 64MHz, 232KB RAM, 792KB Flash
float_normal() Execution time: 172851 microseconds
integer_normal() Execution time: 158203 microseconds
integer_underflow_fix() Execution time: 157227 microseconds
float_add() Execution time: 109375 microseconds
int_add() Execution time: 110351 microseconds
こちらもfloatを使わなくしても速度に影響は殆どない。というより、これは優秀。クロック数比較してもS3の1/4なのだから。
ATMega32
HARDWARE: ATMEGA32U4 16MHz, 2.50KB RAM, 28KB Flash
float_normal() Execution time: 13632928 microseconds
integer_normal() Execution time: 14393728 microseconds
integer_underflow_fix() Execution time: 40327272 microseconds
float_add() Execution time: 10637824 microseconds
int_add() Execution time: 1078020 microseconds
にわかに信じがたいのだが、こうなった。アセンブラを見てみたが、float演算はcall命令だけみたい。スタック操作も起きていないように見えるのはなぜ?!クロックに比例して遅い。long intを使うととたんに遅くなるようだ。floatの足し算で十分時間がかかっているので、こちらのほうが本来の実力なのだろう。volatile宣言しているのだけれどコンパイル時に定数が使い回されちゃっていて、float_normal()が速く見えたのかもしれない。
Intel Core I9 10900
Hardware: Intel Core I9 10900K (3.6GHz / 5GHz)
float_normal() Execution time: 2034 microseconds
integer_normal() Execution time: 2989 microseconds
integer_underflow_fix() Execution time: 5624 microseconds
はい、流石にPCは速いです。参考程度に。
結果のまとめ
| CPU | クロック | float_normal (μs) | integer_normal (μs) | integer_underflow_fix (μs) | 備考 |
|---|---|---|---|---|---|
| RP2040 | 133MHz | 915,007 | 264,396 | 264,375 | FPUなし |
| RP2350 | 150MHz | 60,226 | 87,030 | 93,708 | FPU搭載 |
| ESP32C3 | 160MHz | 1,037,713 | 207,578 | 207,578 | FPU搭載 |
| ESP32C6 | 160MHz | 478,412 | 100,751 | 100,753 | FPU搭載 |
| ESP32S3 | 240MHz | 58,617 | 54,433 | 62,805 | FPU搭載 |
| nRF52840 | 64MHz | 172,851 | 158,203 | 157,227 | FPU搭載 |
| ATMega32U4 | 16MHz | 13,632,928 | 14,393,728 | 40,327,272 | FPUなし |
| Intel Core I9 | 3.6GHz | 2,034 | 2,989 | 5,624 | 参考値 |
性能比較(浮動小数点演算の相対速度)
※Intel Core I9は参考値のため除外
- 最速: ESP32S3 (58,617μs)
- 最遅: ATMega32U4 (13,632,928μs)
- 速度差: 約232.6倍
性能比較(整数演算の相対速度)
※Intel Core I9は参考値のため除外
integer_normal() の比較
- 最速: ESP32S3 (54,433μs)
- 最遅: ATMega32U4 (14,393,728μs)
- 速度差: 約264.4倍
integer_underflow_fix() の比較
- 最速: ESP32S3 (62,805μs)
- 最遅: ATMega32U4 (40,327,272μs)
- 速度差: 約642.4倍
演算方式別の性能ランキング
※Intel Core I9は参考値のため除外
浮動小数点演算 (float_normal)
- ESP32S3: 58,617μs
- RP2350: 60,226μs
- nRF52840: 172,851μs
- ESP32C6: 478,412μs
- RP2040: 915,007μs
- ESP32C3: 1,037,713μs
- ATMega32U4: 13,632,928μs
整数演算 (integer_normal)
- ESP32S3: 54,433μs
- RP2350: 87,030μs
- ESP32C6: 100,751μs
- nRF52840: 158,203μs
- ESP32C3: 207,578μs
- RP2040: 264,396μs
- ATMega32U4: 14,393,728μs
整数演算 (integer_underflow_fix)
- ESP32S3: 62,805μs
- RP2350: 93,708μs
- ESP32C6: 100,753μs
- nRF52840: 157,227μs
- ESP32C3: 207,578μs
- RP2040: 264,375μs
- ATMega32U4: 40,327,272μs
FPU効果の比較
FPU搭載による浮動小数点演算の改善
RP2040 (FPUなし) vs RP2350 (FPU搭載)
- RP2040: 915,007μs
- RP2350: 60,226μs
- 改善率: 約15.2倍の高速化
精度を保つ演算での比較(float_normal vs integer_underflow_fix)
※integer_normal()は除算による誤差を含むため、正確な比較にはinteger_underflow_fix()を使用
ESP32S3 (240MHz, FPU搭載)
- float_normal: 58,617μs
- integer_underflow_fix: 62,805μs
- 差: floatが6.7%高速(FPUによる最適化)
RP2350 (150MHz, FPU搭載)
- float_normal: 60,226μs
- integer_underflow_fix: 93,708μs
- 差: floatが35.7%高速(FPUの威力)
nRF52840 (64MHz, FPU搭載)
- float_normal: 172,851μs
- integer_underflow_fix: 157,227μs
- 差: integerが9.9%高速(僅差)
FPU効果のまとめ
- FPU搭載の効果は絶大: RP2040→RP2350で15倍の性能向上
- 現代マイコンではfloat使用推奨: ESP32S3、RP2350、nRF52840ではfloatが実用的
- FPUなしでは整数演算が有利: RP2040では整数演算が3.5倍高速
- 8bitマイコンは例外: ATMega32U4ではfloatライブラリが意外に最適化されている
加算命令の比較
ATMEGA32U4の結果に疑問を感じたので、加算命令の実行時間も測定してみた。
| CPU | クロック | float_add (μs) | int_add (μs) | 備考 |
|---|---|---|---|---|
| RP2040 | 133MHz | 664,611 | 68,009 | FPUなし |
| RP2350 | 150MHz | 46,852 | 33,477 | FPU搭載 |
| ESP32C3 | 160MHz | 566,049 | 44,049 | FPU搭載 |
| ESP32C6 | 160MHz | 352,502 | 31,494 | FPU搭載 |
| ESP32S3 | 240MHz | 50,232 | 37,683 | FPU搭載 |
| nRF52840 | 64MHz | 109,375 | 110,351 | FPU搭載 |
| ATMega32U4 | 16MHz | 10,637,824 | 1,078,020 | FPUなし |
こちらの結果はおそらく妥当なのではないだろうか。ATMega32U4はやはり遅い。float_add()がint_add()の約10倍かかっている。
感想
S3は優秀ですね。CPUクロックも一番高いのですが、速さとしては優秀ですね。続いてRP2350の浮動小数点計算の速さに興味を引かれました。
驚いたのがATMega32です。どうしてこんなに速いのかよくわからないのですが、速いです。
当たり前といえば当たり前なのですが、FPUをちゃんと搭載しているCPUならば、浮動小数点演算を避けるために整数にすると、実は遅くなることがあるんだなということです。Intel Core I9での結果を見ると明らかですが、他のマイコンでも似たような結果になるのは時間の問題でしょう。S3とかRP2350ではもうfloatを使わない理由が遅いからというのは過去の話のようです。
ということで、私のコードで百万回なんか連続して呼び出すようなことはおそらく一生ないと思うので、floatをびしびし使っていきたいなと思いました。
これを読まれた方で、「なんか違うんじゃないの?」とか「条件が甘いでしょ!」とか改善提案とかありましたら教えていただけると嬉しいです。
マイコン選びの参考になれば幸いです。