公開日:2021/05/08
title.png

16th Season


#16-1 見せてもらおうか、Xtensa LX6コアの性能とやらを

 去年(2020年)40周年を迎えた「アレ」のオマージュでタイトルを付けたら、タイトル長いな……。それは兎も角、今回のテーマはESP32に搭載されたCPU Xtensa LX6コアの性能、特に、浮動小数点演算に関する実力についてフォーカスしてみたいと思います。

 ESP32をArduino IDEで開発する環境立ち上げに関しては「#14-10 Arduino IDEのESP32開発環境」をご確認ください。

Xtensaコア

 あんまり詳しくは無いので調べたところ、XtensaコアはアメリカTensilica社が開発したCPUコアだそうです。現在は、アメリカCadence社に買収されています。Cadence社と言えば、半導体集積回路開発用EDA(Electronic Design Automation)ツールの会社として有名ですので、同社のツールを使っている方も多いのではないでしょうか?

 Xtensaコアは、Cadence社からライセンスを受けると、自社のFPGA(Field Programmable Gate Array)なりASIC(Application Specific Integrated Circuit)なり、集積回路なりに組み込むことが出来るCPUコアになっています。同様なビジネスモデルには、ARM社のARMコア(Cortex-A,R,MやARM7,11等)があります。

 手元のマイコンボードでは、Xtensa LX6コアを搭載しているのがESP32ボード(下左図)で、Cortex-M3コアを搭載しているのがArduino DUEボード(下右図)となりますので、必然的にこの2機種を比較することになります。

ar41_esp32.jpga119_due.jpg

 ただし、ESP32 @240MHzに対してDUE @84MHzと、そもそも動作クロック周波数が異なりますので、双方とも「同じクロック周波数だった場合」に換算して比較します。

 32ビット整数演算の性能については、既に「#14-10 Arduino IDEのESP32開発環境」で実施済みです。以下表にまとめておきます。

  ESP32 @240MHz DUE @84MHz DUE/ESP32
@1MHz
実測 @1MHz 実測 @1MHz
円周率5,116桁計算時間 1.58s 379s 6.47s 543s ×1.43倍

 「@1MHz」欄に記載したのが、クロック周波数が1MHzだった場合に想定される計算時間です。ESP32なら、実測(@240MHz)で1.58sですので、ESP32 @1MHzだった場合には単純に×240倍の時間がかかるという計算です: 1.58s×240倍≒379s。
 同じクロック周波数だった場合、ESP32(Xtensa LX6コア)の方が、DUE(Cortex-M3コア)よりも約×1.43倍高速でした。

 円周率は多倍長固定小数点演算で実現しましたが、中の演算は32ビット整数演算ですので、円周率の計算プログラムによる処理能力計測では、通常のCPU命令と整数演算の性能が反映されていると見ることが出来ます。

 また、DUEのCortex-M3がシングルコアなのに対して、ESP32のXtensa LX6はデュアルコアなので、応用によっては更にその差が広がる可能性がありますが、今回フォーカスを当てるのは「浮動小数点数の演算速度」なので、双方ともシングルコアとして評価します。

マンデルブロ集合

 何か適切な浮動小数点演算のモチーフが無いか?と考えて、今回はマンデルブロ集合⛄を使ってみようと思います。数学者マンデルブロさんが提唱した図形(?)です。すごく粗い画像ですが、以下図のような感じです。

