FPGAボード(Sipeed Lichee Tang Primer)を使って設計したオリジナル16ビットCPUコアMoscoviumのパフォーマンスがちょっと気になった件。
問題と感じたのは、SDRAMにキャッシュメモリを適用して実行したマーチングテストの話です。
「#20-2 キャッシュメモリコントローラの設計(下)」
上記事中の実験では、キャッシュヒット率が85%を超えていたにもかかわらず、マーチングテストは10%程しか高速化できていませんでした。
原因は、SDRAMアクセスに要していた時間よりも、Moscoviumの32ビットデータ処理の方が支配的だったため、でした。何という事でしょう!キャッシュメモリの速さを実感する前に、Moscoviumの遅さが目立ってしまうとは。
なんとかせねば、と思ったのです。
前回、オリジナル16ビットCPUコアMoscoviumを32ビットに拡張、Nihoniumと呼ぶこと、命令コードとしては16ビット固定長を維持する、所まで検討しましたので引き続き検討を続けて行きます。
ここは更に一歩踏み込んで、NihoniumをMoscoviumと「オブジェクトコードレベル後方互換」にしようと思います。
C言語ソースとか、アセンブラソースとか言うレベルではなくて、オブジェクトコードレベルと言う事は、Moscoviumで動作していた機械語のバイナリコードを持ってきてNihoniumに喰わせると、そのまま正常に動作することを指します。
でも、普通は無理なんです。だって、演算器(ALU)が32ビットに拡張しているのに、32ビット演算命令を追加してないのですから。命令コードを共通にしてあっても、演算ビット幅が異なればオブジェクトコードレベルで「そのまま」動かすのは無理と言う物です。
そこで、CPUの状態レジスタsrにNHビットを新設して、このビット値でモードを切り替えることとします。
リセット解除後のデフォルトはNH=0としておきます。リセット解除後はNH=0なので、完全にMoscoviumと互換動作となります。
NihoniumがNH=1に設定すると、そこから先は32ビット演算が実行されるようになります。
NH=0 ⇔ 1の切り替えは、プログラム中いつでも変更可能です。
Thumb対応しているARMの場合、確か、プログラムカウンタpcが奇数ならThumbコード(命令長16ビット)、偶数ならNative ARMコード(命令長32ビット)に切り替わるんじゃなかったかな?モードを切り替えるには分岐(BX)命令を使います。命令コードが変わるので、パイプライン内の命令を押し出してからじゃないと切り替えられないんだね、きっと。
※Thumb ARMアセンブル後のオブジェクトを眺めて見た時、割り込みベクタアドレスや分岐先アドレス値が奇数アドレスになっていて気持ち悪かった記憶があります。
Nihoniumの16ビット/32ビットモード切り替えは、状態レジスタsrのビットをセット/クリアするだけでダイナミックに切り替えることができます。Thumbみたいに命令コードが切り替わる訳ではないのでへっちゃらです。汎用レジスタや演算器(ALU)のビット幅が切り替わるだけなのですから。
ただし、Moscoviumで除算コプロセッサDIVC32を使うdivl*系(divlu/divls/divlq/divlr)命令については、Nihoniumではサポートしません。この命令は、16ビットのMoscoviumで32ビット除算を実行するという、変則的な命令なので、Moscovium専用命令とします。
なおNihoniumでは、通常のdivu命令/divs命令で、NH=0では16ビット除算、NH=1では32ビット除算が可能です。
※真の意味で「完全互換」としたかったですが、divl*系命令のみ、対象外となります。元々、コプロセッサ命令なので、「一部コプロセッサ命令を除いて完全互換」と言い訳しておきます。
単に汎用レジスタのビット幅を16→32ビットに拡張するのは簡単なのですが、それだけ論理規模が増大します。論理増加量はなるべく小さく抑えたいし、それに、NihoniumはNH=0の状態ではMoscoviumと完全互換である必要があるので、少し知恵を絞ってみます。
下図は、Moscoviumのプログラマモデルです。
Moscoviumには16ビットの汎用レジスタが8本(R0〜R7)あって、更にその組が4バンクありました。思えば、かなり贅沢な構成でしたね。
汎用レジスタを32ビットに拡張して、かつ、Nihoniumの論理増加量を抑えるため、Nihoniumでは汎用レジスタバンク数を半減して、2バンク構成にしようと思います。その上で、Nihoniumのバンク0は、Moscoviumのバンク2レジスタを上位16ビット、Moscoviumのバンク0レジスタを下位16ビットとする32ビットレジスタとします。
NihoniumのRn(バンク0)=(MoscoviumのRn(バンク2)<<16)|MoscoviumのRn(バンク0)
NihoniumのRn(バンク1)=(MoscoviumのRn(バンク3)<<16)|MoscoviumのRn(バンク1)
元々Moscoviumに存在するレジスタを流用するので、Nihoniumの論理規模増加量は最小限に留めることができます。Nihonium (NH=1モード)でのプログラマモデルは以下の様になります。
汎用レジスタを32ビットに拡張した影響を見て行くため、以下にNihoniumのブロック図を示します。
上図程度のブロック図レベルでは、Moscoviumと大差ありませんでした(1^^;)。
違いとして見えるのは、FCHブロックからBバスへの出力(拡張命令レジスタeir)です。16ないし32ビット即値を取る多ワード長命令(解説は次節)の即値データをBバスに乗せるための出力です。
3本の内部(A,B,C)バスは32ビットに拡張が必要です。演算器(ALU)も32ビットに拡張して、Nihoniumでは乗除算器をALUに内蔵します。
命令コードはMoscoviumと同じなので、制御CTLブロックは、ほぼそのまま使います。新規設計するブロックは無くて、ちょっと論理を追加するだけです。
内部(A,B,C)バスが32ビットに拡張されているので、コプロセッサインターフェイスも32ビットになります。
乗除算コプロセッサ(MULC16とDIVC32)については、NihoniumのALUに内蔵するので、非対応とします。
既存のコプロセッサとしては、半精度浮動小数演算ユニットH-FPUについて、Nihoniumでも接続可能としておきます。将来的には、32ビットの単精度浮動小数演算ユニットに対応した方が良いかもしれませんが、当面必要を感じないので、まずはH-FPUに対応しておくこととします。
ALUに内蔵する乗算器ですが、「#16-2 Moscoviumで四則演算する」の記事でMoscovium用に設計した乗算コプロセッサは16×16=32ビット乗算器でした。乗算器その物は、TD IDEの乗算器マクロを使っています。TD IDEの乗算器マクロは最大18×18=36ビットで29個まで使用することが可能です。16×16=32ビット乗算には、乗算器マクロが1個あれば十分でした。
Nihoniumでは乗算器が32×32=32ビットになったので、乗算器マクロが3個(※)必要となりました。演算ビット幅が拡張した分の増加です。
※TD IDEが提供する乗算器マクロは18×18=36ビット乗算器です。これでは32×32=32ビット乗算には満たないため、乗算器マクロを複数使って部分積を計算した後、加算して求めているようです。コンパイル時、TD IDEが自動的に展開しているらしく、RTLを組む上では特に何も考えずに済みます。
その分、演算時間も増加しています。今までのMoscovium(16×16=32ビット乗算)でも、時々STAのクリティカルパスにレポートが挙がっていましたので、32×32=32ビット乗算を1サイクルで実行するのは諦めました。
更に、Nihoniumで32×32=64ビット乗算が必要な場合、乗算器マクロをもう1個余分に消費します。その分、部分積の加算回路も追加されて、論理規模(LUT)も増加するはずです。演算に掛かる時間も増加して、クリティカルパスを更に強化してしまうため、32×32=32ビット乗算に留めてあります。
このクラスのマイコンで64ビット乗算は実行しないよね(要らないよね)?っていう腹積もりです。
よって、NihoniumのNH=1モードでは、mulu命令/muls命令は2サイクルを要します。また、NH=1でMLビット=1に設定することは禁止しておきます(将来32×32=64ビット乗算に対応する時のための予約)。
ここでちょっと困ったのが、NH=0(16ビット)モードでの乗算命令の扱いです。NH=0モードでは、16×16=32ビット乗算を実行しないと、Moscoviumとの互換性を維持できません。が、32×32=32ビット乗算器を使ってしまうと2サイクル演算となってしまい、1サイクルで実行できるMoscoviumに対してビハインドとなります。
演算性能(実行速度)を向上しようとして設計しているNihoniumが、Moscoviumに引けを取るわけには行かないので、勿体ないのですが、16×16=32ビット乗算器(1サイクル演算可能)を別に1個持つことにしました。
結果、Nihoniumは乗算器マクロを計4個消費します。乗算器マクロは、TD IDEのIP Generatorを使い、以下図のように各自生成してください。
乗算器に触れたところで、除算器についても言及しておきます。
Nihoniumに内蔵する除算器も、「#16-3 除算器の設計」でMoscovium用に設計した除算コプロセッサと同じ、フラッシュ除算器とステップ除算器のハイブリッド方式で、32ビット符号無除算を8ステップで終了します。ブロック図を下図に示します。
コプロセッサとのインターフェイスバスが16ビットだったことで、除算コプロセッサとのデータ授受に時間がかかって、結局14サイクルも要していたMoscoviumと異なり、Nihoniumでは、インターフェイスバス(上図A,B,Cバス)が32ビットに拡張されたためオーバヘッドが抑えられ、10サイクルで終了することができます。
32ビット除算が10サイクルなら、今時のSRT除算器を搭載したマイコンにも、演算性能で引けを取ることはありません。
Moscoviumでは、16ビットデータのバレルシフト演算命令を備えていたので、−16〜15ビットシフト演算に対応していました。シフト量≧0で左シフト、シフト量<0で右シフトです。
バレルシフト演算は巨大なセレクタのお化けとなるので、2冪数1,2,4,8ビットシフト演算器をカスケードに接続して実現していました(下図右側)。
Nihoniumでは、32ビットデータのバレルシフト演算に対応するため、16ビットシフト演算器(bsft16)をカスケードに追加して対応します。シフト可能な範囲は、−32〜31ビットです。
Moscoviumには8ビット即値ロード命令しか無くて、16ビット即値を汎用レジスタにセットするには、以下の様に2命令2サイクルを要しました。
@0000 c012 // ldbih r0,((0x1234)>>8)&0xff
@0002 b834 // ldbil r0,(0x1234)&0xff
現行Moscovium命令だけで32ビット即値を汎用レジスタにセットしようとすると、例えば以下の様なコードとなります。
@0000 c012 // ldbih r0,((0x1234)>>8)&0xff
@0002 b834 // ldbil r0,(0x1234)&0xff
@0004 c156 // ldbih r1,((0x5678)>>8)&0xff
@0006 b978 // ldbil r1,(0x5678)&0xff
@0008 d810 // lsfti r0,16
@000a 7801 // or r0,r1
余計な汎用レジスタ(r1)を汚してしまうし、6命令6サイクルもかかります。
32ビット即値は、SDRAM領域の24ビットアドレスを指定して、データ(変数)にアクセスするようなケースで多用する可能性があります。
何らかの命令を追加した方が良いですね。
他のCPUで32ビット即値をロードするのにどんな命令が使われるか?と言うと、SuperH (SH)やThumb ARMだと、リテラル方式を採用しています。この方式は、プログラムカウンタpcからの相対アドレスで、即値に相当するデータを読み出す方式です。
以下にコード例を示します。
// アセンブラの記述例: mov.l r0,#0x12345678
// 以下、アセンブル後のコードイメージ。
mov.l r0,@(lab_imm32-lab_this,pc) // 現Pcからの相対アドレスから読み出し
lab_this:
:
:
bra next // リテラル領域を跳び越すための分岐命令
lab_imm32:
data.l 0x12345678 // 32ビット即値に相当するデータ
:
next:
彼らも、16ビットと言う限られた命令語長の中で、32ビット即値のロードを実現する方法として、苦労の跡が見えます。ただこの方式、予めメモリにデータを記憶しておくのと同じなので、アクセスはオペランドアクセスとなっています。もう「即値」ではありませんね。
※オペランドアクセス: 普通のデータアクセス、と言う事。フェッチではない。
折角のハーバードアーキテクチャなのに、オペランドアクセスで即値データにアクセスすると言うのはいかがなものでしょうか?
オペランドキャッシュとフェッチキャッシュが別々に存在するシステムだと、リテラル方式の場合、即値データ(プログラム)のフェッチでオペランドキャッシュを汚してしまいます。
プログラムカウンタからの相対アドレス指定も、そんなに遠くのアドレスまで指定できません。使う場所の近くにリテラル(データ)領域を配置せざるを得ず、場合によっては、そのデータ領域を跳び越すための分岐命令が必要となって、実行サイクル数の増加やコード効率の低下を招きます。
考えた結果、Nihoniumではリテラル方式を採用せず、多ワード長命令方式を採用しようと思います。例えば、以下の様になります。
@0000 7f40 // ldli r0,0x12345678
@0002 1234
@0004 5678
32ビット即値を汎用レジスタへロードするldli命令(新規追加する)は、上述の通り3ワード命令です。全てフェッチバスからの読み出しとなって、オペランドアクセスは発生しません。
フェッチバスから読み出した即値データをCPUの内部バスに載せて、演算器や汎用レジスタに転送するための拡張命令レジスタeirがこちらのブロック図に示した信号線eirです。
Moscoviumは全命令が16ビット固定長だったのですが、Nihoniumでは即値オペランドを持つ、ごく少量の命令だけ2〜3ワード長命令を許容して追加します。
※なお、ldli命令は3ワード3サイクル実行なので、CPIを増加させます。しかし、複数命令を組み合わせて32ビット即値を汎用レジスタにロードする(6ワード6サイクル)よりは高速に処理できると言う考え方です。コード効率も向上します。
NihoniumをMoscoviumに対するオブジェクトコードレベル後方互換とすること、演算器(ALU)内に乗除算器を内蔵すること、32ビット即値ロード命令を多ワード長命令とすることを決めました。次回、更に詳細設計を進めていきます。