第4章 乗算

例題を実行してみる 

CASL II には乗算を行う命令がありません。 掛け算を行うにはプログラムを組む必要があります。 左は符号なし掛け算を行うプログラムです。 サブルーチンとして呼び出されることを想定しています。 GR0とGR1の値を掛け合わせてGR0に値を返します。

シフト命令

19行目のSLL(Shift Left Logical)は符号なし左シフト命令です。 第2オペランドで指定した実効アドレスのビット数だけ第1オペランドのレジスタの値を左にシフトします。 実効アドレスにある語の内容ではなく、実効アドレスそのものがシフトするビット数になります。 「SLL GR0,1」の実効アドレスは定数で1を指定しているので1ビット左にシフトします。指標レジスタを指定すれば可変のビット数のシフトもできます。

15行目のSRL(Shift Right Logical)命令は符号なし右シフト命令です。ほかに、符号付きシフトを行うSLA(Shift Left Arithmetic)命令とSRA命令(Shift Right Arithmetic)もあります。

シフト命令

符号付きと符号なしのシフト命令の違いはビット15(符号)を含めてシフトするかどうかです。 符号なしシフト命令はビット15も含めてシフトします。 符号付きシフト命令はビット15を含めずにシフトします(ビット15の値はシフトしても変わりません)。 シフトにより空いたビットには、符号付き右シフトでは符号が、それ以外では0が入ります。

左に1ビットシフトすると、値が2倍になります。 右に1ビットシフトすると、値が半分になります。 端数は切り捨てられます。 nビット左シフトした場合は2のn乗をかけたことに、nビット右シフトした場合は2のn乗で割ったことになります。

SRA命令で、空いたビット14に符号のコピーが入るのは、符号を変えずに2のn乗での割り算になるようにするためです。ただし、端数の処理に注意が必要です。負の数を符号付き右シフトした場合には数値の小さい方向(絶対値の大きい方)に切り捨てられます。 例えば2進数で1111111111111101(-3)を右に1ビット符号付きシフトすると1111111111111110(-2)になります。

ちなみに、割り算の余りがほしい場合は、右からnビットがすべて1の定数とANDを取ると2のn乗で割ったときの余りを求められます。 たとえば8(=2の3乗)で割ったときの余りを求めるには右3ビットが1である7とANDを取ればいいことになります。

シフト命令を実行すると、シフトした結果が0であるかによってZFの値が、ビット15が立っているかによってSFの値が設定されます。 OFには押し出されたビット(2ビット以上のシフトの場合は最後に押し出されたビット)が設定されます。

右にシフト命令の動作をまとめて図示します。 上段がシフト前、下段が1ビットシフト後の状態です。 2ビット以上シフトした場合はこの動作がその回数繰り返されることになります。

乗算のアルゴリズム

筆算の掛け算

筆算の掛け算と同じ要領で、2進数の掛け算ができます。 右は3×13=39を2進数で計算した例です。 (B)の1桁ずつについて、(A)を1桁ずつ左へずらしながら掛けて、それを合計すると掛け算になります。

10進数の掛け算と違うのは、桁の値が1か0かしかないことです。 だから実際には1桁ずつ掛けるという処理はいりません。 (B)の値を1ビットずつ調べて、1ならその値を加えるし、0なら加えない、と、これだけでいいのです。

左のプログラムはこのアルゴリズムをそのままコーディングしたものです。 (A)にあたるGR0を1ビットずつ左シフトしながらGR2に足しこんでいきます。 15〜16行目では、(B)にあたるGR1の各ビットを調べるために、右シフトしてOFに押し出されたビットが1かどうかを調べています。

オーバーフローの考慮

左のプログラムはオーバーフローを考慮していません。 オーバーフローについてチェックするなら、18行目の加算、および19行目のシフト命令のあとでOFフラグをチェックするとよいでしょう。

定数を掛ける

左に1ビットシフトすると、値が2倍になります。 この性質を使えば、2倍、4倍、8倍・・・の計算はシフト命令ひとつでできます。

         SLL    GR1,2    ; GR1を4倍する

2のべき乗以外の数でも、分解して2のべき乗の和にすれば、シフト命令と加算命令で掛け算に代えることができます。 たとえば10倍であれば、10倍=2倍+8倍ですから、1ビットシフトしたものと3ビットシフトしたものを加えることで計算できます。

         LD     GR2,GR1
         SLL    GR1,3      : GR1=8倍
         SLL    GR2,1      ; GR2=2倍
         ADDL   GR1,GR2    : GR1=8倍+2倍=10倍

