公開日:2022/03/26
title.png

20th Season


#20-4 32ビットに拡張するゾ(ひ)

 FPGAボード(Sipeed Lichee Tang Primer)を使って設計したオリジナル16ビットCPUコアMoscoviumのパフォーマンスがちょっと気になった件。

 問題と感じたのは、SDRAMにキャッシュメモリを適用して実行したマーチングテストの話です。

  「#20-2 キャッシュメモリコントローラの設計(下)

 上記事中の実験では、キャッシュヒット率が85%を超えていたにもかかわらず、マーチングテストは10%程しか高速化できていませんでした。

 原因は、SDRAMアクセスに要していた時間よりも、Moscoviumの32ビットデータ処理の方が支配的だったため、でした。何という事でしょう!キャッシュメモリの速さを実感する前に、Moscoviumの遅さが目立ってしまうとは。
 なんとかせねば、と思ったのです。

Moscoviumを高速化する

 「Moscoviumを高速化するゾ」と、意気込んでは見たものの、何だかんだと、Moscoviumは1サイクル/1命令実行のCPUなので、その意味では十分高速なのでした。
 CPI(Clocks Per Instruction)と言う指標があります。その名の通り、1個の命令を実行するのに要するクロックサイクル数を示した指標です。

  CPI=<命令の実行に要した時間(サイクル数)>÷<実行した命令数>

 一般的には、CPIの数値が大きくなると低速、小さくなると高速であることを示す指標です。

※上の様に、CPIの大/小と、速度の高/低が逆転してしまうため、IPC (Instructions Per Clock、CPIの逆数=1/CPI)を使う方も居るようです。

 Moscoviumは1サイクル/1命令のCPUなので、理想的にはCPI≒1です。一部メモリアクセス命令や分岐命令などで2サイクル以上となる命令があるので、実効的にはCPI=1.2〜2程度になるのではないかと思われます。
 実際にMoscoviumの全命令セルフテストプログラム(mcvm_self.asm)で計測したところ、CPI≒1.1でした。

※SDRAMアクセスはリード3サイクル、ライト2サイクルと遅いので、これを多用するプログラムでは、CPI=2〜4になる可能性もあります。CPUの性能としてCPIを論ずる時は、メモリアクセスの影響は排除しておかないと話がおかしくなりますので、最速の1サイクルでアクセスできた場合を想定してCPIを議論します。


 さらに高速化するには:

 上に挙げた案は、色々と影響が大きい(修正量莫大、論理規模も大幅増加)ので却下しました。さて、どうした物か……。

一応、却下の理由

汎用レジスタと演算器(ALU)を32ビット化する

 そもそも、マーチングテストが遅かった理由は、24ビットアドレスを処理するため32ビットデータ処理が必要となり、16ビットのMoscoviumで複数命令が必要となった事でした。

  例)32ビットの加算
  // r6:r7+=r0:r1
  add r7,r1
  adc r6,r0


 Moscoviumの汎用レジスタは16ビット幅なので、32ビットデータを処理しようとすると、上例の様に複数レジスタの組み合わせ(レジスタペア)と複数命令が必要となります。
 汎用レジスタと演算器(ALU)の32ビット化によって、1命令で処理できるようになると、それだけ高速化することができます。

  // r7,r1が32ビットレジスタなら、1命令
  add r7,r1


 2命令2サイクル掛かっていた処理が、1命令1サイクルで終了するので、該当箇所に関しては一気に2倍速となります。この内部32ビット化案が、一番安易かつ現実的なソリューションとなりそうです。

Moscoviumの32ビット化

 Moscoviumの汎用レジスタと演算器(ALU)のビット幅を16ビット→32ビットに拡張して行きましょう。この目標に従って、どのブロックをどのように拡張すべきか?検討を進めます。演算器が32ビット化するので、ある程度の論理規模増加は止むを得ませんが、なるべくその増加量を抑えたいと考えています。


 と、その前に、汎用レジスタを32ビット化すると、プログラマモデル的に全く異なる別CPUとなるので、オリジナル32ビットCPUコアの呼称を決めておきます。


 呼称📛が決まったところで、早速、命令コードをどうするか?から検討を始めたいと思います。

コード効率

 コード効率とは、簡単に言うと、「同じプログラムを機械語に翻訳した時のコードサイズの指標」です。コードサイズの小さいことを「コード効率が高い」と表現します。

※と言う事は、コード効率=プログラム÷コードサイズ、的な指標なんですね。

 特に、組み込み用途に使うマイコンなどで、プログラムメモリ(ROMやFlash)の容量が限られている時に、重要な指標となります。

 イメージを掴んでいただくために、例題を設定してコード効率を確かめてみましょう。

  例1)フィボナッチ数列を生成するプログラムコード比較

