今回は前回追加した機能を使って COMET2A 用の簡単なサンプルプログラムを作ってみましょう。
前回までに作成したサブルーチンのいくつかを COMET2A 用に書き直してライブラリ lib.cas にまとめておくことにします。また、サブルーチン内でレジスタの値を書き換える場合、GR0 以外のレジスタ (GR1 - GR7) は元の値に戻すためスタックに退避していましたが、今回から GR0 と GR1 は保護しなくてもよいことにします。それ以外のレジスタ (GR2 - GR7) は今までどおり保護するものとします。
それから、プログラムをロードして実行するとき、lib.cas をリンクするように関数 asm-run を修正します。
リスト : プログラムの実行 (defun asm-run (name &optional (dump-num 32)) (load-code (assemble (append (read-casl2-file name) (read-casl2-file "lib.cas")))) ; 0 から開始 (vm 0 dump-num))
ライブラリの詳細はプログラムリストをお読みください。
最初は簡単な再帰呼び出しの例を示しましょう。フィボナッチ (fibonacci) 関数は再帰的に定義される関数です。
階乗と同様に、フィボナッチ関数も再帰定義を使えば簡単にプログラムできます。Common Lisp でプログラムすると次のようになります。
リスト : フィボナッチ関数 (defun fibo (n) (if (< n 2) 1 (+ (fibo (1- n)) (fibo (- n 2)))))
* (dotimes (x 20) (print (fibo x))) 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 NIL
関数 fibo は階乗のプログラムとは違い、自分自身を 2 回呼び出しています。これを「二重再帰」といいます。fibo の関数呼び出しをトレースすると下図のようになります。
fibo(5) ┬ fibo(4) ┬ fibo(3) ┬ fibo(2) ┬ fibo(1) │ │ │ │ │ │ │ └ fibo(0) │ │ └ fibo(1) │ └ fibo(2) ┬ fibo(1) │ │ │ └ fibo(0) │ └ fibo(3) ┬ fibo(2) ┬ fibo(1) │ │ │ └ fibo(0) └ fibo(1) 図 : 関数 fibo のトレース
同じ値を何回も求めているため、効率はとても悪くなります。これを COMET2A でプログラムすると次のようになります。
リスト : フィボナッチ関数 ; 入力 +2) : N ; 出力 gr0 fibo (link gr7 0) (push 0 gr2) (lad gr1 2) (ld gr0 2 gr7) (cpl gr0 gr1) (jmi fibo-lab1) ; N は 2 未満 (ld gr1 gr0) (lad gr1 -1 gr1) ; N - 1 -> gr1 (push 0 gr1) (call fibo) ; -> gr0 (pop gr1) (ld gr2 gr0) ; fibo(N-1) -> gr2 (lad gr1 -1 gr1) ; N - 2 -> gr1 (push 0 gr1) (call fibo) ; fibo(N-2) -> gr0 (pop gr1) (addl gr0 gr2) ; fibo(N-1) + fibo(N-2) fibo-exit (pop gr2) (unlk gr7) (ret) fibo-lab1 (lad gr0 1) (jump fibo-exit)
最初に引数 N を取り出して gr0 にセットします。gr0 が 2 未満の場合は fibo-lab1 へジャンプし、gr0 に返り値 1 をセットします。そうでなければ、fibo を 2 回再帰呼び出しします。とくに難しいところはないので、コメントをみながらリストをお読みください。
テストプログラムと実行結果を示します。
リスト : フィボナッチ関数のテスト test-fibo (lad gr3 0) (lad gr4 20) test-fibo-loop (cpl gr3 gr4) (jze test-fibo-exit) (lad sp -1 sp) (st gr3 0 sp) (call fibo) (st gr0 0 sp) (call print) (call newline) (lad sp 1 sp) (lad gr3 1 gr3) (jump test-fibo-loop) test-fibo-exit (halt)
* (asm-run "fibo.cas") 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 NIL
フィボナッチ関数は累算変数を使って二重再帰を末尾再帰へ変換することができます。Common Lisp でプログラムすると次のようになります。
リスト : フィボナッチ関数 (末尾再帰) (defun fibo (n) (labels ((fiboi (n a1 a2) (if (zerop n) a1 (fiboi (- n 1) (+ a1 a2) a1)))) (fiboi n 1 0)))
累算変数 a1 と a2 の使い方がポイントです。現在のフィボナッチ数を変数 a1 に、ひとつ前の値を変数 a2 に格納しておきます。あとは a1 と a2 を足し算して、新しいフィボナッチ数を計算すればいいわけです。fiboi の呼び出しを図に示すと、次のようになります。
(fiboi 5 1 0) (fiboi 4 1 1) (fiboi 3 2 1) (fiboi 2 3 2) (fiboi 1 5 3) (fiboi 0 8 5) => a1 の値 8 を返す => 8 => 8 => 8 => 8 => 8 図 : fiboi の呼び出し
二重再帰では、同じ値を何回も求めていたため効率がとても悪かったのですが、このプログラムでは無駄な計算を行っていないので、値を高速に求めることができます。末尾再帰になっているので、Lisp / Scheme のように末尾再帰最適化を行う処理系では、プログラムを高速に実行できるでしょう。
これをそのまま COMET2A でプログラムすると次のようになります。
リスト : フィボナッチ関数 (末尾再帰版) ; 入力 : +2) N ; : +3) a1 ; : +4) a2 ; 出力 : gr0 fiboi-sub (link gr7 0) (ld gr0 2 gr7) (jze fiboi-zero) (lad sp -3 sp) (lad gr0 -1 gr0) ; N - 1 (st gr0 0 sp) (ld gr0 3 gr7) ; a1 (st gr0 2 sp) ; a1 をセット (addl gr0 4 gr7) ; a1 + a2 (st gr0 1 sp) ; a1 + a2 をセット (call fiboi-sub) ; 再帰呼び出し (lad sp 3 sp) fiboi-exit (unlk gr7) (ret) fiboi-zero (ld gr0 3 gr7) ; a1 を返す (jump fiboi-exit) ; 入力 : +2) N ; 出力 : gr0 fiboi (link gr7 0) (lad sp -3 sp) (ld gr0 2 gr7) (st gr0 0 sp) ; N をセット (lad gr0 1) (st gr0 1 sp) ; a1 (1) をセット (xor gr0 gr0) (st gr0 2 sp) ; a2 (0) をセット (call fiboi-sub) (lad sp 3 sp) (unlk gr7) (ret)
サブルーチン fiboi から fiboi-sub を呼び出します。fiboi-sub は自分自身を 1 回だけ再帰呼び出します。その返り値 gr0 がそのまま fiboi-sub の返り値になっていて、末尾再帰になっていることがよくわかると思います。
アセンブリ言語の場合、Lisp / Scheme など関数型言語のように末尾再帰最適化が行われることはありません。プログラマが手作業で繰り返しに変換する必要があります。ご参考までに、末尾再帰を繰り返しに変換したプログラムを示します。
リスト : フィボナッチ関数 (繰り返し版) ; 入力 : +2) N ; 出力 ; gr0 fibol (link gr7 0) (push 0 gr2) (push 0 gr3) ; a3 (WORK) (ld gr3 2 gr7) ; N (lad gr0 1) ; a1 = 1 (xor gr1 gr1) ; a2 = 0 fibol-loop (ld gr3 gr3) (jze fibol-exit) (ld gr2 gr0) (addl gr2 gr1) ; a1 + a2 -> a3 (ld gr1 gr0) ; a1 -> a2 (ld gr0 gr2) ; a3 -> a1 (lad gr3 -1 gr3) (jump fibol-loop) fibol-exit (pop gr3) (pop gr2) (unlk gr7) (ret)
もうひとつ簡単な数値計算の例を示しましょう。負でない整数 a と b の最大公約数を求めるプログラムを「ユークリッド(Euclid) の互除法」で作ります。
[ユークリッドの互除法] 負でない整数 a と b (a > b) で、a を b で割った余りを r とする。 このとき、a と b の最大公約数は b と r の最大公約数に等しい。
これを Common Lisp でプログラムすると次のようになります。
リスト : 最大公約数を求める (defun my-gcd (a b) (if (zerop b) a (my-gcd b (mod a b))))
* (my-gcd 42 30) 6 * (my-gcd 15 70) 5
Common Lisp には関数 gcd が用意されているので、ここでは関数名を my-gcd としました。とても簡単ですね。
COMET2A のプログラムも簡単です。次のリストを見てください。
リスト : 最大公約数を求める (ユークリッドの互除法) ; 入力 +2) : a ; +3) : b ; 出力 gr0 gcd (link gr7 0) (ld gr0 2 gr7) ; a (ld gr1 3 gr7) ; b (jze gcd-exit) (divl gr0 gr1) ; -> gr0 (商), gr1 (余り) (lad sp -2 sp) (ld gr0 3 gr7) (st gr0 0 sp) ; b をセット (st gr1 1 sp) ; a % b をセット (call gcd) (lad sp -2 sp) gcd-exit (unlk gr7) (ret)
引数 a, b を取り出して、gr0, gr1 にセットします。gr1 が 0 ならば gr0 をそのまま返します。これが再帰呼び出しの停止条件になります。そうでなければ、divl で gr0 / gr1 を計算して、gcd を再帰呼び出しするだけです。
テストプログラムと実行結果を示します。
リスト : gcd の簡単なテスト test-gcd (lad sp -2 sp) (lad gr0 42) (st gr0 0 sp) (lad gr0 30) (st gr0 1 sp) (call gcd) (st gr0 0 sp) (call print) (call newline) (lad gr0 15) (st gr0 0 sp) (lad gr0 70) (st gr0 1 sp) (call gcd) (st gr0 0 sp) (call print) (call newline) (lad sp 2 sp) (halt)
* (asm-run "test0.cas") 6 5
サブルーチン gcd は末尾再帰になっているので、繰り返しに変換することも簡単にできます。興味のある方は繰り返しバージョンのプログラムを作ってみてください。
次は簡単なソートプログラムを作ってみましょう。「バブルソート (buble sort) 」は泡がぶくぶくと浮いてくるように、いちばん小さいデータが後ろから前に浮かび上がってくるアルゴリズムです。
隣接する 2 つのデータを比較して、順序が逆であれば入れ換えます。これを順番に後ろから前に行っていけば、いちばん小さなデータは頂上に浮かび上がっているというわけです。先頭が決まったならば、残りのデータに対して同じことを行えば、2 番目には残りのデータの中でいちばん小さいものが浮かび上がってきます。これをデータ数だけ繰り返せばソートが完了します。
9 5 3 7 6 4 8 交換しない ~~~ 9 5 3 7 6 4 8 交換する ~~~ 9 5 3 7 4 6 8 交換する ~~~ 9 5 3 4 7 6 8 交換しない ~~~ 9 5 3 4 7 6 8 交換する ~~~ 9 3 5 4 7 6 8 交換する ~~~ 3 9 5 4 7 6 8 いちばん小さいデータが決定する + 残りのデータに対して同様な操作を行う 図 : バブルソート
これをそのまま Common Lisp でプログラムすると次のようになります。
リスト : バブルソート (Common Lisp 版) (defun buble-sort (buff) (do ((k (1- (length buff))) (i 0 (1+ i))) ((= i k) buff) (do ((j k (1- j))) ((= i j)) (when (> (aref buff (1- j)) (aref buff j)) (rotatef (aref buff (1- j)) (aref buff j))))))
* (buble-sort #(5 6 4 7 3 8 2 9 1 0)) #(0 1 2 3 4 5 6 7 8 9)
rotatef は値を交換するマクロです。
rotatef place-1 place-2 ... place-n
rotatef は setf と同様に汎変数を引数として受け付けます。そして、2 番目から n 番目の引数の値を 1 番目から n - 1 番目の引数にセットし、1 番目の引数の値を n 番目の引数にセットします。つまり、先頭と末尾の変数が連結されていて、引数の値をひとつずつ左へシフトする、という「回転シフト (rotate shift) 」の動作になります。
簡単な例を示しましょう。
* (let ((a 10) (b 20) (c 30)) (rotatef a b c) (list a b c)) (20 30 10)
b の値が a に、c の値が b に、そして a の値が c にセットされるので、(20 30 10) というリストが返されます。引数が 2 つの場合、rotatef を使って値を交換することができます。
COMET2A でバブルソートをプログラムすると次のようになります。
リスト : バブルソート ; 入力 : +2) バッファ ; +3) 個数 ; 出力 : None buble-sort (link gr7 0) (push 0 gr2) (push 0 gr3) (push 0 gr4) (ld gr1 2 gr7) ; 先頭アドレス (ld gr2 gr1) (addl gr2 3 gr7) (lad gr2 -1 gr2) ; 末尾要素へのアドレス buble-sort-loop1 (cpl gr1 gr2) ; 先頭アドレスを +1 していき (jze buble-sort-exit) ; 最後まで調べたら終了 ; (ld gr3 gr2) buble-sort-loop2 ; 末尾から gr1 までループする (cpl gr3 gr1) (jze buble-sort-exit2) (ld gr0 0 gr3) (cpa gr0 -1 gr3) (jpl buble-sort-lab1) ; 値の交換 (ld gr4 gr0) (ld gr0 -1 gr3) (st gr0 0 gr3) (st gr4 -1 gr3) buble-sort-lab1 (lad gr3 -1 gr3) (jump buble-sort-loop2) ; buble-sort-exit2 (lad gr1 1 gr1) (jump buble-sort-loop1) buble-sort-exit (pop gr4) (pop gr3) (pop gr2) (unlk gr7) (ret)
gr1 がバッファの先頭アドレス、gr2 が末尾要素へのアドレスです。最初のループ (buble-sort-loop1) で gr1 を一つずつ減らしていき、2 番目のループ (buble-sort-loop2) で末尾から gr1 まで順番に要素を調べていきます。このとき、要素を示すポインタに gr3 を使っています。要素 (gr3 + 0) と要素 (gr3 - 1) を比較し、(gr3 + 0) の方が小さい場合は値を交換します。
テストプログラムと実行結果を示します。
リスト : バブルソートの簡単なテスト test-buble-sort (lad sp -2 sp) (lad gr0 data00) (st gr0 0 sp) (ld gr0 len00) (st gr0 1 sp) (call buble-sort) (call print-vector) (lad sp 2 sp) (halt) len00 (dc 10) data00 (dc 5 6 4 7 3 8 2 9 1 0)
* (asm-run "buble.cas") 0 1 2 3 4 5 6 7 8 9 NIL
選択ソート (selection sort) は、ソートしていないデータの中から最小値(または最大値)を見つけ、それを先頭のデータと交換する、という手順を繰り返すことでソートを行います。最初は、すべてのデータの中から最小値を探し、それを配列の先頭 buff[0] と交換します。次は、buff[1] 以降のデータの中から最小値を探し、それを buff[1] と交換します。これを繰り返すことでソートすることができます。
[9 5 3 7 6 4 8] 3 と 9 を交換する + + 3 [5 9 7 6 4 8] 5 と 4 を交換する + + 3 4 [9 7 6 5 8] 9 と 5 を交換する + + 3 4 5 [7 6 9 8] 7 と 6 を交換する + + 3 4 5 6 [7 9 8] 7 と 7 を交換する + 3 4 5 6 7 [9 8] 9 と 8 を交換してソート終了 + + 図 : 選択ソート
このように、選択ソートは単純でわかりやすいアルゴリズムです。
選択ソートの場合、ベクタの中から要素を選択する関数を引数として渡すことができると便利です。Common Lisp でのプログラムは次のようになります。
リスト : 選択ソート (Common Lisp 版) ; ベクタの中から最大値を求める (defun max-vector (buff &optional (start 0)) (do ((pos start) (val (aref buff start)) (i start (1+ i))) ((= i (length buff)) (values val pos)) (when (< val (aref buff i)) (setf pos i val (aref buff i))))) ; ベクタの中から最小値を求める (defun min-vector (buff &optional (start 0)) (do ((pos start) (val (aref buff start)) (i start (1+ i))) ((= i (length buff)) (values val pos)) (when (> val (aref buff i)) (setf pos i val (aref buff i))))) ; 選択ソート (defun select-sort (buff selector) (do ((i 0 (1+ i))) ((= i (1- (length buff))) buff) (multiple-value-bind (val pos) (funcall selector buff i) (shiftf (aref buff pos) (aref buff i) val))))
関数 max-vector はベクタの中から最大値を探して、その値と位置を返します。逆に、関数 min-vector は最小値とその位置を返します。要素を選択する関数を用意すると、選択ソートのプログラムは簡単です。ベクタの i 番目から末尾までの中から selector で要素を選び、それと i 番目の要素を交換するだけです。値の交換にはマクロ shiftf を使っています。
shiftf place-1 place-2 ... place-n new-value
shiftf は setf と同様に汎変数を引数として受け付けます。2 番目から n 番目の引数の値を 1 番目から n - 1 番目の引数にセットし、new-value を n 番目の引数にセットします。つまり、引数の値をひとつずつ左へシフトする動作になります。
簡単な例を示しましょう。
* (let ((a 10) (b 20) (c 30) (d 40)) (shiftf a b c d) (list a b c)) (20 30 40)
b の値が a に、c の値が b に、そして d の値が c にセットされるので、(20 30 40) というリストが返されます。
COMET2A でも引数にサブルーチンを渡すことができます。次のリストを見てください。
リスト : 選択ソート ; 最大値を求める ; 入力 +2) バッファ ; +3) 個数 ; 出力 gr0 最大値、gr1 アドレス max (link gr7 0) (push 0 gr2) (push 0 gr3) (ld gr1 2 gr7) ; 先頭アドレス (ld gr2 3 gr7) (addl gr2 gr1) ; 末尾アドレス (ld gr3 gr1) (ld gr0 0 gr3) ; 先頭要素を仮の最大値とする (lad gr3 1 gr3) ; 次の要素から調べる max-loop (cpl gr3 gr2) (jze max-exit) (cpa gr0 0 gr3) (jpl max-lab1) ; 最大値を書き換える (ld gr0 0 gr3) (ld gr1 gr3) max-lab1 (lad gr3 1 gr3) (jump max-loop) max-exit (pop gr3) (pop gr2) (unlk gr7) (ret) ; 最小値を求める ; 入力 +2) バッファ ; +3) 個数 ; 出力 gr0 最小値、gr1 アドレス min (link gr7 0) (push 0 gr2) (push 0 gr3) (ld gr1 2 gr7) ; 先頭アドレス (ld gr2 3 gr7) (addl gr2 gr1) ; 末尾アドレス (ld gr3 gr1) (ld gr0 0 gr3) ; 先頭要素を仮の最小値とする (lad gr3 1 gr3) ; 次の要素から調べる min-loop (cpl gr3 gr2) (jze min-exit) (cpa gr0 0 gr3) (jmi min-lab1) (jze min-lab1) ; 最小値を書き換える (ld gr0 0 gr3) (ld gr1 gr3) min-lab1 (lad gr3 1 gr3) (jump min-loop) min-exit (pop gr3) (pop gr2) (unlk gr7) (ret) ; 選択ソート ; 入力 : +2) バッファ ; +3) 個数 ; +4) 要素を選択するサブルーチン ; 出力 : None select-sort (link gr7 0) (push 0 gr2) (push 0 gr3) (push 0 gr4) (ld gr2 2 gr7) ; 先頭アドレス (ld gr3 3 gr7) ; 個数 select-sort-loop (lad gr0 1) (cpl gr0 gr3) (jze select-sort-exit) ; 末尾要素まで調べたら終了 (lad sp -2 sp) (st gr2 0 sp) (st gr3 1 sp) (ld gr0 4 gr7) ; 選択用サブルーチンを取り出す (call 0 gr0) ; -> gr0 (要素), gr1 (アドレス) (lad sp 2 sp) (ld gr4 0 gr2) ; 先頭の要素 (st gr4 0 gr1) ; 交換する (st gr0 0 gr2) (lad gr2 1 gr2) (lad gr3 -1 gr3) (jump select-sort-loop) select-sort-exit (pop gr4) (pop gr3) (pop gr2) (unlk gr7) (ret)
最大値を求めるサブルーチンが max で、最小値を求めるサブルーチンが min です。どちらのサブルーチンも難しいところはないと思います。選択ソートを行う select-sort のポイントは、引数に渡された選択用のサブルーチンを呼び出すところです。(ld gr0 4 gr7) で引数からサブルーチンのアドレスを取り出して gr0 にセットします。CALL 命令は間接アドレッシングができるので、あとは (call 0 gr0) を実行すれば選択用のサブルーチンを呼び出すことができます。
選択ソードの簡単なテストと実行結果を示します。
リスト : 選択ソートの簡単なテスト test-select-sort (lad sp -3 sp) (lad gr0 data01) (st gr0 0 sp) (ld gr0 len01) (st gr0 1 sp) (lad gr0 min) ; 最小値を選択 (st gr0 2 sp) (call select-sort) (call print-vector) (lad sp 3 sp) (halt) len01 (dc 10) data01 (dc 5 6 4 7 3 8 2 9 1 0)
* (asm-run "select.cas") 0 1 2 3 4 5 6 7 8 9 NIL
単純挿入ソートはソート済みのベクタに新しいデータを挿入していくことでソートします。最初は先頭のデータひとつがソート済みのベクタと考え、2 番目のデータをそこに挿入することからスタートします。データを挿入するので、そこにあるデータをどかさないといけません。そこで、挿入位置を決めるため後ろから順番にデータを比較するとき、いっしょにデータの移動も行うことにします。
[9] 5 3 7 6 4 8 5 を取り出す [9] * 3 7 6 4 8 5 を[9]の中に挿入する [5 9] 3 7 6 4 8 9 をひとつずらして先頭に 5 を挿入 [5 9] * 7 6 4 8 3 を取り出して[5 9]の中に挿入する [3 5 9] 7 6 4 8 先頭に 3 を挿入 [3 5 9] * 6 4 8 7 を取り出して[3 5 9] に挿入 [3 5 7 9] 6 4 8 9 を動かして 7 を挿入 残りの要素も同様に行う 図 : 単純挿入ソート
ソートする場合、要素を比較する関数を渡すことができると便利です。比較関数を comp とすると、(comp a b) は a が b よりも小さい場合は負の値を、等しい場合は 0 を、a が b よりも大きい場合は正の値を返すものとします。Common Lisp でプログラムすると次のようになります。
リスト : 単純挿入ソート (Common Lisp 版) (defun insert-sort (buff comp) (do ((i 1 (1+ i))) ((= i (length buff)) buff) (do ((tmp (aref buff i)) (j i (1- j))) ((zerop j) (setf (aref buff j) tmp)) (cond ((<= 0 (funcall comp tmp (aref buff (1- j)))) (setf (aref buff j) tmp) (return)) (t (setf (aref buff j) (aref buff (1- j)))))))) ; 比較関数 (defun comp (a b) (- a b))
* (insert-sort #(5 6 4 7 3 8 2 9 1 0) #'comp) #(0 1 2 3 4 5 6 7 8 9)
アルゴリズムをそのままプログラムしただけなので、とくに難しいところはないと思います。COMET2A でのプログラムは次のようになります。
リスト : 単純挿入ソート ; 入力 +2) : バッファ ; +3) : 個数 ; +4) : 比較用サブルーチン ; 出力 None insert-sort (link gr7 0) (push 0 gr3) (push 0 gr4) (push 0 gr5) (push 0 gr6) (ld gr3 2 gr7) (ld gr4 gr3) (lad gr4 1 gr4) ; gr4 の示すデータを挿入する (addl gr3 3 gr7) ; 末尾アドレス insert-sort-loop (cpl gr4 gr3) ; 最後まで調べたか (jze insert-sort-exit) ; (ld gr5 gr4) (ld gr6 0 gr5) ; 挿入するデータ -> gr6 insert-sort-loop1 (cpl gr5 2 gr7) ; 先頭に到達したか (jze insert-sort-exit1) ; データを比較する (lad sp -2 sp) (st gr6 0 sp) (ld gr0 -1 gr5) (st gr0 1 sp) (ld gr0 4 gr7) (call 0 gr0) ; -> gr0 (-,0,+) (lad sp 2 sp) (and gr0 gr0) (jpl insert-sort-exit1) ; gr6 が小さい (ld gr0 -1 gr5) (st gr0 0 gr5) (lad gr5 -1 gr5) (jump insert-sort-loop1) insert-sort-exit1 ; (gr5 + 0) の位置に gr6 を挿入 (st gr6 0 gr5) ; (lad gr4 1 gr4) (jump insert-sort-loop) insert-sort-exit (pop gr6) (pop gr5) (pop gr4) (pop gr3) (unlk gr7) (ret)
insert-sort は二重ループになっているのでちょっと複雑に見えますが、アルゴリズムをそのままプログラムしているだけです。コメントをみながら、リストを読んでみてください。
簡単なテストプログラムと実行結果を示します。
リスト : 挿入ソートの簡単なテスト test-insert-sort (lad sp -3 sp) (lad gr0 data02) (st gr0 0 sp) (ld gr0 len02) (st gr0 1 sp) (lad gr0 cmp) (st gr0 2 sp) (call insert-sort) (call print-vector) (lad sp 3 sp) (halt) len02 (dc 10) data02 (dc 5 6 4 7 3 8 2 9 1 0) ; 比較用サブルーチン cmp (link gr7 0) (ld gr0 2 gr7) (suba gr0 3 gr7) (unlk gr7) (ret)
* (asm-run "insert.cas") 0 1 2 3 4 5 6 7 8 9 NIL
次は高速なソートアルゴリズムとして有名な「クイックソート (quick sort) 」を取り上げます。クイックソートはある値を基準にして、要素をそれより大きいものと小さいものの 2 つに分割していくことでソートを行います。2 つに分けた各々の区間を同様に分割して 2 つの区間に分けます。最後は区間の要素がひとつになってソートが完了します。
9 5 3 7 6 4 2 8 最初の状態 9 5 3 7 6 4 2 8 7 を枢軸にして左側から 7 以上の値を探し、 L R 右側から 7 以下の値を探す。 2 5 3 7 6 4 9 8 交換する L R 2 5 3 7 6 4 9 8 検索する L R 2 5 3 4 6 7 9 8 交換する L R 2 5 3 4 6 7 9 8 検索する。R と L が交差したら分割終了。 R L [2 5 3 4 6] [7 9 8] この 2 つの区間について再び同様な分割を行う 図 : クイックソート
基準になる値のことを「枢軸 (pivot) 」といいます。枢軸は要素の中から適当な値を選びます。今回は中央にある要素を選ぶことにしましょう。上図を見てください。左側から枢軸 7 以上の要素を探し、左側から 7 以下の要素を探します。探索のときは枢軸が番兵の役割を果たすので、ソート範囲外の要素を探索することはありません。見つけたらお互いの要素を交換します。探索位置が交差したら分割は終了です。
あとは同じ手順を分割した 2 つの区間に適用します。これは再帰定義を使えば簡単に実現できます。分割した区間の要素数が 1 になったときが再帰の停止条件になります。
Common Lisp でプログラムすると次のようになります。
リスト : クイックソート (Common Lisp 版) (defun qsort (buff low high comp) (do ((pivot (aref buff (floor (+ low high) 2))) (i low (1+ i)) (j high (1- j))) () (do () ((<= 0 (funcall comp (aref buff i) pivot))) (incf i)) (do () ((<= 0 (funcall comp pivot (aref buff j)))) (decf j)) (when (>= i j) (when (< low (1- i)) (qsort buff low (1- i) comp)) (when (< (1+ j) high) (qsort buff (1+ j) high comp)) (return)) (rotatef (aref buff i) (aref buff j)))) (defun quick-sort-vector (pred buff) (qsort buff 0 (1- (length buff)) pred) buff)
関数 qsort の引数 buff がソートするベクタ、low が区間の下限値、high が区間の上限値、comp が要素を比較する関数です。qsort は buff の low から high までの区間をソートします。最初の do ループで区間の中央にあるデータを枢軸 pivot として選び、pivot を基準にして区間を 2 つに分けます。
次の do ループで、左側から枢軸以上の要素を探しています。ここでは枢軸以上という条件を、枢軸より小さい間は探索位置を進める、というように置き換えています。同様に次の do ループで右側から枢軸以下の要素を探します。お互いの探索位置 i, j が交差したら分割は終了です。そうでなければお互いの要素を交換します。
そして、分割した区間に対して qsort を再帰呼び出しします。このとき要素数をチェックして、2 個以上ある場合に再帰呼び出しを行います。この停止条件を忘れると正常に動作しません。ご注意ください。
これをそのまま COMET2A でプログラムすると、次のようになります。
リスト : クイックソート ; 入力 : +2) バッファ ; +3) 個数 ; +4) 比較関数 ; 出力 : None quick-sort (link gr7 0) (push 0 gr2) (push 0 gr3) (ld gr1 2 gr7) ; 先頭アドレス (ld gr2 3 gr7) (addl gr2 gr1) (lad gr2 -1 gr2) ; 末尾要素へのアドレス (ld gr3 4 gr7) ; 比較関数 (lad sp -3 sp) (st gr1 0 sp) (st gr2 1 sp) (st gr3 2 sp) (call qsort-sub) (lad sp 3 sp) (pop gr3) (pop gr2) (unlk gr7) (ret) ; qsort-sub ; 入力 +2) 先頭アドレス ; +3) 末尾要素へのアドレス ; +4) 比較関数 ; 出力 None qsort-sub (link gr7 0) (push 0 gr3) (push 0 gr4) (push 0 gr5) (push 0 gr6) (ld gr5 2 gr7) ; 先頭アドレス (i) (ld gr6 3 gr7) ; 末尾アドレス (j) (ld gr3 gr5) (addl gr3 gr6) (jov qsort-sub-ov) ; 桁あふれ (srl gr3 1) ; 枢軸の位置 (ld gr3 0 gr3) ; 枢軸 (jump qsort-sub-loop) qsort-sub-ov (srl gr3 1) (lad gr0 #x8000) (or gr3 gr0) ; 枢軸の位置 (ld gr3 0 gr3) ; 枢軸 qsort-sub-loop (lad sp -2 sp) (ld gr0 0 gr5) (st gr0 0 sp) ; buff[i] => data (st gr3 1 sp) ; pivot (ld gr0 4 gr7) (call 0 gr0) ; -> gr0 (lad sp 2 sp) (ld gr0 gr0) (jpl qsort-sub-lab1) ; data >= pivot でジャンプ (lad gr5 1 gr5) ; i++ (jump qsort-sub-loop) qsort-sub-lab1 (lad sp -2 sp) (st gr3 0 sp) ; pivot (ld gr0 0 gr6) (st gr0 1 sp) ; buff[j] => data (ld gr0 4 gr7) (call 0 gr0) ; -> gr0 (lad sp 2 sp) (ld gr0 gr0) (jpl qsort-sub-lab2) ; pivot >= data でジャンプ (lad gr6 -1 gr6) ; j-- (jump qsort-sub-lab1) qsort-sub-lab2 (cpl gr5 gr6) (jpl qsort-sub-lab3) ; i >= j でジャンプ ; 交換する (ld gr0 0 gr5) (ld gr4 0 gr6) (st gr4 0 gr5) (st gr0 0 gr6) (lad gr5 1 gr5) ; i++ (lad gr6 -1 gr6) ; j-- (jump qsort-sub-loop) qsort-sub-lab3 (lad gr5 -1 gr5) ; i - 1 (ld gr0 2 gr7) ; low -> gr0 (cpl gr0 gr5) (jpl qsort-sub-lab4) ; low >= i - 1 でジャンプ (lad sp -3 sp) (st gr0 0 sp) (st gr5 1 sp) (ld gr0 4 gr7) (st gr0 2 sp) (call qsort-sub) ; 再帰 (lad sp 3 sp) qsort-sub-lab4 (lad gr6 1 gr6) ; j + 1 (ld gr0 3 gr7) ; high (cpl gr6 gr0) (jpl qsort-sub-exit) ; j + 1 >= high ならばジャンプ (lad sp -3 sp) (st gr6 0 sp) (st gr0 1 sp) (ld gr0 4 gr7) (st gr0 2 sp) (call qsort-sub) ; 再帰 (lad sp 3 sp) qsort-sub-exit (pop gr6) (pop gr5) (pop gr4) (pop gr3) (unlk gr7) (ret)
実際の処理はサブルーチン qsort-sub で行います。プログラムが長くなったので、処理を追いかけるのは大変だと思いますが、コメントをみながらリストを読んでみてください。処理を適切なサブルーチンに分割したほうが良かったのかもしれませんね。
簡単なテストプログラムと実行例を示します。
リスト : クイックソートの簡単なテスト test-quick-sort (lad sp -3 sp) (lad gr0 data03) (st gr0 0 sp) (ld gr0 len03) (st gr0 1 sp) (lad gr0 cmp) (st gr0 2 sp) (call quick-sort) (call print-vector) (lad sp 3 sp) (lad gr0 data03) (svc 1) (halt) len03 (dc 10) data03 (dc 5 6 4 7 3 8 2 9 1 0)
* (asm-run "quick.cas") 0 1 2 3 4 5 6 7 8 9 NIL
最後にパズル「8クイーン」を解いてみましょう。アルゴリズムは拙作のページ N Queens Problem で取り上げた「ビット演算による高速化」による解法と同じです。Common Lisp の場合、マクロを使わないでプログラムすると次のようになります。
リスト : 8クイーンの解法 (Common Lisp 版) (defun queens (nums left right a) (if (zerop nums) (print-answer (reverse a)) (do ((n nums)) ((zerop n)) (let ((q (logand (- n) n))) (when (zerop (logand (logior left right) q)) (queens (logxor nums q) (ash (logior left q) 1) (ash (logior right q) -1) (cons q a))) (setf n (logxor n q)))))) (defun print-answer (ls) (print (mapcar #'(lambda (x) (logcount (1- x))) ls)))
* (queens #xff 0 0 nil) (0 4 7 5 2 6 1 3) (0 5 7 2 6 3 1 4) ・・・省略・・・ (7 2 0 5 1 4 6 3) (7 3 0 2 5 1 6 4)
解は 92 通りあります。これをそのまま COMET2A でプログラムすると次のようになります。
リスト : 8クイーンの解法 ; ; queens.cas : 8 Queens Problems ; ; Copyright (C) 2011 Makoto Hiroi ; test-queens (lad sp -4 sp) (xor gr0 gr0) (st gr0 0 sp) (st gr0 2 sp) (st gr0 3 sp) (lad gr0 #xff) (st gr0 1 sp) (call queens) (lad sp 4 sp) (halt) ; 入力 +2) : N (手数) ; +3) : Nums (配置するクイーン) ; +4) : Left (左の利き筋) ; +5) : Right (右の利き筋) ; 出力 : None queens (link gr7 0) (push 0 gr2) (push 0 gr3) (ld gr3 3 gr7) ; 配置するクイーン -> gr3 (jze queens-found) ; クイーンをすべて配置した queens-loop (lad gr2 #xffff) (xor gr2 gr3) (lad gr2 1 gr2) ; 符号を反転 (2 の補数) (and gr2 gr3) ; クイーンをひとつ取り出す -> gr2 (ld gr0 4 gr7) (or gr0 5 gr7) (and gr0 gr2) ; (left or right) and gr2 (jnz queens-lab1) (ld gr0 2 gr7) ; N -> gr0 (st gr2 qbuff gr0) ; クイーンを書き込む (lad gr0 1 gr0) ; N+=1 (lad sp -4 sp) (st gr0 0 sp) ; 手数 (ld gr0 3 gr7) (xor gr0 gr2) ; 配置したクイーンを取り除く (st gr0 1 sp) (ld gr0 4 gr7) ; 左の利き筋 (or gr0 gr2) (sll gr0 1) (st gr0 2 sp) (ld gr0 5 gr7) ; 右の利き筋 (or gr0 gr2) (srl gr0 1) (st gr0 3 sp) (call queens) (lad sp 4 sp) queens-lab1 (xor gr3 gr2) ; ビットオフ (jnz queens-loop) queens-exit (pop gr3) (pop gr2) (unlk gr7) (ret) queens-found (lad sp -1 sp) (ld gr0 2 gr7) (st gr0 0 sp) (call print-answer) (call newline) (lad sp 1 sp) (jump queens-exit) qbuff (ds 10) ; 解の表示 ; 入力 +2) 個数 print-answer (link gr7 0) (push 0 gr2) (xor gr2 gr2) print-answer-loop (cpl gr2 2 gr7) (jze print-answer-exit) (ld gr0 qbuff gr2) (lad gr0 -1 gr0) (lad sp -1 sp) (st gr0 0 sp) (call logcount) ; ビット数 -> gr0 (st gr0 0 sp) (call print) ; 数値の表示 (lad gr0 32) ; space (st gr0 0 sp) (call write-char) (lad sp 1 sp) (lad gr2 1 gr2) (jump print-answer-loop) print-answer-exit (pop gr2) (unlk gr7) (ret)
選んだクイーンをバッファ gbuff に格納していること以外は Common Lisp のプログラムとほぼ同じです。コメントをみながら、リストを読んでみてください。実行結果は当然ですが Common Lisp のプログラムと同じです。
今回はここまでです。次回は COMET2A 用の簡単なメモリ管理プログラムを作ってみましょう。
; ; lib.cas : COMET2A 簡易シミュレータ用ライブラリ ; ; Copyright (C) 2011 Makoto Hiroi ; ; 2011/01/22 ; 規約の変更 : gr0 と gr1 は保存しなくてもよい ; ; 文字の入力 ; 入力 : 無し ; 出力 : gr0 文字 read-char (svc 2) (ret) ; 文字の出力 ; 入力 : sp + 0) リターンアドレス ; + 1) 文字 ; 出力 : 無し write-char (ld gr0 1 sp) (svc 3) (ret) ; 改行を出力 newline (lad gr0 10) (svc 3) (ret) ; 無符号整数の N 進表示 ; 入力 a + 0) : gr7 ; a + 1) : ret adr ; a + 2) : 整数 ; a + 3) : 基数 N ; 出力 : None printu (link gr7 0) (ld gr0 2 gr7) ; 整数 (ld gr1 3 gr7) ; 基数 (divl gr0 gr1) ; gr0 / gr1 -> gr0 商, gr1 余り (jze printu-l1) (push 0 gr1) ; 余りを保存 (lad sp -2 sp) ; 引数領域を確保 (st gr0 0 sp) ; 商をセット (ld gr0 3 gr7) (st gr0 1 sp) ; 基数をセット (call printu) (lad sp 2 sp) ; 引数領域を解放 (pop gr1) printu-l1 (ld gr1 code-table gr1) (push 0 gr1) ; 引数セット (call write-char) (pop gr1) ; 引数を取り除く (unlk gr7) (ret) code-table (dc "0123456789ABCDEF") ; 符号付き整数の 10 進表示 ; 入力 a + 0) : gr7 ; + 1) : ret adr ; + 2) : 整数 ; 出力 : None print (link gr7 0) (push 0 gr2) (ld gr2 2 gr7) (jpl print-l1) ; flag check ; 符号を反転する (lad gr0 #xffff) (xor gr2 gr0) ; bit を反転 (lad gr2 1 gr2) ; 1 を足す (push 45) (call write-char) ; '-' を出力 (pop gr0) print-l1 (lad sp -2 sp) ; 引数領域確保 (st gr2 0 sp) ; 整数をセット (lad gr0 10) (st gr0 1 sp) ; 基数 10 をセット (call printu) (lad sp 2 sp) ; 引数領域解放 (pop gr2) (unlk gr7) (ret) ; データの探索 ; 入力 +2) : データ ; +3) ; バッファ ; +4) : 個数 ; 出力 : gr0 位置 (0 以上の数値), -1 失敗 position (link gr7 0) (push 0 gr2) (ld gr0 2 gr7) ; データ (ld gr1 3 gr7) ; 先頭アドレス (ld gr2 4 gr7) ; 個数 (addl gr2 gr1) ; 末尾アドレス position-loop (cpl gr1 gr2) (jze position-false) ; 探索失敗 (cpa gr0 0 gr1) (jze position-true) ; 探索成功 (lad gr1 1 gr1) (jump position-loop) position-true (ld gr0 gr1) ; 位置を求める (subl gr0 3 gr7) position-exit (pop gr2) (unlk gr7) (ret) position-false (lad gr0 -1) (jump position-exit) ; バッファの初期化 ; 入力 : +2) 初期値 ; : +3) バッファ ; : +4) 個数 fill (link gr7 0) (push 0 gr2) (ld gr0 2 gr7) (ld gr1 3 gr7) (ld gr2 4 gr7) (addl gr2 gr1) ; 末尾アドレス fill-loop (cpl gr1 gr2) (jze fill-exit) (st gr0 0 gr1) (lad gr1 1 gr1) (jump fill-loop) fill-exit (pop gr2) (unlk gr7) (ret) ; ベクタの表示 ; 入力 +2) バッファアドレス ; +3) 個数 ; 出力 : None print-vector (link gr7 0) (push 0 gr2) (push 0 gr3) (ld gr2 2 gr7) ; 先頭アドレス (ld gr3 3 gr7) ; 個数 (addl gr3 gr2) ; 末尾アドレス print-vector-loop (cpl gr2 gr3) (jze print-vector-exit) (lad sp -1 sp) (ld gr0 0 gr2) (st gr0 0 sp) (call print) (lad gr0 32) ; 空白を出力 (st gr0 0 sp) (call write-char) (lad sp 1 sp) (lad gr2 1 gr2) (jump print-vector-loop) print-vector-exit (pop gr3) (pop gr2) (unlk gr7) (ret) ; ビット 1 を数える (4 bit ずつ処理する) ; 入力 (gr7 + 2) : データ ; 出力 gr0 : ビット 1 の個数 logcount (link gr7 0) (push 0 gr2) (xor gr0 gr0) (ld gr1 2 gr7) logcount-loop (ld gr2 gr1) (and gr2 logcount-mask) (addl gr0 logcount-table gr2) (srl gr1 4) (jnz logcount-loop) (pop gr2) (unlk gr7) (ret) logcount-mask (dc 15) logcount-table ; 0 1 2 3 4 5 6 7 8 9 a b c d e f (dc 0 1 1 2 1 2 2 3 1 2 2 3 2 3 3 4)