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

BASICMASTER, 昔のパソコン

前回はさまざまな乗算処理を見てみた。今回は除算。乗算ではシフト方向によって 大きく2つの方法があるが、除算も同様。

また、16bit*16bitの乗算で結果の下16bitだけを使う場合は符号あり・符号なしのどちらでも同じルーチンが使えるが、除算の場合は事前に正の数に直しておく必要がある。
(GAMEやNTBでは、言語仕様としても正の数として処理する必要がある)。

また、除数を引いてみて引けなければ即時に戻す方法(引き戻し法)と、次の桁の処理時に戻す方法(引き放し法)がある。


GAME言語の除算

GAMEでは、人間が筆算するのと同じ方法で除算を行なっている。実際の手法については、ASCII.jp:CPU黒歴史 大損失と貴重な教訓を生んだPentiumのバグ (2/3)の例がわかりやすい。

プログラムサイズを小さくするために、原プログラムではサブルーチンを多用しているが、わかりやすくするために下記に引用したプログラムはサブルーチンを展開してある。

まず、除数を最左bitが 1になるまで左シフトする。Carry flagが立つまで左シフトして行きすぎた1bit分を右シフトしている。
(Cではなく、N flagを見るようにしたら2回分儲かりそうだが、除数が32768の場合を想定しているのだろう)

以下は筆算同様に引き算を行い、引けなかった場合は引き戻し法で元に戻す処理を行なっている。なので、引けた場合と引けなかった場合で必要時間が異なっている。

最悪の時間がかかるのは 0を1で割る場合で、最初に16+1回、次に16回のシフトが発生する。引き戻しも16回行われる。

大きな数、例えば32767なら最初のシフトが 1+1回、次も1回で3回のシフトで済む。

大抵の場合は除数は小さな数字だろうから、平均的な計算時間は不利になる。

また、除数を0ページに置いているため、シフト操作で14cycle必要になるのも痛い。

1バイトスキップに $9C を使っているのは、Cフラグに影響を与えないようにするため。細かい工夫である。BRAだと1バイトに必要になる。


DIVPOS	CLR	W66	; 6 3
DP1	INC	W66	; 6 3
	ASL	1,X	; 7 2
	ROL	0,X	; 7 2
	BCC	DP1	; 4 2	↑ 24cycle/loop
	ROR	0,X	; 7 2
	ROR	1,X	; 7 2
	CLR	W68	; 6 3
	CLR	W68+1	; 6 3
DP2	SUBB	1,X	; 5 2
	SBCA	0,X	; 5 2
	BCC	DP3	; 4 2
	ADDB	1,X	; 5 2
	ADCA	0,X	; 5 2
	CLC		; 2 1
	FCB	$9C	; 4 2
DP3	SEC		; 2 1
	ROL	W68+1	; 6 3
	ROL	W68	; 6 3
	DEC	W66	; 6 3
	BEQ	TDE	; 4 2
	LSR	0,X	; 7 2
	ROR	1,X	; 7 2
	BRA	DP2	; 4 2	; ↑70 or 56cycle/loop
TDE	RTS		; 5 1

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

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


NAKAMOZU Tiny BASICの除算

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

プログラムサイズを小さくするために、原プログラムではサブルーチンを使っているが、下記に引用したルーチンはそれを展開してある。

こちらは商と除数を連結して4バイトずつ左シフトしている。シフト回数は除数の大小によらず16回である。

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

MDIV4は、被除数の最上位ビットを見て引き戻しを行いつつ、Cフラグを設定している。設定したフラグはMDV2のDECでは壊れないのがミソであるが、難解。

乗算のときと同様に、インデックスアドレッシングをダイレクトモードに変えれば 所用時間は14cycle減らせるが、4バイト増える。

電大版 Tiny BASIC (bit誌1978年8月号 共立出版)のものも基本的には同じ。NTBの方がサイズが小さい。

