FPGAボード(Sipeed Lichee Tang Primer)を使って設計したオリジナル16ビットCPUコアMoscoviumのパフォーマンスを向上しようと、内部演算器(ALU)と汎用レジスタを32ビットに拡張した、オリジナル32ビットCPUコアNihoniumを設計しました。
出来上がったNihoniumのパフォーマンスを、実機(FPGAボード)やシミュレーションにて検証したところ、かけた費用(コスト)に対して、効果(パフォーマンス)が伸びていないことが判りました。同時に、「きっと、こういう物なのだな」と、悟りを開いてしまいました(1^−^;)。
つまり、コスト→論理の増加量なので、50%論理が増加する工夫によって、パフォーマンス→実行速度が50%高速化すると思ってはいけないのだと。マイコンのパフォーマンスを向上しようとする目論見には、それ相応の大きな代償(コスト)がかかるものなのだという事。
そこで、「コスト増加量が大きすぎる」として一旦保留したスーパスケイラを実施して、費用対効果がどのように見えるものなのか?検証してみたいと思います。
前回、スーパスケイラ版Nihonium-SSの設計まで完了させました。今回は、シミュレーションで詳細に動作確認した後、実機(FPGAボード)に書き込んで実験してみようと思います。
シミュレーション用のテストベンチ(test.v)ですが、通常版Nihoniumとスーパスケイラ版Nihonium-SSで、内部信号の参照が異なる場合があるので、新たにSIM_CORE_NHSSという`defineマクロを設定しました。
`define SIM_CORE_NH
`define SIM_CORE_NHSS // need "`define SIM_CORE_NH" too
シミュレーションするCPUコアに従って、有効化/無効化して使ってください。iverilogを使ったシミュレーションの実行方法はこちらをご確認ください。
シミュレーション検証の手順としては、以下の様に実施しました。
まず、1)です。以下にタイミング図を示しておきます。
「#20-8 スーパスケイラ」や「#20-9 イシュー(命令の発行)」、「#20-10 クローズ(設計の完了)」にて、例として挙げたパイプライン実行図の様な命令列のシミュレーションを実行して、正常に動作するか?想定通りの動作となっているか?確かめました。
橙色に着色しておいたのが、CPUの命令レジスタirです。スロット2個分あるので、Ir0,Ir1となっています。このタイミング図から、以下の様な事が読み取れます。
2)続いて、セルフテストプログラムを使って、従来機能通りに動作することを確認しました。
2本ともパスを確認しました。スーパスケイラ化に伴って、動作が変わってしまった命令はありませんでした。
3)最後に実際のアプリケーションの中から、円周率の計算プログラム(calc_pi.asm)を選んでシミュレーションしてみました。シミュレーションには時間がかかるので、小数点以下12桁の計算に留めてあります。
【iverilog -f test.lst ; if ( ! $status ) vvp a.out】
OK+ pi=3.
1415
9265
3591
OK+ digits= 12 (size= 4)
Test Pass
CPI: 8124/ 5562( 2768)= 1460/1000
finish: 326520 ns
最後の2桁を除いて、期待値(3.1415926535)は一致しました。
※計算した円周率の最後の数桁は正しくありません。より下位桁からの桁上がりを計算できていないためです。アルゴリズム上の問題なので、最後の数桁が期待値と合わないことに関しては無視します。
ここで、シミュレーション中に計測した指標について、まとめておきます。
シミュレーションで内部信号をモニタしながらカウントしないと計測できないので、ここでまとめておきます。
CPUコア | サイクル数 | 命令数 | 並列実行数 | CPI | 並列実行率 |
mcvm_self.asm | |||||
Moscovium | 1,180 | 1,084 | 0 | 1.089 | 0.0 |
Nihonium | 1,180 | 1,084 | 0 | 1.089 | 0.0 |
Nihonium-SS | 1,042 | 1,085 | 308 | 0.960 | 28.4 |
niho_self.asm | |||||
Nihonium | 1,452 | 1,026 | 0 | 1.415 | 0.0 |
Nihonium-SS | 1,158 | 1,027 | 342 | 1.128 | 33.3 |
calc_pi.asm (12桁) | |||||
Moscovium | 12,316 | 7,530 | 0 | 1.636 | 0.0 |
Nihonium | 9,594 | 5,565 | 0 | 1.724 | 0.0 |
Nihonium-SS | 8,124 | 5,562 | 2,768 | 1.460 | 49.8 |
calc_pi_ss.asm (12桁) | |||||
Nihonium-SS | 7,752 | 5,840 | 4,070 | 1.327 | 69.7 |
prime_fact.asm (N=32,771) | |||||
Nihonium | 3,784 | 1,639 | 0 | 2.309 | 0.0 |
Nihonium-SS | 2,364 | 1,639 | 1,020 | 1.442 | 62.2 |
以下考察です。
なかなか、並列実行できないものですね……。普通に作ったアプリケーションでは、並列実行率で約50%、処理速度比で約×1.20倍速(20%高速)、程度しか期待できないという結果です。円周率計算プログラムをスーパスケイラ用に最適化してようやく、並列実行率で約70%、処理速度比で約×1.25倍速(25%高速)でした。
円周率計算プログラム(calc_pi.asm)をスーパスケイラ用(calc_pi_ss.asm)に最適化するのに使った手法をいくつか紹介しておこうと思います。
calc_pi.asm (通常のシングルスロット版) | calc_pi_ss.asm (スーパスケイラ用に最適化) |
// for (idx=1; idx<lfp_siz; idx++) // B[idx]=0; ldbiu r0,0 ldwi r1,lfp_siz ldw r1,[r1] |
// for (idx=1; idx<lfp_siz; idx++) // B[idx]=0; ldwi r1,lfp_siz ldbiu r0,0 // @ ldw r1,[r1] |
ldwi命令とldw命令のリソース(r1)に依存関係が有った(←左)ため、依存関係のないldbiu命令を移動して、間に挟んだ(右→)。 | |
add r1,r7 loop: addi r7,2 cmp r7,r1 bhs break stw [r7],r0 bra loop break: |
add r1,r7 addi r7,2 // @ loop: cmp r7,r1 bhs break stw [r7],r0 addi r7,2 // @ bra loop break: |
addi命令→cmp命令の命令列は、リソース(r7)に依存関係が有って並列実行できなかったため(←左)、addi命令の実行位置をずらした(右→)。 | |
// while (A[idx]>=base) // { cry_while2: cmp r0,r2 blt cry_while_brk2 // A[idx] -= base; sub r0,r2 // A[idx - 1]++; addi r1,1 // } bra cry_while2 |
// while (A[idx]>=base) // { cmp r0,r2 // @ cry_while2: blt cry_while_brk2 // A[idx] -= base; sub r0,r2 // A[idx - 1]++; addi r1,1 // } cmp r0,r2 // @ bra cry_while2 |
cmp命令→blt命令の命令列は、状態レジスタsrの演算フラグ(N,C,V,Z)にリソース依存関係があるため、並列実行できない(←左)。 そこで、cmp命令をループの最後に移動して、並列実行できるようにした(右→)。 |
|
// r0=A[idx] + A2[idx], r1=A[idx - 1] ldw r0,[r7] exsgl r0 ldw r3,[r5] exsgl r3 add r0,r3 subi r7,2 ldw r1,[r7] |
// r0=A[idx] + A2[idx], r1=A[idx - 1] ldw r0,[r7] ldw r3,[r5] exsgl r0 // @ exsgl r3 subi r7,2 add r0,r3 // @ ldw r1,[r7] |
2組のldw命令とexsgl命令にリソース(r0,r3)依存関係が有った(←左)ので、exsgl命令を移動して解消した(右→)。 exsgl命令とadd命令にもリソース(r7)依存関係が有った(←左)ので、add命令を移動して解消した(右→)。 |
|
// B[idx]=quo; ldbiu r7,atn_quo ldwsp r1,[r7] ldbiu r7,atn_b ldwsp r7,[r7] add r7,r6 add r7,r6 stw [r7],r1 |
// B[idx]=quo; ldbiu r7,atn_quo ldwsp r1,[r7] ldbiu r7,atn_b lsfti r6,1 // @ ldwsp r7,[r7] add r7,r6 lsfti r6,-1 // @ stw [r7],r1 |
r7にr6の2倍を加算する処理。シングルスロットの場合、「r6を2回加算(add,add)」する方が速かった(←左)。スーパスケイラでは、「r6を2倍(lsft)して、r7に加算(add)したら、r6を1/2倍(lsft)する」方が速い(右→)。 並列実行できるよう、リソース依存関係の無い位置に分散してlsft命令とadd命令を配置していることに注目。実行する命令数が2個から3個に増えているのに、速くなった例。 |
ほんのちょっとしたことなのですが、機械的に対処できない例もあって、一筋縄ではいきません。一々、人間が頭を使って、命令をスケジューリングする必要があるのが難点です。しかも、確実にプログラムの可読性を低下させています。
実行順番を変更したことで、プログラムの意味(動作)が変わってしまっていないかどうか?十分に確かめなくてはならないことも障害となります。
また、今回の様にシミュレーションで確認できるようなケースなら良いのですが、シミュレーションできないプログラムだった場合、実際に動作させてみて、高速化できたかどうか?時間を計測してみるしか確かめようがないのも困りものです。
本来はコンパイラが最適化すべき部分なので、コンパイラサポートの無いNihoniumでは、致し方なし、です。
比較するのに適切なマイコンが見当たらないので、何ですが、Arduino UNOとDUEを使って、参考データを取っておこうと思います。Arduino用のC言語版素因数分解プログラムを以下にリンクしておきます。
素因数分解プログラム:prime_fact.ino
コンパイルしてArduino UNOやDUEに書き込むと、シリアルモニタからこちらのコマンドを受け付けるようになっています。
FPGAボードの準備は、こちらの説明に従って、スーパスケイラ版Nihonium-SSを搭載したマイコンNSOC113SD0408をコンパイル(論理合成・配置配線)して、FPGAボードに書き込んでください。この時、プログラム(prime_fact.asm)をアセンブルして同時に書き込むか、ブートリセットを使って書き込みしてください。
また、FPGAボードにFTDI等のUSB⇔UART変換モジュールを接続して、電源を投入してください。
以下図は、実験中の様子です(写真は使い回し)。
実験サンプル:【 】内はシリアルモニタからの送信データ
【prim,2147483648】
OK+ elapsed time: 32 [us]
OK: prime factors: 2147483648 = 2 * 2 * …… * 2 (省略、231)
231=0x8000_0000=2,147,483,648を素因数分解しました。2が31個表示されたはずです。素因数分解の計算に要した時間も表示されました。
素因数の数(31個)に比して、このケースの処理時間が一番短く(速く)なります。こちらで解説した様に、K=2,3,……,√Nまで割り切りるかどうかを試していくのですが、いきなりN=2で割り切れることが判明するだけでなく、その度にN’=N/2となって、Nの大きさがあっという間に小さくなっていくためです。
【prim,20220402】
OK+ elapsed time: 57 [us]
OK: prime factors: 20220402 = 2 * 3 * 61 * 101 * 547
実験した日の日付を素因数分解してみました。電卓などで掛け算すると、検算できます。後は、全ての数値が素数かどうかですね(1^^;)/。2,3は良いとして、大きな数値が素数かどうか?
素数リストを検索する(別ウインドウが開きます)
61,101,547も素数リストに含まれていました。素因数分解としては正常に動作しているようです。
【prim,2147483647】
OK+ elapsed time: 24136 [us]
OK: prime factors: 2147483647 = 2147483647
今度は、素数231−1=0x7fff_ffff=2,147,483,647を素因数分解してみました。素数の場合、素因数は自分自身の1個だけが表示されます。素数を入力した場合が一番処理時間がかかるので、この時の演算時間を取得して、比較してみます。
N=2,147,483,647 (素数)の素因数分解処理時間比較 | ||||
マイコン | CPU | 処理時間[μs] | 相対比較 | |
NHOC113SC0408 | Nihonium | 24MHz | 37,653 | 1.00 |
NSOC113SD0408 | Nihonium-SS | 24MHz | 24,136 | 1.56 |
Arduino DUE(SAM3X8E) | ARM Cortex-M3 | 84MHz | 13,397 | 2.81 |
Arduino UNO(ATmega328) | AVR8 | 16MHz | 1,871,084 | 0.02 |
以下に考察します。
素因数分解のアプリケーションは想像した以上にチートだったので……(1^〜^;)っ、シミュレーション検証にも使った、円周率計算プログラムでも確かめておきましょう。
円周率計算プログラム:calc_pi.asm
アセンブルして、FPGAボードに書き込んで実験します。
実験サンプル:【 】内はシリアルモニタからの送信データ
【pi,5116】
OK+ start calculation
OK+ Klingenstierna's formula
OK+ pi=3.
1415
9265
3589
:
: (中略)
:
0141
6314
9648
OK+ digits=5116 (size=1280)
OK: pi time[us]=0x01d9e7f8
5,000桁以上の期待値が一致しました。スーパスケイラ化しても、ちゃんとシングルスロット版Nihoniumと同じ計算結果が得られたので、Nihonium-SSの動作としては正しい様です。
※↑などと結論しておいてこう言うのも何ですが、どうかな?何しろ、命令(100種類)の組み合わせだけで100×100=10,000通り、リソース依存関係で145通りの組み合わせがあるのに対して、calc_pi.asmが実行した組み合わせ数なんて、数10〜100通り程度でしょうから。動作検証としては、まだまだ足りない気はしてます……。
※高々100種類ほどの全命令を検証したら終了だった、通常のシングルスロット版CPUと比べて、スーパスケイラ版CPU(1002×145通りもの組み合わせ数)の検証には、相応の検証コストもかかるという事ですね。
上の実験では、「#20-7 32ビットに拡張するゾ(よ)」で、オリジナル32ビットCPUコアNihonium用に最適化した円周率計算プログラム(calc_pi.asm)をそのまま使いました。
アセンブラで、人間が見易いように一連の処理を一塊にして記述しています。スーパスケイラとしてみると、「一連」の処理が固まっていることで前後の命令にリソースの依存関係が発生する可能性が高まっており、必ずしも最適なプログラムではありません。
そこで、多少プログラムの可読性を落とすことになりますが、命令の実行順番を入れ替えて、Nihonium-SS用に命令の実行順番を最適化したプログラムを使って、効果を見てみます。スーパスケイラ用に最適化した内容に付いてはこちらで解説済みです。
最適化したプログラム:calc_pi_ss.asm
実験サンプル:【 】内はシリアルモニタからの送信データ
【pi,5116】
OK+ start calculation
OK+ Klingenstierna's formula
OK+ pi=3.
1415
9265
3589
:
: (中略)
:
0141
6314
9648
OK+ digits=5116 (size=1280)
OK: pi time[us]=0x01bea4c0
期待値は一致しましたので、実行順最適化後のプログラム(calc_pi_ss.asm)にミスは無かったようです。
円周率の計算アルゴリズムに使う公式として、Machin公式とKlingenstierna公式を使いました。以下にまとめ表を示して、考察を続けます。
円周率計算時間(calc_pi*.asm) | 5,116桁 | 速度比 | 備考 | |||
マイコン | CPU [MHz] | 実測[s] | Mc基準 | Nh基準 | ||
Machin公式 | ||||||
MCOC115CP0408 | Moscovium | 24 | 50.703 | 1.00 | 0.74 | 16bit,MUL,DIV |
MCOC115DC0416 | Moscovium×2 | 24 | 39.270 | 1.29 | 0.96 | 16bit,MUL,DIV,DUAL |
NHOC113SC0408 | Nihonium | 24 | 37.704 | 1.34 | 1.00 | 32bit,MUL,DIV |
NSOC113SD0408 | Nihonium-SS | 24 | 30.232 | 1.68 | 1.25 | 32bit,MUL,DIV,Super-Scalar |
Klingenstierna公式 | ||||||
MCOC115CP0408 | Moscovium | 24 | 49.017 | 1.00 | 0.74 | 16bit,MUL,DIV |
MCOC115DC0416 | Moscovium×2 | 24 | 27.577 | 1.78 | 1.32 | 16bit,MUL,DIV,DUAL |
NHOC113SC0408 | Nihonium | 24 | 36.450 | 1.34 | 1.00 | 32bit,MUL,DIV |
NSOC113SD0408 | Nihonium-SS | 24 | 29.227 | 1.68 | 1.25 | 32bit,MUL,DIV,Super-Scalar |
※速度比は基準が1.00で、それより大きいと速い、小さいと遅い。
考察:
今回のテーマ「スーパスケイラ」の最終目標を確認しておいてください。「マイコン高速化における費用対効果の確認」が、最終目標でした。
データが出そろったので、まとめて比較してみたいと思います。と言っても、テーマや基準がバラバラなので、以下、まとめ方の方針を示しておきます。
まとめ表を以下に示します。
ユニット | テーマ | LUT数 | 比較対象 | 効果 | 費用 |
費用対 効果 |
備考 | |
高速化 比率 |
LUT 増加量 |
増加 比率 |
効果 ÷費用 |
|||||
Moscovium | 基準 | 3,000 | − | − | − | − | − | #20-6 |
Moscovium Dual Core |
分散処理 | 6,000 | Moscovium | 78% | 3,000 | 100% | 0.780 |
円周率計算 (5,000桁) #17-8 |
キャッシュ メモリ |
SDRAM 高速 アクセス |
1,400 | Moscovium | 11% | 1,400 | 47% | 0.236 |
マーチングテスト #20-2 |
Nihonium | 32ビット化 | 4,700 | Moscovium | 34% | 1,700 | 57% | 0.600 |
円周率計算 (5,000桁) #20-7 |
Nihonium-SS |
スーパ スケイラ |
11,500 | Moscovium | 68% | 8,500 | 283% | 0.240 |
円周率計算 (5,000桁) #21-1 |
Nihonium-SS |
スーパ スケイラ |
11,500 | Nihonium | 25% | 6,800 | 145% | 0.173 |
円周率計算 (5,000桁) #21-1 |
※備考欄に数値の根拠としたページへのリンクが張ってあります。円周率計算プログラムは、デュアルコア用に最適化したKlingenstierna公式と、スーパスケイラ用に最適化したcalc_pi_ss.asmを用いた結果を使いました。
以下に考察します。
かかった費用に対して、それに見合う効果が得られた場合、上表の費用対効果(=効果÷費用)は1.00となります。表を眺めると、総じて1未満の値が並びます。効果÷費用<1未満なのは、費やした費用に見合う高速化が達成出来なかったことを意味しています。ので、中々思ったようには行かないのが現実みたいです。
こういう現実を理解しておくと、過度な期待をして結果にガッカリしたり、費用増加を恐れるあまり効果が目標に未達になるという事態も避けることができるでしょう。Moscovium/Nihoniumを高速化しようとするテーマは、今回のスーパスケイラ版Nihonium-SSにて、漸く行き着くところまで行き着いたという感じがします。達成感を感じながら、このテーマを閉じたいと思います。