3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

各種マイコンで数値計算してみた!

3
Last updated at Posted at 2025-09-13

始める前に

みなさま閲覧ありがとうございます。数日前に記事を投稿して、その後いくつか修正を加えました。内容は変わっていないのですが、コードのミスや説明不足なところを補足しました。結果の傾向が大きく変わったわけではありませんが、より正確な比較ができるようになったと思います。

特にATMega32U4の結果が信じがたいものだったので、コードを見直しました。そもそもintで扱える範囲が他のCPUとは違っています。これをちゃんと考慮しないと他のCPUとの比較が妥当ではありません。

また、ATMega32U4の結果が速すぎるのはなぜか?という点についても、コードを見直しました。違う関数を書いてみましたので、参考にしてください。

追記:ESP32P4を入手したので、そのデータを追加しました。あとで触れますが、RISC-V系チップの傾向を見るうえで面白いサンプルになりました。

はじめに

サイン波を再生するプログラムを書いていたのです。そこで、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
  • M5Stack Stamp P4
  • 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とあまり大きな差はないかも?って思ったら、FPU載ってないんだってさ。

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を整数にする意味はもはやない。

ESP32P4

  Model             : ESP32-P4
  CPU Frequency     : 360 MHz
float_normal() Execution time: 33498 microseconds
integer_normal() Execution time: 25127 microseconds
integer_underflow_fix() Execution time: 22335 microseconds
float_add() Execution time: 27917 microseconds
int_add() Execution time: 13954 microseconds

速い!クロック数は1.5倍だけど、それより速い。正直に書くと、コンパイラのバージョンが違う。が、バージョンをそろえても傾向は変化しないだろう。

ただし面白いのは、絶対的な速度は全チップ中トップなのに、float_normal()よりinteger_underflow_fix()のほうが速いという傾向はESP32C3・C6と同じであること。ESP32P4もESP32C3/C6と同じくRISC-Vコアなので、後述するがこれは偶然ではなさそうだ。

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搭載(RISC-V)
ESP32C6 160MHz 478,412 100,751 100,753 FPU搭載(RISC-V)
ESP32S3 240MHz 58,617 54,433 62,805 FPU搭載
ESP32P4 360MHz 33,498 25,127 22,335 FPU搭載(RISC-V・デュアルコア)
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は参考値のため除外

  • 最速: ESP32P4 (33,498μs)
  • 最遅: ATMega32U4 (13,632,928μs)
  • 速度差: 約407.0倍

性能比較(整数演算の相対速度)

※Intel Core I9は参考値のため除外

integer_normal() の比較

  • 最速: ESP32P4 (25,127μs)
  • 最遅: ATMega32U4 (14,393,728μs)
  • 速度差: 約572.8倍

integer_underflow_fix() の比較

  • 最速: ESP32P4 (22,335μs)
  • 最遅: ATMega32U4 (40,327,272μs)
  • 速度差: 約1805.6倍

演算方式別の性能ランキング

※Intel Core I9は参考値のため除外

浮動小数点演算 (float_normal)

  1. ESP32P4: 33,498μs
  2. ESP32S3: 58,617μs
  3. RP2350: 60,226μs
  4. nRF52840: 172,851μs
  5. ESP32C6: 478,412μs
  6. RP2040: 915,007μs
  7. ESP32C3: 1,037,713μs
  8. ATMega32U4: 13,632,928μs

整数演算 (integer_normal)

  1. ESP32P4: 25,127μs
  2. ESP32S3: 54,433μs
  3. RP2350: 87,030μs
  4. ESP32C6: 100,751μs
  5. nRF52840: 158,203μs
  6. ESP32C3: 207,578μs
  7. RP2040: 264,396μs
  8. ATMega32U4: 14,393,728μs

整数演算 (integer_underflow_fix)

  1. ESP32P4: 22,335μs
  2. ESP32S3: 62,805μs
  3. RP2350: 93,708μs
  4. ESP32C6: 100,753μs
  5. nRF52840: 157,227μs
  6. ESP32C3: 207,578μs
  7. RP2040: 264,375μs
  8. 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()を使用

ESP32P4 (360MHz, FPU搭載・RISC-V)

  • float_normal: 33,498μs
  • integer_underflow_fix: 22,335μs
  • 差: integerが約50.0%高速

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ライブラリが意外に最適化されている
  • RISC-V系(Espressif)は例外その2: ESP32P4はFPUを搭載していても、整数演算のほうが速い

RISC-Vチップの傾向について

今回追加したESP32P4も含めると、テストしたチップのうちESP32C3・ESP32C6・ESP32P4の3つがRISC-Vコアを採用している。この3チップに絞ってfloat_normal()とinteger_underflow_fix()の比を見てみる。