a161_mandelbrot.jpg

 上の図では横軸が実数軸r、縦軸が虚数軸iとなっていて、−1.5≦r<0.5、−0.5≦i<0.5の範囲を描画したものとなっています。

 マンデルブロ集合は複素空間で定義されていて、以下の様な漸化式で定義される複素数列z[n]に対して、n→∞の極限においてz[n]が無限大に発散しないcの集合となります。

  漸化式:

 この定義に従ってプログラムに落とし込んで行けば良いだけです。

 まず、複素数の計算方法から。標準C言語では複素数の計算はできないので、複素数xの実数部をxr、虚数部をxiと置いて、別々に計算します。xr、xiは各々実数値なので、今回は単精度浮動小数点float型で定義します(↓)。

  float zr;  // 複素数列z
  float zi;
  float cr;  // 複素定数c
  float ci;


 漸化式の計算でz[n]2+cを計算する必要があります。この計算はあらかじめ手計算しておきます。

  t=z[n]2+c
  t=(zr[n]+zi[n]×j)2+(cr+ci×j)
  t=zr[n]2+2×zr[n]×zi[n]×j+zi[n]2×j2+cr+ci×j

 ここで、虚数単位j=√(−1)なので、j2=−1を代入して整理すると、

  t=(zr[n]2−zi[n]2+cr)+(2×zr[n]×zi[n]+ci)×j

 すなわち、

  tr=zr[n]2−zi[n]2+cr
  ti=2×zr[n]×zi[n]+ci

 この漸化式をn→∞まで繰り返し演算すれば良いのですが、そもそも、コンピュータでは無限大と言う量を扱うことが出来ません。n→∞まで繰り返すということは、プログラムが無限ループに入ってしまって、一向に処理が終わりません。
 そこで、コンピュータ演算用に少し、定義を読み替えます。

  n→∞の極限でz[n]が無限大に発散しない
    ↓
    ↓(読み替え)
    ↓
  n=NITRまで計算したところで、(abs (z[n]))2<NINFだったら、無限大に発散しないと判定する

 マンデルブロ集合の境界線近くにズームアップして行く場合には、このNITRもNINFも、なるべく大きな数値に設定しておく必要があります。しかし、今回は、浮動小数点演算速度を評価することが目的で、描画(演算)範囲を−1.5≦r<0.5および−0.5≦i<0.5とするので、以下の設定で十分だと思います。

  const int mdbr_NITR=32;
  const float mdbr_NINF=10000.;


 なお、元々の定義では、「z[n]が無限大に発散しない」となっています。z[n]は複素数なので、z[n]の大きさは以下の様に計算する必要があります。

  複素数z[n]の大きさ:abs (z[n])=abs (zr[n]+zi[n]×j)=√(zr[n]2+zi[n]2)

 演算上は平方根(√、sqrt()関数)を取るべきなのですが、繰り返しの度に平方根を取るのは演算時間の無駄なので、以下の様に式を変形してあります。

  abs (z[n])<√NINF …… 両辺二乗
  (abs (z[n]))2<NINF
  (√(zr[n]2+zi[n]2))2<NINF
  zr[n]2+zi[n]2<NINF

 詳細コーディングは完成したプログラム(以下リンク)でご確認ください。

  完成プログラム:mandelbrot.ino

