乗算ルーチンの検討(ベーシックマスター開発 その23)

BASICMASTER, 昔のパソコン

BASICMASTERは日立製のCPU HD6800を使っている。これはモトローラのMC6800の互換品。昔のCPUなので乗算・除算命令は無く、シフト命令と加減算を組み合わせる必要がある。

コンパイラを作成・改良するにあたり、各種言語で、16bit = 16bit*16bitの乗算を行うサブルーチンがどのように実装されているか調べたので、結果をメモしておく。

GAME言語の乗算

GAMEでは下記のような AccAB*(IX,IX+1) → AccAB を実行する乗算ルーチンが使われていた。右端は実行サイクル数と、命令のバイト数である。

被乗数を右シフトしたキャリーを見て乗数をAccABに加算する。乗数はループのたびに2倍(左シフト)していく。

Electronic DIY with Arduinoの図がわかりやすい。

ループ部分は ML1〜BNE ML1 までで、これを16bit分(16回)繰り返す。1ループあたりのサイクル数は、50cycle(分岐不成立時)、40cycle(分岐成立時)である。

(プログラムで実際に使われる数は小さな数が多いだろうから、平均すると45cycle未満だろう)

プログラムサイズの節約もあって インデックスアドレッシングを使っている部分があり、そこをダイレクトモードにすれば 各1サイクル節約できる (46cycle/36cycle)。その代わり4バイトも!増えてしまうので、メモリが極めて少ない当時の環境では悩ましい問題だ。

乗数(W68)が256以下のときはループ回数は8回で済むので、その処理を入れると平均すると速くなる。これは他のルーチンも同様。


MLTPLY  STAA	W68		; 4 2
        STAB	W68+1		; 4 2
        LDAB	#16		; 2 2
        STAB	W66		; 4 2
        CLRA			; 2 1
        CLRB			; 2 1
ML1     LSR	W68		; 6 3
        ROR	W68+1		; 6 3
        BCC	ML2		; 4 2
	ADDB	1,X		; 5 2
	ADCA	0,X		; 5 2
ML2     ASL	1,X		; 7 2
        ROL	0,X		; 7 2
        DEC	W66		; 6 3
        BNE	ML1		; 4 2
        RTS			; 5 1

GAMEの該当部分のソースはASCII誌連載二回目(1978年7月号)に掲載されている。

GAME68コンパイラの演算ルーチンも上記と同じ。


NAKAMOZU Tiny BASICの乗算

NAKAMOZU Tiny BASICでは (IX,IX+1)*(IX+2,IX+3) → AccAB という処理を行なっている。

こちらは被乗数の左側のbitから処理していき、計算結果AccABを左シフトしている。乗数は変わらない。被乗数と計算結果を連結してシフトしているのが肝。

file.php (688×781)の一番上の図がわかりやすい。

ループ部分は MLT1〜BNE までで、1ループあたり 42cycle(分岐不成立時)、32cycle(分岐成立時)である。

GAMEよりも実行時間が短いのは、シフト対象が乗数(ダイレクトページ)ではなく結果(AccAB)になっているため。

2バイトシフトに14cycleもかかるのか、4サイクルで済むのかは大きな差である。

こちらもインデックスアドレッシングをダイレクトモードに変えれば、38,28サイクルまで減らせる。しかし、プログラムサイズとの兼ね合いでインデックスアドレッシングが使われたのだと思う(4バイト変わる)。

電大版 Tiny BASIC (Bit誌1978年8月号 共立出版)のものも同様


MLTPLY  LDAA	#16		; 2 2	
        STAA	LPCT		; 4 2
        CLRA  			; 2 1
        CLRB   			; 2 1
MLT1    ASLA			; 2 1 
        ROLB			; 2 1
        ROL	1,X             ; 7 2
        ROL	0,X             ; 7 2
        BCC	*+6             ; 4 2
        ADDA	3,X             ; 5 2
        ADCB	2,X             ; 5 2
        DEC     LPCT            ; 6 3
        BNE     MLT1            ; 4 2
	RTS			; 5 1

NTBのソースは下記のサイトに掲載されている。


Fuzix-Compiler-Kit/support6800の乗算

EtchedPixels氏(aka. Alax cox)による6800コンパイラのサポートルーチンに含まれているサブルーチンである。英小文字を使っているのが現代風である。

スタックマシンなので 被乗数はスタックトップにある。それをインデックスレジスタ経由でアクセスしている。乗数も先頭でスタックに積んでいる。

処理はGAMEと同様。ループ部分は nextbit 〜 bne まで。 52,42cycle。GAMEより2cycle多いのは、乗数も被乗数もインデックスアクセスしているため。


__mul:
	pshb			; 4 1
	psha			; 4 1
	tsx			; 4 1
	ldab #16		; 2 2
	stab @tmp		; 4 2
	clra			; 2 1
	clrb			; 2 1
nextbit:
	lsr ,x			; 7 2
	ror 1,x			; 7 2
	bcc noadd		; 4 2
	addb 5,x		; 5 2
	adca 4,x		; 5 2
noadd:	lsl  5,x		; 7 2
	rol  4,x		; 7 2
	dec @tmp		; 6 3
	bne nextbit		; 4 2
	ins			; 4 1
	ins			; 4 1
	jmp __pop2		; 3 3


