FPGAボード(Sipeed Lichee Tang Primer)を使って設計したオリジナル16ビットCPUコアMoscoviumのパフォーマンスがちょっと気になった件。
問題と感じたのは、SDRAMにキャッシュメモリを適用して実行したマーチングテストの話です。
「#20-2 キャッシュメモリコントローラの設計(下)」
上記事中の実験では、キャッシュヒット率が85%を超えていたにもかかわらず、マーチングテストは10%程しか高速化できていませんでした。
原因は、SDRAMアクセスに要していた時間よりも、Moscoviumの32ビットデータ処理の方が支配的だったため、でした。何という事でしょう!キャッシュメモリの速さを実感する前に、Moscoviumの遅さが目立ってしまうとは。
なんとかせねば、と思ったのです。
前回、オリジナル32ビットCPUコアNihoniumを完成させました。今回は、実物(FPGAボード)で動作確認して、どの程度高速化できたのか検証してみたいと思います。と、その前に、iverilogを使った論理シミュレーションを実行してみます。
最後にシミュレーション環境を整備しておきます。
テストベンチtest.v(およびtest.lst)では、マイコンの最上位モジュールをインスタンスしています。test.vの中に、シミュレーションによるタイミング図確認を容易にするための、CPU内部レジスタをモニタする記述があり、MoscoviumとNihoniumで共通化することができません(汎用レジスタのビット幅が異なるため)。
また、シミュレーションを実行した時に出力するVCD(タイミング図)ファイルも、CPUが異なればモニタすべき信号が異なるので、共通化しない方が便利です。
このため、`defineマクロSIM_CORE_NHの有無によって、テストベンチtest.v内の記述を切り替えて使います。
`define SIM_CORE_NH // 有効にすると、Nihonium系テストベンチ
//`define SIM_CORE_NH // 無効にすると、Moscovium系テストベンチ
マイコンの最上位モジュール(nhoc113sc0408.v等)を切り替えるのに併せて、SIM_CORE_NHを適切に設定して使って見てください。
では、CPU全命令のセルフテストプログラムで動作を確認しておきましょう。
iverilogを使ってシミュレーションを実行する方法についてはこちらをご確認ください。
2本のセルフテストプログラムのシミュレーションによって、1)Moscoviumとのオブジェクトコードレベル後方互換性、2)Nihonium拡張機能の正常動作を確認しました。
NihoniumをNH=0のモードで使うなら、Moscoviumとの後方互換なので、基本的にそのまま動作します。移植と言う作業は不要です。当然、動作速度もMoscoviumと同じなので、全くパフォーマンス(実行速度)は向上しません。
本節では、円周率πの計算プログラム(calc_pi.asm)をNihonium(NH=1、32ビットモード)用に移植してみようと思います。その過程で、移植に際して注意すべき点について解説したいと思います。
と、上の様な点に注意しながら、円周率計算プログラム(calc_pi.asm)をNihonium用に移植しました。数1,000桁の円周率を計算して、期待値との比較ができるので、CPUが正常に機能しているかどうか見極めるには、良い題材だと思います。
以下に、完成した円周率計算プログラムをリンクしておきます。
円周率計算プログラム:calc_pi.asm
実機(FPGAボード)で動作確認する前に、シミュレーションしてみました。iverilogを使ったシミュレーションの実行方法はこちらをご確認ください。
シミュレーションなので、8桁で留めておきました。次節で実機(FPGAボード)を使い、数1,000桁の円周率を計算してみます。シミュレーションで8桁の円周率を求めるのに要したシミュレーション時間⌚は以下の通りでした。
元々、Moscoviumも32ビット除算コプロセッサDIVC32を使用していたので、Nihonium(32ビット)化による高速化は2割ほどだったようです(7,695/6,045≒×1.27倍速)。まだ、たった8桁のシミュレーションですので、次節で実機(FPGAボード)に書き込んで、5,000桁以上の計算時間を計測します。
なお、MoscoviumとNihoniumでは、実行すべきプログラムが異なるので、プログラム(calc_pi.asm)先頭付近の文字列マクロで切り替えられるようになっています。
# Moscovium: | Nihonium(NH=0): | Nihonium(NH=1): | |
# mc="" | mc="" | mc="#" | |
# n0="#" | n0="" | n0="#" | |
# n1="#" | n1="#" | n1="" | |
def mc,"#" def n0,"#" def n1,"" |
// for Moscovium if "" // for Nihonium (NH=0) if "" // for Nihonium (NH=1) if "" |
デフォルトは、Moscovium用としてあるので、Nihonium (NH=1)用に設定を変更後、アセンブルしてください。
※NihoniumのNH=0モードは、「divlu; divlq」をソフトウェアでエミュレートしているので、実行速度は一番遅くなっています(非推奨)。
NHOC113SC0408を書き込んだFPGAボードにFTDI等のUSB⇔UART変換モジュールを接続して、電源を投入してください。
以下図は、実験中の様子です(写真は使い回し)。
実験サンプル:【 】内はシリアルモニタからの送信データ
【pi,5116】
OK+ start calculation
OK+ Klingenstierna's formula
OK+ pi=3.
1415
9265
:
:
6314
9648
OK+ digits=5116 (size=1280)
OK: pi time[us]=0x022c30fc
約5,000桁の円周率を計算させました。こちらの期待値と比較して、最後の2桁を除き、期待値との一致を確認できました。これだけの円周率を計算するには、相当な数の命令と演算を実行していることになりますので、まず、Nihoniumの正常動作を確認できた、と言って良いでしょう。
※計算した円周率の最後の数桁は正しくありません。より下位桁からの桁上がりを計算できていないためです。アルゴリズム上の問題なので、最後の数桁が期待値と合わないことに関しては無視します。
演算に要した時間をMoscoviumと比較してみましょう(以下表)。
円周率計算時間(calc_pi) |
5,116桁 実測[s] |
速度比 | 備考 | ||
マイコン | マイコン [MHz] | ||||
MCOC115DC0416 | Moscovium×2 | 24 | 27.577 | 1.78 | Klingenstierna,16bit,MUL,DIV,DUAL |
MCOC115CP0408 | Moscovium | 24 | 49.017 | 1.00 | Klingenstierna,16bit,MUL,DIV |
NHOC113SC0408 | Nihonium | 24 | 36.450 | 1.34 | Klingenstierna,32bit,MUL,DIV |
考察:
円周率計算プログラムの速度としては約34%の高速化を確認できました。
円周率計算プログラム(calc_pi.asm)では、元々Moscoviumに32ビット除算コプロセッサを接続してあって、時間のかかる32ビット除算をハードウェアで計算していました。その分を含めて、32ビット化による速度が約34%向上でしたので、まずまず、と言った結果ではないでしょうか?
元々、本プロジェクトの起点となったSDRAMマーチングテストについても確認しておきましょう。
以下、Nihonium(32ビットモード、NH=1)に対応したSDRAMマーチングテストプログラムをリンクしておきます。
実験プログラム:test_sdram.asm
上のプログラム(test_sdram.asm)では、アセンブルオプションでNihonium/Moscoviumを切り替えるのではなく、実行時動的にNihoniumとMoscoviumを判定して、実行するコード(マーチングテストサブルーチン)を切り替えるようにしてあります。
Nihonium/Moscoviumの判定には、状態レジスタsrのNHビットに1を書き込み出来るかどうか?を使いました。Moscoviumでは、NHビット位置が0に固定されているので、NH=1に設定できるのはNihoniumコアだけです。その他、IDレジスタユニットのidrgcodeレジスタ=0x1130を確認する事でも判定できます。
マーチングテストの一部、read1/write0部分のコードを以下に抜粋して、32ビット化による効果を予測しておきます。
処理 | Moscovium (16ビット) | ワード数 | サイクル | Nihonium (32ビット) | ワード数 | サイクル | |
ループ開始 |
// read 1, write 0 ldwi r6,mch_top>>16 ldwi r7,mch_top&0xffff mch_r1w0_loop: |
4 | 4 |
// read 1, write 0 ldli r7,mch_top mch_nh_r1w0_loop: |
3 | 3 | |
メモリ アクセス |
movtc tr,r6 ldwx r3,[r7] stwx [r7],r0 |
3 | 4 |
ldw r3,[r7] stw [r7],r0 |
2 | 3 | |
アドレス 更新 |
add r7,r2 adc r6,r0 |
2 | 2 | add r7,r2 | 1 | 1 | |
期待値 比較 |
cmp r3,r1 bne mch_fail |
2 | 2 |
cmp r3,r1 bne mch_nh_fail |
2 | 2 | |
終了判定 |
cmp r7,r5 cmb r6,r4 blo mch_r1w0_loop |
3 | 4 |
cmp r7,r5 blo mch_nh_r1w0_loop |
2 | 3 | |
合計 (ループ内数) |
Moscovium | 14(10) | 16(12) | Nihonium | 10(7) | 12(9) |
メモリアクセスのサイクル数は、1サイクルアクセスできた場合で記載してあります。
まず、コードサイズはMoscovium=14ワードからNihonium=10ワードと、コード効率が30%向上しました。
次に実行サイクル数ですが、上表でループ開始時の処理は1回しか実行されないので、ループ内の処理で比較してみましょう。合計欄の丸カッコ内数値がループ内のワード数と実行サイクル数です。
実行サイクル数はMoscovium=12サイクル/ループからNihonium=9サイクル/ループと、約25%高速に処理できるようになりました。
上々です。
※ループ内のCPIは、Moscovium=1.2CPI、Nihonium≒1.3CPIでした。上コードの中だと、分岐(blo)命令とロード(ldw)命令が、CPIを増加させる2サイクル実行となっています。それ以外の命令は1サイクル実行です。1サイクル実行命令の数が減って、相対的に2サイクル実行命令の密度が増加したNihoniumの方がCPI値は増加していますが、実行速度自体は高速化しています。この様に、CPIと言う指標は、同じCPU・同じプログラムで比較する時にしか機能しません。
実機(FPGAボード)でも確認しておきます。実験前の準備は、前節円周率の実験と同じです。実験用プログラム(test_sdram.asm)の使い方は、こちらに説明があります。
実験サンプル:【 】内はシリアルモニタからの送信データ
【march】
DBG: Nihonium mode
DBG: write all 0
DBG: read 0, write 1
DBG: read 1, write 0
DBG: read all 0
OK: marching finished: 0x000019c1[ms]
キャッシュメモリ無効状態でマーチングテストを実施しました。テストはパスしていますので、問題ありません。実行時間に注目しておいてください。後で比較します。
【cche,1】
OK: cache on1/off0=0001
【march】
DBG: Nihonium mode
DBG: write all 0
DBG: read 0, write 1
DBG: read 1, write 0
DBG: read all 0
OK: marching finished: 0x0000166a[ms]
キャッシュメモリ有効状態でマーチングテストを実施しました。こちらもテストはパスしました。SDRAMに対するアクセス手順は同じなので、キャッシュヒット率はこちらと同じ(87.5%)です。
肝心なのは、Moscoviumの32ビット化によって、処理速度がどれくらい上がったか?なので、以下表で確認しておきます。
CPU |
論理規模 LUT数 |
マーチング キャッシュ無効 |
マーチング キャッシュ有効 |
Moscovium(16ビット) | 3,000LUT | 8,668ms | 7,814ms |
Nihonium(32ビット) | 4,700LUT | 6,593ms | 5,738ms |
考察:
う〜ん、こんなのばっかり、ですね。以下の様なイタレーション……。
どうやら、マイコン(CPU)の実行速度を上げるというテーマは、一朝一夕には行かないみたいです。
ちょっと一度、高速化によるコスト増加を無視して、もう一押し、行き着くところまで行かないとスッキリできないみたいです。引き続きあがいてみたいと思います。