ちなみに、「SLL GR1,1」は「ADDL GR1,GR1」と書き換えると1語短くなります。

スタック

スタックポインタ

10〜11行目のPUSH命令と、25〜26行目のPOP命令はスタックを操作する命令です。 スタックとは、プログラムの外にあるメモリブロックで、プログラム起動時にあらかじめ与えられます。 スタックは、プログラムで一時的にデータを蓄えておくために使用できます。

スタックにデータを入れる操作をpush、スタックからデータを取り出す操作をpopといいます。 CASL II ではスタックに対してはこの2つの操作しかできません。

プログラム実行開始時にSP(スタックポインタ)レジスタにスタックのアドレスが入っています。 プログラムの実行中、SPレジスタはスタックの位置をつねに指しています。

PUSH命令・POP命令

PUSH命令はスタックに値を入れる機械命令です。 PUSH命令は、実効アドレスをオペランドに取ります。 PUSHを行うと、実効アドレスそのものがスタックに格納されます。 実効アドレスにあるメモリ内の語の値ではないことに注意してください。 10行目の「PUSH 0,GR1」ではGR1の値そのものがスタックに格納されます。

PUSH命令を実行すると、SPから1が引かれ、そのアドレスに実効アドレスが格納されます。 C/C++風に書けば「*(--SP)=実効アドレス;」ということです。

POP命令はスタックから値を取り出す機械命令です。 POP命令は汎用レジスタひとつをオペランドとして取ります。 POPを行うと1語の値がスタックから取り出され、オペランドで指定した汎用レジスタに入ります。

POP命令を実行すると、SPの指しているアドレスにある語の内容が汎用レジスタに入り、そのあとSPに1が足されます。 C風に書けば「レジスタ=*(SP++);」ということです。

 PUSH/POP

SPレジスタの動きを見ればわかるとおり、POPでは、PUSHしたのとは逆の順番で値が出てきます。 POP命令を行うと、最後にPUSHされた値が出てきます。 もう一度POP命令を行うと、その前にPUSHされた値が出てきます。 10〜11行目のPUSH命令と25〜26行目のPOP命令でレジスタの順序が逆になっているのはこのためです。 PUSH/POPを使う場合は、必ず、PUSHした数だけPOPするように注意してください。

CASL II ではSPレジスタの値を取り出したり、SPレジスタに値を設定したりする命令はありません。 SPの値はCASL II プログラムからは制御できません。

PUSH命令ではGR0の値をpushすることはできないことに注意してください。 PUSH命令のオペランドは実効アドレスの形で指定しますが、GR0は指標レジスタには使えないので、「PUSH 0,GR0」という記述はできません。

レジスタとサブルーチン呼び出し

サブルーチンを呼び出した場合、汎用レジスタには呼び出し元で設定した値がそのまま入ってサブルーチンに制御がわたってきます。 サブルーチン内でレジスタの値を変更した場合は、呼び出し元にそのままの値が渡ります。 これを利用してパラメータや戻り値の受け渡しができます。

逆に言うと、サブルーチン内でレジスタを使用した場合、呼び出し元でもともと設定していた値が失われてしまうということになります。 そこで、サブルーチン内で作業用に使う汎用レジスタはスタックに退避しておいて、リターンする前にスタックから復元する、ということがよく行われます。 上のプログラム中のPUSHとPOPはこの目的で使われています。 もちろんサブルーチンの先頭と最後以外の場所でもPUSH・POPを使うことは可能です。 

RPUSHマクロ・RPOPマクロ

CASL II にはRPUSHとRPOPというマクロ命令があります。 これらの命令にはオペランドはありません。 「RPUSH」と書くとGR0以外の汎用レジスタをすべてpushすることができます。 「RPOP」でこれらをpopすることができます。

除算

割り算

2進数の割り算も筆算と同じ要領でできます。 除数を1桁ずつずらしながら被除数から引けるなら引きます。 引けたときには商のその桁に1を立てます。 最後の桁まで処理して残るのが剰余です。 右は41÷3=13余り2を計算したものです。

以下に符号なし除算のプログラミング例をあげます。

 

次章へ次章へ