拙作のGAMEクロスコンパイラの乗算ルーチン(1)

NAKAMOZU Tiny BASICを参考に、なるべくダイレクトページを使うようにしてみたルーチン。

ループ部分は ML01〜BNEまで 34,28 cycle。インデックスアクセスが無くなったので、ループカウンタに IXレジスタを使うことで さらに2cycle縮めている。

1サイクルでも減るとループ回数分(16回)減るので、効果は意外に大きい。ベーシックマスターのクロックは755KHz弱で、1サイクルあたり1.325μsもかかるので なおさらだ。

初期化部分で256未満の場合は16*8bitのルーチンを呼ぶようにして、姑息に速度を稼いでいる。

また、FCB $8C はASLB/ROLAをスキップするため。ABともに0なので実行しても問題はないのだが、1バイトと1サイクルを交換している。無くても大差はない。


MULTIPLY    STX     TDXWK           ;   5   2
            LDX     0,X             ;   6   2
            STX     W66             ;   4   2
            STAB    W68+1           ;   4   2
            STAA    W68             ;   4   2
            BEQ     MULTI02         ;   4   2
MULTI       LDX     #16             ;   3   3
            CLRA                    ;   2   1
            CLRB                    ;   2   1
            FCB     $8C             ;   3   1       SKIP2. It costs one byte but saves one cycle.
ML01        ASLB                    ;   2   1
            ROLA                    ;   2   1
            ROL     W68+1           ;   6   3
            ROL     W68             ;   6   3
            BCC     ML02            ;   4   2
            ADDB    W66+1           ;   3   2
            ADCA    W66             ;   3   2
ML02        DEX                     ;   4   1       
            BNE     ML01            ;   4      2	    34cycle/loop => *16=>544cycle
            LDX     TDXWK
            RTS


拙作のGAMEクロスコンパイラの乗算ルーチン(2)

現在使っているもの。 Dr Jefyll’s method を使っている。

いままでの手法では掛け算を行うときに、通常はループごとに4byteのシフトが必要だが、これを3byteに抑えることで速度を稼ぐ方法。

file.php (688×781)の一番下の図がわかりやすい。

MC6800は、ダイレクトページであってもシフトに6cycleもかかるので、ループ内からシフトが無くなるのは大きい。

その代わり、前半ループ8回と後半ループ8回で処理を分けないといけないので、プログラムサイズは大きくなる。


*
*   MULTIPLY Dr Jefyll's method
*   MULTIPLY    AB = (IX,IX+1)*AB
*
MULTIPLY    STX     TDXWK           ;   5 2
            LDX     0,X             ;   6 2
            STX     W66             ;   4 2
            STAA    W68             ;   4 2
            CLRA                    ;   2 1
            STAB    W68+1           ;   4 2
            CLRB                    ;   2 1
            LDX     #8              ;   3 3
ML01        ROR     W68+1           ;   6 3
            BCC     ML02            ;   4 2
            ADDB    W66+1           ;   3 2
            ADCA    W66             ;   3 2
ML02        LSRA                    ;   2 1
            RORB                    ;   2 1
            DEX                     ;   4 1
            BNE     ML01            ;   4 2         28cycle/loop => *8=>224cycle
            ROR     W68+1           ;   6 3
ML03        TST     W68             ;   6 3
            BNE     ML031           ;   4 2         Is W68,W68+1 <= 255?
            TBA                     ;   2 1
            BRA     ML07            ;   4 2
ML031       LDX     #8              ;   3 3
ML04        ROR     W68             ;   6 3
            BCC     ML05            ;   4 2
            ADDB    W66+1           ;   3 2
            ADCA    W66             ;   3 2
ML05        LSRA                    ;   2 1
            RORB                    ;   2 1
            DEX                     ;   4 1
            BNE     ML04            ;   4 2         ↑28cycle/loop => *8=>224cycle
            ROR     W68             ;   6 3
ML06        LDAA    W68             ;   3 2
ML07        LDAB    W68+1           ;   3 2
            LDX     TDXWK           ;   4 2
            RTS                     ;   5 1


他のCPUの例

掛け算は、Z80と6502で8bit*8bitの掛け算を行うルーチンを比較したもの。

他のCPUと比べると6800遅いなーと思います。実は6809も遅いんだけど、LEAXが強力で、七難隠してたんだよな。

TobyLobster/multiply_test: Comparing 6502 multiply routinesは6502で各種乗算ルーチンを比較したもの。すごい。一読の価値あり。 Dr Jefyll’s method は、このページで知りました。

表引きをすると高速化できるというのは、誰でも思いつくが、素直に8bit*8bitの表を作ると128KBになるのでメモリに入らない (ROMに焼いて6820PIO経由でアクセスすれば……ロマンだ)。

16bit乗算を8bit乗算3回で実現するとして、各乗算を2乗の和or差で計算する場合、2乗を表引きしても表は512byteで足りる。

表引きした乗算については マイナス二乗法:PICマイコンは面白い:So-netブログ が詳しい。ただ、6800は表引きが遅いので、どれぐらい速くなるかなあ。


リンク