昔のプログラムはメモリーを大事にしていたよなあ。今とは大違いである。


MDIV    LDAA	#16		;
        STAA	LPCT		;
        CLRA			;
MDV1    ROL	3,X		; 7 2
        ROL	2,X		; 7 2
        ROLA                    ; 2 1
        ROLB                    ; 2 1
        SUBA	1,X             ; 5 2
        SBCB	0,X             ; 5 2
MDV4    PSHB                    ; 4 1
        EORB	0,X		; 5 2
        PULB			; 4 1
        SEC			; 2 1
        BPL	MDV2		; 4 2	↑47cycle
ADD     ADDA	1,X		; 5 2
        ADCB	0,X		; 5 2
        CLC			; 2 1	↑12cycle
MDV2    DEC	LPCT            ; 7 6
        BNE	MDV1		; 4 2	↑11cycle
MDV3    ROL	3,X		; 7 2
        ROL	2,X		; 7 2
        RTS			; 5 1

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




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

EtchedPixels氏のFuzix-Compiler-Kitの除算を参考にして、少し高速化したもの。

数の大小によらず16回のループが発生するのだが、小さな数だと無駄なので、先頭で256未満なら下位バイトを上位に移動している。ループ回数も8回に減る。

インデックスアドレッシングを使っていないので、IXはループカウンタとして利用している。ダイレクトページをカウンタとして使うと6サイクル必要だが、IXなら4サイクルで済む。

引き算の結果、引けた時と引けなかった時の処理を分けて、無理に合流させないようにしている。サイズは増えるが処理時間は減る。


*
*   DIVIDE Positive number
*           W68 = W68/W66 AB=MODULO
*           IX is destroyed
*
DIVPOS      LDB     W68         ; 3 3   the dividend less than or equal to 255 (<=255?)
            BNE     DIVPOS00    ; 4 2       ↑7cycle overhead
            LDA     W68+1       ; 3 2   W68 = W68<<8
            STAA    W68         ; 3 2
            STAB    W68+1       ; 3 2   clear W68+1
            LDX     #8          ; 3 3
            BRA     DIVPOS10    ; 4 2   AccB is 0, so the jump destination is CLRA.
DIVPOS00    LDX     #16         ; 3 3
            CLRB                ; 2 1
DIVPOS10    CLRA                ; 2 1   7cycle for preparation
DIVPOS01    ASL     W68+1       ; 6 3
            ROL     W68         ; 6 3
            ROLB                ; 2 1
            ROLA                ; 2 1
            SUBB    W66+1       ; 3 2
            SBCA    W66         ; 3 2
            BCS     DIVPOS02    ; 4 2   ↑26cycle
            INC     W68+1       ; 6 3
            DEX                 ; 4 1
            BNE     DIVPOS01    ; 4 2   +14cycle => 40cycle/loop => 640cycle for 16bit repetitions
            RTS                 ; 5 1   5cycle
DIVPOS02    ADDB    W66+1       ; 3 2
            ADCA    W66         ; 3 2
            DEX                 ; 4 1
            BNE     DIVPOS01    ; 4 2   +14cycle => 40cycle/loop => 640cycle for 16bit repetitions
            RTS                 ; 5 1   5cycle


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

現在は引き放し法を使っている。最初はコンピュータアーキテクチャの話(85) 引き過ぎを戻す必要があるのか? | TECH+(テックプラス)を参考に書いたのだが、速度は引き戻し法と大差なかった。

割り算にあるZ80用のルーチンを参考にして書き直したものが、現在のルーチンである。

割り算のZ80用ルーチンの解読中。本物のチラシの裏である)

引いて正になるときと負になるときを別のループにしているのがミソ。普通ならフラグを立てて処理するところであるが、それだとフラグの設定・判定に時間がかかる。

これは要するにbit 1980年7月号のプログラミングセミナーにある、プログラム構造とデータの交換である。