実験なのだ

 まずは、DUEにTFTシールドを装着して、TFTシールドにマンデルブロ集合を描画してみましょう。SDカードI/Fを接続しておくと(オプション)、計算した画像をセーブすることが出来るようにしてあります。こちら(「#14-7 DUEでTFTシールドのSDカードI/Fを使う」)に従ってSOFTWARE_SPIを有効化してあると、TFTシールド上のSDカードI/Fを使うことが出来ます。

 DUEにTFTシールドを装着して、プログラム(mandelbrot.ino)をコンパイル&書き込みすると、シリアルモニタから以下の様なコマンドを受け付けるようになっています。

以下、実験中の様子です。

a161_expr.jpg

  実験サンプル:【 】内はDUEシリアルモニタからの送信データ
  【mdbr,1】
  DBG: start calculation
  OK: Mandelbrot (1,float) 15313010[us]

 TFTシールドにマンデルブロ集合が描画されたと思います。表示された計算時間は単位μsなので判り難いですが、約15sかかりました。念のため、TFTシールドへの描画時間を除いた、演算時間を測定しておきましょう。

  【mdbr,0】
  DBG: start calculation
  OK: Mandelbrot (0,float) 15223034[us]

 【mdbr,1】の実行時間との差分で、ざっと90ms程がTFTシールドの描画に要していたようですね。


 引き続き、ESP32にも同じプログラム(mandelbrot.ino)をコンパイル&書き込みして実験を続けましょう。ESP32でもシリアルモニタからコマンドを受け付けていますが、SDカードおよびTFTシールドの制御部分は#ifdefで除外してあるので、受け付けるコマンドは【mdbr】,【who】,【help】コマンドだけとなります。

  実験サンプル:【 】内はESP32シリアルモニタからの送信データ
  【mdbr,0】
  DBG: start calculation
  OK: Mandelbrot (0,float) 1948976[us]


 以下表にESP32とDUEのマンデルブロ集合計算速度を比較します。

  ESP32 @240MHz DUE @84MHz DUE/ESP32
@1MHz
実測 @1MHz 実測 @1MHz
円周率5,116桁計算時間 1.58s 379s 6.47s 543s ×1.43倍
マンデルブロ集合float 1.95s 468s 15.31s 1,286s ×2.75倍
マンデルブロ集合double 4.16s 997s 17.84s 1,499s ×1.50倍

※計算に要した時間を単位秒で表にまとめました。

 どうですか?「マンデルブロ集合float」行に関して、クロック周波数が1MHzだったと仮定した時の@1MHz換算時計算時間を比較すると、ESP32の方がDUEよりも約×2.75倍も高速だと判りました。
 円周率の計算では約×1.43倍程度の速度差だったのに、さらに×2倍弱の性能向上が認められます。この違いは何でしょうか?

 答えは、「浮動小数点演算のスピードが違うから」です。何と、ESP32に搭載されたXtensa LX6コアには、浮動小数点演算ユニット(以下FPU;Floating Point Unit)が搭載されているのです。DUEのCortex-M3にはFPUが無いので、ソフトウェアで浮動小数点数の演算をプログラムしてあって、そのルーチンを呼び出して計算を実行しています。
 一方、Xtensa LX6の方はCPUコアにFPUが接続されているので、アセンブラ命令1個で浮動小数点演算が終了してしまいます。さすがに1命令を1クロックで処理できるなんてことは無いでしょうが、そこはハードウェアですから、ソフトウェアでチマチマ作ったプログラムなんかとスピードが違う訳ですよ。

 それがはっきりと数字に表れたのが上の表です。同一クロック周波数で比べて約×2.75倍ですから、そこにクロック周波数比×240MHz/84MHzが掛かるとすごいことになります。「実測」欄の時間を比べてみてください。ESP32の方がDUEの約×8倍速ですよ!

 念のため確認で、実数値の計算をfloat型からdouble型に変更(#define USE_DOUBLE)して実行した計測結果(「マンデルブロ集合double」)も表に記してあります。Xtensa LX6のFPUは単精度(float型)なので、倍精度(double型)演算にはDUE(Cortex-M3)と同様にソフトウェアで計算するルーチンが使われます。
 このため、円周率の計算(整数演算)時と同程度の約×1.50倍に留まっています。速いのは単精度(float型)だけ、と言う事なので、「マンデルブロ集合float」行の測定結果がXtensa LX6搭載FPUの性能、ってことです。

※単精度浮動小数点数(float型)については、こちらに簡単な解説があります。
※半精度浮動小数点数(half型)を題材とした演算アルゴリズムの解説については「#16-5 半精度浮動小数点演算ユニット」を参照してください。

 まあ、Arduinoで電子工作なんてしていると、「Arduinoでfloat型使って、何するの?(int型で十分)」なんて思ってしまう所ですが、ESP32にはI2Sが搭載されていて、Bluetoothも使えるので、オーディオ系のアプリケーションを作る機会も多くなるわけです。オーディオ系のコーデックを作ろうとすると、意外とfloat型演算を使うみたいなので、かなり強い味方になってくれるのではないでしょうか?

 お見逸れしましたm(1__)m。

ESP32の計算画像を確認する

 前節の表だけ見てしまうと、

  「ESP32の処理時間、短過ぎない?」

  「実は計算させたつもりがGCCに最適化されて空ループなんじゃ?」

なんてことを考えそうですので、ちゃんと計算しているよね?ってことを目で確認しておきましょう。

 ESP32にTFTシールドを接続するのはしんどそうなので、計算したらデータをシリアルモニタに出力する様にしてあります。シリアルモニタで受信しても、単なる16進数値の羅列なので、このデータをPCのウインドウに描画するスクリプトを作りました。

  データ受信・描画スクリプト:mandelbrot_smon.olv

 実行には拙作Olive+が必要ですので、ダウンロードコーナなどから入手してWindowsへインストールしておいてください。
 スクリプト(mandelbrot_smon.olv)をOlive+で実行すると、以下の様なウインドウが開きます。

a161_comport.png

 ESP32の通信ポートを選択し、通信速度などのパラメータを適切に設定してOKボタンを押します。
 すると、以下の様なコマンド入力ウインドウが開きますので、ここにシリアルモニタで受け付けられるコマンドを打ち込みます。

a161_cmnd.png


  実験サンプル:【 】内はmandelbrot_smon.olv(上図ウインドウ)からの送信データ
  【mdbr,1】

a161_mandelbrot.jpg

 解像度は違いますが、上図の様な図形が表示されたはずです。まぎれもなく、ESP32が計算したマンデルブロ集合の画像です。

 なお、お使いのPCの処理能力次第では、115,200baudの通信速度が速過ぎてバッファオーバフロウが起こり、データを取りこぼす場合があります。途中でエラーが出て正しく表示できない様であれば、シリアルモニタの通信速度「Serial.begin (115200);」を遅くして(115,200 → 57,600 → 38,400 → 19,200……)通信して見てください。


 ESP32(Xtensa LX6)の浮動小数点演算は、異次元のスピードでした……。ただ、筆者はオーディオコーデックなんて作れないし、何に応用できるか思案中です(1−_−)。


btn_arduino.png

btn_prev.png btn_next.png

home