MC6800のプログラミングテクニック(20) 除算の再検討 8bit版

BASICMASTER, 昔のパソコン

1978年に発表された Intel 8086 には乗除算命令が搭載されていた。それ以前の8bitマイクロプロセッサでは、MC6801やMC6809に8bit乗算命令があるぐらい。

初期のRISCであるSPARCでも パイプラインを乱さないように乗除算命令すらなく びっくりしたものだ。

さて、ソフトウェアで除算を行う場合、代表的な方法は回復法と非回復法(引きっ放し法)である。SRT法などハードウェア実装を主にした方法はソフトウェア実装には不向きなので使わない。

回復法で8bit除算

わかりやすい回復法で8bit除算を書いてみよう。Aに剰余、Bに被除数と商、メモリ上に除数を置く。

テストのため、chibiccの関数として使えるように 引数はAccBとスタックで与えているが、通常のサブルーチンとして使うなら両方ともレジスタ渡しが良い。

ループ本体の処理時間は 22-25cyc, 14bytes である。商に1が立つときが25cyc, 0のときは22cycである。


	.setcpu 6800
	.export _div8x8
	.code
;
; AccB = AccB / TOS
;
; AccB: divient
; Stack:
;   0-1: return address
;     2: divisor
;
_div8x8:
        tsx
        tba
        ldab 2,x
        clrb
        stab @tmp1	; divisor
        ldx #8
loop:
        asla		; 2 1
        rolb		; 2 1
        subb @tmp1	; 3 2	divient - divisor
        bcs skip	; 4 2	can't subtract
        inca		; 2 1
        bra next	; 4 2
skip:
        addb @tmp1	; 3 2
next:
        dex		; 4 1
        bne loop	; 4 2
        tab
        clra
	rts


キャリーを利用した改良

引き算の直後のキャリーフラグは、引けた場合はC=0, 引けなかった場合はC=1になっている。本来必要な情報の逆になっているのだが、これが使える。

キャリーを次回のシフト時にAccAの末尾に入れるようにする(asl→rolに変更)。ループ終了時点で1bit未処理なので rol命令を追加する。終了時点でビット反転(com)することで 辻褄を合わせる。

ループ内の処理時間は商が1になるときに19cyc, 0のときに22cycになる。1のときはに6cyc縮んでいる。0のときは変わらない。バイト数も1つ少ない。

ただし、ループ最後の辻褄合わせで4cyc必要になる。商が0になるときは損だが 1bitでも1になっていれば少し速くなる。


_div8x8:
        tsx
        tba
        ldab 2,x
        stab @tmp1    ; divisor
        ldx #8
        clrb
loop:
        rola            ; 2 1
        rolb            ; 2 1
        subb @tmp1      ; 3 2
        bcc next        ; 4 2
        addb @tmp1      ; 3 2
next:
        dex             ; 4 1
        bne loop        ; 4 2
        rola            ; 2 1
        coma            ; 2 1
        tab
        clra
	rts


回復法で引かない方法(比較法)

一般的なアルゴリズムの説明では比較と減算は同コストなので、引いてみてだめなら足す方法が良いと書かれている。

しかしながら、MC6800の場合、8bit除算であれば、比較して 引けるなら引くという方法でも良いのである。MC6809なら16bitでも同様。
(MC6800で16bitの場合は引いてみるしかない。cmp命令のキャリー付きがあれば比較法でも良いが、そんな便利な命令はないので)

ループ内の処理時間は商が1になるときに22cyc, 0のときに19cycになる。先ほどと必要時間が逆になっていて、0の時が速い。

実は整数除算では、商のビット数は圧倒的に0が多くなるので(これは別項で書く)、こちらの方が平均的に速い。


_div8x8:
        tsx
        tba
        ldab 2,x
        stab @tmp1    ; divisor
        ldx #8
        clrb
loop:
        rola            ; 2 1
        rolb            ; 2 1
        cmpb @tmp1      ; 3 2
        bcs next        ; 4 2
        subb @tmp1      ; 3 2
next:
        dex             ; 4 1
        bne loop        ; 4 2
        rola            ; 2 1
        coma            ; 2 1
        tab
        clra
	rts


非回復法除算

回復法では引けなかったときは足すのだが、これが無駄なので足さなくても良いようにしたのが非回復法。

足し戻すコストが大きい場合(longなど)では効果絶大だが、8bit除算ではオーバーヘッドの方が大きい。

非回復法の除算は符号ビットを使用するので、8bit全体を使った除算には そのままでは適用できない。

そこで、このプログラムはCarry bitを符号ビットに見立てて処理している。

キャリーが1のときのadd/subの結果が期待の反対になってしまうので、そこを分岐で判断 (pcarry,mcarry付近)。

この分岐の判断に4cyc必要なので、8bit除算では却って遅くなる(回復のaddは3cycで済むので)。

高速化のためには incをsec/clcで代用したり、ループ末のdex/bneを展開する必要があるが、わかりにくくなるので、このプログラムは素直に書いてある。

剰余を正しく得るためには、最後のステップで AQ<0 のときに、剰余に除数を加える必要がある。


       	.setcpu 6800
        .export _div8x8
        .code
_div8x8:
        tsx
        ldaa 2,x
        staa @tmp1    ; divisor
        ldx #8
        clra            ; clear remainder
;
; main loop when AQ>=0
;
ploop:
        aslb
        rola
        bcs pcarry
        suba @tmp1
        bcs mnext
        bra pnext
pcarry:
        suba @tmp1
        bcc mnext
pnext:
        incb
        dex
        bne ploop
        clra
        rts
;
; main loop when AQ<0
;
mloop:
        aslb
        rola
        bcs mcarry
        adda @tmp1
        bcc pnext
        bra mnext
mcarry:
        adda @tmp1
        bcs pnext
mnext:
        dex
        bne mloop
        clra
        rts

剰余が欲しい場合は、ループ終了時点でAccAに入っている値を使う。最後にキャリーが立っている場合(結果が負の時)は、剰余の補正が必要。


mnext:
        dex
        bne mloop
        tab		; 剰余を戻り値にする
        beq mrem	; 剰余が0でなければ
        addb @tmp1	; 補正する
mrem:
        clra
        rts


以前、GAME-CCコンパイラ用の非回復法の除算ルーチンを紹介しましたが、GAME言語は符号付き整数しか扱わないので問題にならなかったのでした。
(以前のルーチンは現在の視点で見ると遅いので、書き直したいものだ)。