CPU float_normal (μs) integer_underflow_fix (μs) float/int 比
ESP32C3 1,037,713 207,578 約5.00倍
ESP32C6 478,412 100,753 約4.75倍
ESP32P4 33,498 22,335 約1.50倍

たしかに3チップともfloatよりintegerのほうが速く、傾向としては「整数演算のほうが速い」で一貫している。ただし差の大きさはかなりバラつきがあって、C3・C6では4〜5倍もの差がついているのに対し、P4では1.5倍程度まで縮まっている。

一方でARM系・Xtensa系のFPU搭載チップを見ると、RP2350(Cortex-M33)はfloatが35.7%高速、nRF52840(Cortex-M4F)はintegerが9.9%高速、ESP32S3(Xtensa)はfloatが6.7%高速と、float有利・integer有利が混在していて、こちらは「RISCだから」という単純な括りでは説明できない。ARM Cortex-M33/M4Fも広義にはRISCアーキテクチャだが、floatが優勢な場面がある。

なので「RISCチップは整数演算能力が高くfloatより明らかに速い」という傾向は、今回のデータではEspressif製RISC-Vチップ(C3・C6・P4)には確かに当てはまっているものの、P4のようにコアが強化されるとその差はかなり縮む。また同じRISC系でもARM Cortex-M33/M4Fでは逆転する場面もあるため、「RISCアーキテクチャそのもの」というより「各社のFPU実装・コンパイラの浮動小数点最適化の作り込み」が効いている可能性のほうが高そうだ。Espressif系RISC-Vコアの単精度float処理系(ソフトウェアフォールバックの有無や命令の粒度)がARMほど練れていない、という見方のほうが実態に近いかもしれない。

加算命令の比較

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搭載(RISC-V)
ESP32C6 160MHz 352,502 31,494 FPU搭載(RISC-V)
ESP32S3 240MHz 50,232 37,683 FPU搭載
ESP32P4 360MHz 27,917 13,954 FPU搭載(RISC-V)
nRF52840 64MHz 109,375 110,351 FPU搭載
ATMega32U4 16MHz 10,637,824 1,078,020 FPUなし

こちらの結果はおそらく妥当なのではないだろうか。ATMega32U4はやはり遅い。float_add()がint_add()の約10倍かかっている。

ESP32P4もfloat_add()がint_add()の約2倍かかっており、単純な加算だけを見てもRISC-V勢(C3・C6・P4)はいずれもintegerが明確に有利。ここでもP4は絶対速度こそ圧倒的だが、float/intの比率自体はC3・C6より縮まっている点は共通している。

感想

S3は優秀ですね。CPUクロックも一番高いのですが、速さとしては優秀ですね。続いてRP2350の浮動小数点計算の速さに興味を引かれました。

驚いたのがATMega32です。どうしてこんなに速いのかよくわからないのですが、速いです。

そして今回追加したESP32P4。絶対速度はダントツでトップなのだが、それでもfloatよりintegerのほうが速いという、ESP32C3・C6と同じ傾向を引き継いでいるのが興味深い。RISC-V系のマイコンだけを見ると「floatを避けて整数にする」意味はまだ残っているのかもしれない。ただし、その差はP4になってかなり縮んでいるので、今後のRISC-Vコア・ツールチェーンの成熟次第では、ARM系やXtensa系のようにfloatが有利になる日も遠くなさそうだ。少なくとも「RISCだから整数が速い」と一般化するのは早計で、同じRISCでもARM Cortex-M33/M4Fでは逆の結果も出ている以上、アーキテクチャよりもチップごとのFPU実装・コンパイラ側の最適化の差のほうが効いていそうというのが今回の感触。

当たり前といえば当たり前なのですが、FPUをちゃんと搭載しているCPUならば、浮動小数点演算を避けるために整数にすると、実は遅くなることがあるんだなということです。Intel Core I9での結果を見ると明らかですが、他のマイコンでも似たような結果になるのは時間の問題でしょう。S3とかRP2350ではもうfloatを使わない理由が遅いからというのは過去の話のようです。一方でEspressifのRISC-V系(C3・C6・P4)は今のところ整数優位が続いているので、チップ選びのときはアーキテクチャ名だけでなくベンダーごとの実装差も気にしたほうが良さそうだ。

ということで、私のコードで百万回なんか連続して呼び出すようなことはおそらく一生ないと思うので、floatをびしびし使っていきたいなと思いました(ただしESP32系のRISC-Vチップを使うときは整数化も一応検討する)。

これを読まれた方で、「なんか違うんじゃないの?」とか「条件が甘いでしょ!」とか改善提案とかありましたら教えていただけると嬉しいです。

マイコン選びの参考になれば幸いです。

参考情報

3
1
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?