fibonacci Moscovium SuperH Thumb ARM Native ARM AVR8
1)f0=0 ldbiu r0,0 mov r0,#0 mov r0,#0 mov r0,#0 ldwi r0,0
2)f1=1 ldbiu r1,1 mov r1,#1 mov r1,#1 mov r1,#1 ldwi r1,1
3)f2=f0+f1=1 mov r2,r0
add r2,r1
mov r2,r0
add r2,r1
add r2,r1,r0 add r2,r1,r0 mov r2,r0
add r2,r1
4)f3=f1+f2=2 mov r3,r1
add r3,r2
mov r3,r1
add r3,r2
add r3,r2,r1 add r3,r2,r1 mov r3,r1
add r3,r2
5)f4=f2+f3=3 mov r4,r2
add r4,r3
mov r4,r2
add r4,r3
add r4,r3,r2 add r4,r3,r2 mov r4,r2
add r4,r3
6)f5=f3+f4=5 mov r5,r3
add r5,r4
mov r5,r3
add r5,r4
add r5,r4,r3 add r5,r4,r3 mov r5,r3
add r5,r4
7)f6=f4+f5=8 mov r6,r4
add r6,r5
mov r6,r4
add r6,r5
add r6,r5,r4 add r6,r5,r4 mov r6,r4
add r6,r5
8)f7=f5+f6=13 mov r7,r5
add r7,r6
mov r7,r5
add r7,r6
add r7,r6,r5 add r7,r6,r5 mov r7,r5
add r7,r6
命令コード長 16ビット 16ビット 16ビット 32ビット 16ビット
コードサイズ
[バイト]
2×14=28 2×14=28 2×8=16 4×8=32 2×14=28


  例2)線形合同法による疑似乱数列生成のプログラムコード

LCGs Nihonium SuperH Thumb ARM Native ARM
1)r0=seed ldli r0,seed mov.l r0,@(disp,pc) ldr r0,[pc,#] ldr r0,[pc,#]
2)r1=1,103,515,245 ldli r1,1103515245 mov.l r1,@(disp,pc) ldr r1,[pc,#] ldr r1,[pc,#]
3)r2=12,345 ldwi r2,12345 mov.w r2,@(disp,pc) ldrh r2,[pc,#] ldrh r2,[pc,#]
4)r0*=r1 mulu r0,r1 mul.l r0,r1
sts r0,macl
mul r0,r1,r0 mul r0,r1,r0
5)r0+=r2 add r0,r2 add r0,r2 add r0,r2,r0 add r0,r2,r0
命令コード長 16ビット 16ビット 16ビット 32ビット
コードサイズ
[バイト]
2×10=20 2×11=22 2×10=20 4×7.5=30


 上の二例を眺めながら一般論として、同じ動作をする命令なら、32ビット長より16ビット長命令の方がコード効率が高くなることがお解りでしょう。もし、命令コード長を16ビット長→32ビット長に変更するなら、1個の命令に2命令分の動作を押し込まないと、コード効率がバランスしません。


 そう言えば、パソコン(PC)のCPUといえばPentium(今はIntel CORE?)ですが、この子、1バイトコードなんて物を持っているのですよ。今やIntel COREは64ビットが主流だというのに、1バイトの命令コードを持っているなんて、おかしい(興味深い)でしょう(1^−^)d?
 もし、固定長命令に拘らず、コード効率の向上を最大優先に持ってくるのであれば、色々なプログラムで生成された命令の統計データを取って、より出現頻度の高い命令に、より短い命令コードを割り当てる、と言うハフマン符号化を適用すると、コード効率を最適化することが可能です。
 多バイト長命令を使うことで、Moscoviumみたいに命令コードの枯渇に頭を悩ませることも無くなって、一石二鳥かも知れません。

 Moscoviumもそうですが、固定長にしておいた方が、ハードウェア(CPU)の設計が楽になるので、今回はそこまでコード効率至上主義には走りません。マイコンとしては、16ビット固定長命令で十分なコード効率を得られると判断します(本節考察の通り)。

16ビット固定長命令

 Moscoviumは16ビット固定長命令を採用していて、比較的コード効率は高いです。Nihoniumでも、16ビット固定長命令は維持した方が良いと結論付けました。

 「add r0,r1」は、C言語風に書くと「r0+=r1」です。Moscoviumでは、これを16ビット幅で演算しました。
 32ビット演算を実行するNihoniumでは、一般的には、addl (add long word)命令などを追加することになるのでしょう。ですが、今回はaddl命令を追加しません!追加したくても、命令コードがそんなに余っていないので、ちょっと無理……。
 それに、プログラムを作るユーザが、「ここは32ビットだからaddl命令、こっちは16ビットだからadd命令」等と気にしながらプログラムするのも難儀な話です。32ビット汎用レジスタに、誤って16ビット演算命令を実行してしまった場合、どんな演算結果が戻るのか?などと言う、余計な心配もしなければなりません。
 そこで:

 だけ。それだけ。
 MoscoviumでもNihoniumでも、「add r0,r1」の命令コードは0x7981で共通とします。Moscoviumと全く同じ命令と命令コードを使い、汎用レジスタと演算器(ALU)のビット幅だけを16 → 32ビットに拡張するのが、Nihoniumとなります。

※C言語のint型をイメージしてください。8/16ビットCPUではint型は16ビットですが、32/64ビットCPUではint型は32ビットです。int型のビット幅が変わっても、「int r0,r1; r0+=r1;」は、不変です。


 Moscoviumを高速化する目的に向けて、32ビット化したCPUコアをNihoniumと呼ぶこと、命令コードを16ビット固定長とすることまで決めました。次回以降、検討を続けていきたいと思います。


btn_arduino.png

btn_prev.png btn_next.png

home