算譜の構造が、十分に作られているなら、その構造の上で、いまどこに位置しているかを知ることで相当の情報が得られる。つまり、算譜のトポロジーが十分な情報を含んでいる。一方,構造が貧弱であれば、それだけでは情報不足となるので、それを補うための情報を料として追加してやらなければならない。

bit 1980年07月号 P.31のプログラミングセミナーより引用

フラグ相当の処理をプログラム構造で行なっているので、プログラムサイズは倍近い。その分、ループごとの処理時間は 36,34cycle と最速である。


*
*   DIVIDE Positive number / non restoring division algorithm
*           W68 = W68/W66 AB=MODULO (W68=Q,W66=M,N=16,A=AccAB)
*           cf. https://www.javatpoint.com/non-restoring-division-algorithm-for-unsigned-integer
*           cf. https://www.wizforest.com/tech/Z80vs6502/div.html;p2
*           IX is destroyed
*
DIVPOS      EQU     *
            LDB     W68         ; 3 3   the dividend less than or equal to 255 (<=255?)
            BNE     DIVPOS00    ; 4 2       ↑7cycle overhead
            LDA     W68+1       ; 3 2   W68 = W68<<8
            STAA    W68         ; 3 2
            STAB    W68+1       ; 3 2   clear W68+1
            LDX     #8-1        ; 3 3
            BRA     DIVPOS10    ; 4 2   AccB is 0, so the jump destination is CLRA.
DIVPOS00    LDX     #16-1       ; 3 3
            CLRB                ; 2 1   clear AB
DIVPOS10    CLRA                ; 2 1   7cycle for preparation
DIVPOS01    ROL     W68+1       ; 6 3
            ROL     W68         ; 6 3
            ROLB                ; 2 1   shift AB<<1
            ROLA                ; 2 1
            SUBB    W66+1       ; 3 2
            SBCA    W66         ; 3 2
            BCS     DIVMNS00    ; 4 2
            SEC                 ; 2 1
            ROL     W68+1       ; 6 3
            ROL     W68         ; 6 3   ;
DIVPLS01    ROLB                ; 2 1
            ROLA                ; 2 1
            SUBB    W66+1       ; 3 2
            SBCA    W66         ; 3 2
            BCS     DIVMNS02    ; 4 2   ↑14cycle
DIVPLS02    SEC                 ; 2 1
DIVPLS03    ROL     W68+1       ; 6 3
            ROL     W68         ; 6 3
            DEX                 ; 4 1
            BNE     DIVPLS01    ; 4 2   ↑22cycle   / 36cycle for plus
            RTS                 ; 5 1
DIVMNS00    ASL     W68+1       ; 6 3
            ROL     W68         ; 6 3   ↑12cycle
DIVMNS01    ROLB                ; 2 1
            ROLA                ; 2 1
            ADDB    W66+1       ; 3 2
            ADCA    W66         ; 3 2
            BCS     DIVPLS03    ; 4 2   ↑14cycle
DIVMNS02    ASL     W68+1       ; 6 3
            ROL     W68         ; 6 3
            DEX                 ; 4 1
            BNE     DIVMNS01    ; 4 2   ↑20cycle   / 34cycle for minus
            ADDB    W66+1       ; 3 2
            ADCA    W66         ; 3 2
            RTS                 ; 5 1


他のCPUの例

割り算は、Z80と6502で16bit/8bitの割り算を行うルーチンを比較したもの。

Z80 Routines:Math:Division - WikiTIは Z80の各種除算ルーチンを載せている。

下はZ80 Routines:Math:Divisionに掲載されている Z80用16/16 division。小さい。

限られているとは言え、16bit演算が使えるZ80は羨ましい。


div_ac_de:
   ld	hl, 0
   ld	b, 16
_loop:
   sll	c
   rla
   adc	hl, hl
   sbc	hl, de
   jr	nc, $+4
   add	hl, de
   dec	c
   djnz	_loop
   ret


リンク