条件分岐
条件分岐
ここまでで、簡単な計算はできるようになりましたが、処理自体は命令メモリの中身を上から順に実行するだけでした。 そのため、条件分岐や繰り返し文も実現できず、実用的な計算ができません。 これらの問題を解決すべく、分岐命令を導入していきます。
条件分岐は計算結果をもとに、次に実行する命令を切り替えることと言い換えられます。 つまり、計算結果に従ってプログラムカウンタの値を変更できるようにすれば、条件分岐を実現できるでしょう。
分岐命令
これまでに作成したアキュムレータ・マシンで条件分岐を行う(=計算結果に従ってプログラムカウンタの値を変更する)には、アキュムレータの結果に従ってプログラムカウンタの内容を変更する必用があります。
計算結果が0か1かで分岐を判断するシンプルな形式の場合、条件分岐の具体的な挙動は以下のようになるでしょう。 このとき、アキュムレータの結果は更新しません。
- アキュムレータの結果が0であれば分岐が成立。次のプログラムカウンタの値は、実行したいアドレス
- アキュムレータの結果が1であれば分岐が不成立。次のプログラムカウンタの値は、いつもどおり現在の次のアドレス
この様に、0か1かで分岐を判断し、分岐が成立すれば所定のアドレスを実行する命令をBEZ
(Branch on Equal Zero) 命令と一般的に呼びます。
その命令のフォーマット(アセンブラ表記)は以下の様な形で、引数に実行したい命令メモリのアドレスを指定します。
BEZ 0010 //アキュムレータの結果が0であれば、次に0010番地の命令を実行する
分岐の成立条件を逆にして、結果が0以外であれば分岐が成立したとする命令も同時に用意されることがよくあります。
この命令は、BNZ
(Branch on Not equal Zero)命令と一般的に呼ばれます。
BNZ 0010 //アキュムレータの結果が1であれば、次に0010番地の命令を実行する
分岐命令はここまでに説明した命令と異なり、プログラムカウンタの値を直接変更し、命令の引数として次に実行したい命令メモリのアドレスを指定するものです。 これまでに説明した命令(加算など)の引数は、データメモリのアドレスであり、命令メモリのアドレスではありませんでした。
他のISAのBEZ/BNZ命令について
RISC-VやARMなど、実際に利用されているCPUの命令セットにもBEQやBNZと行った命令が存在します2。
これらの命令は今回説明したBEZ
命令と少し異なり、ある2つのレジスタの値が同値かどうかで処理を分岐させます。
本講義で扱っているアキュムレータ・マシンはもう少し原始的な計算機であるため、このような機能はありません。
RISC-Cや命令セットについては、コンピュータ・アーキテクチャに関する講義で聞いてみてください。
構成概要
分岐命令をアキュムレータ・マシンに組み込むことを考えます。 組み込んだ際に必要な動作は、分岐命令が実行され、なおかつ、アキュムレータの結果によって分岐が成立したときに、指定された命令のアドレスをプログラムカウンタに書き込むというものになります。 また、分岐命令が実行された時は、ALUからの計算結果をシンプルデータ・パス回路のレジスタに保存してはいけないという点も必用です。
具体的な動作は、以下のようになります。
- 分岐命令実行 && アキュムレータの結果が0の場合:プログラムカウンタには、実行したい命令のアドレスを書き込む。また、アキュムレータに計算結果を保存しない。
- 分岐命令実行 && アキュムレータの結果が1の場合:プログラムカウンタには、現在の値に+1した値を書き込む。また、アキュムレータに計算結果を保存しない。
- それ以外の命令の場合:これまで通りの動作。プログラムカウンタには現在の値に+1した値を書き込み、アキュムレータに計算結果を保存。
命令メモリ
条件分岐対応に対応した命令メモリの動作イメージは、マルチプレクサを利用して以下のように表せます。
図中の"BEZ?"は「selector
信号がBEZ
命令かどうかを判定する」という意味です。
分岐に対応した命令メモリは、以下の図のような回路になります。 命令メモリのモジュールの左上に、上記のマルチプレクサが追加されています。
シンプル・データパス回路
アキュムレータに計算結果を保存しない、という動作はレジスタにwrite enable(ここではBEZ
信号)信号が付く形になります。
データメモリ
これらの変更に伴いデータメモリにも変更を加えなければなりません。 具体的には、分岐を行うとレジスタを挟むため、単純に接続したものとタイミングが2サイクルだけずれてしまいます。 これを調整するために、データメモリの出力部分に2サイクル分の遅延を挿入します。 理由は後述します。
全体像
分岐に対応した各モジュールを接続した全体像は以下の通りです。
接続の関係はこれまでと同様ですが、各モジュールが分岐対応に従い変化しています。
なお、"BEZ?"はselector
信号がBEZ
命令かどうかを判定するという意味で、実際にはselector
(オペレータであり、dataout
信号の[12:9]ビット目)です。
それらの接続関係は図が複雑になるため省略しています。
実装
ここまでで示した分岐命令に関する処理を実装していきます。 分岐命令の動作は、プログラムカウンタとアキュムレータへのデータの保存に関する処理でした。 2つの処理はシンプル・データパス回路と命令メモリのモジュールにあるので、それぞれを分岐命令が必要とする形に変更します。
なお、imem.dat
とdmem.dat
はソフトウェアに相当しますので、次の節で改めて説明します。
命令メモリ
分岐判定を行うファンクションis_branch
を作成し、命令実行(BEZ,BNZ) && アキュムレータの結果を判定します。
その結果をもとに、プログラムカウンタに+1を入れるか、実行したいアドレスを代入するかを決定します。
simple_instruction_memory_w_branch.v
命令メモリの条件分岐対応
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
回路合成結果
図:simple_instruction_memory_w_branch.v
のDigitalJS Onlineによる合成結果。上半分が分岐判定関数で、その結果が右真ん中のマルチプレクサのセレクタ信号に入っている。分岐が成立した場合は、operand
が出力される。上半分は既存のシンプル・データパス回路から変化なし。結果はこちら
シンプル・データパス回路
こちらにも分岐判定を行うファンクションis_branch
を作成し、命令実行(BEZ,BNZ) && アキュムレータの結果を判定します。
その結果をもとに、レジスタに値を保持するかどうかを決定します。
simple_datapath_w_branch.v
シンプル・データパス回路の条件分岐対応
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
回路合成結果
図:simple_datapath_w_branch.v
のDigitalJS Onlineによる合成結果。下半分が分岐判定関数で、AND回路が利用されている。その結果が命令メモリのレジスタのenable信号(en
)に接続されている。上半分は既存の命令メモリから変化なし。結果はこちら
データメモリ
タイミングが2サイクルだけずれるのに対処するため、データメモリの出力部分にレジスタを2つ挿入します。
simple_memory_w_branch.v
データメモリの条件分岐対応
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
回路合成結果
図:simple_memory_w_branch.v
のDigitalJS Onlineによる合成結果。上から2行目あたりにレジスタ(DQフリップフロップ回路)が追加されている。結果はこちら
全体像
上記のモジュールの変更の接続は、これまでと同様に、テストベンチで行ないます。
大きな変更はありませんが、opcode
とoperand
を伝えるためのwireが追加、利用されています。
なお、この時点で回路は完成しますが、テストベンチでも特に何もしていません。
これは、ソフトウェアであるdmem
とimem
については次の節で解説し、この時点ではどちらもまだ未完成なためです。
testbench_accumulator_w_branch.v
条件分岐対応したもののテストベンチ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
|
細かい話
実はこの実装は実用的ではありません。
この実装は、命令メモリから命令がロードされた2サイクル後に演算が行なわれるためです。
そのため、ストア命令を発行すると、2サイクル前の値をストアすることになるので、一発NOP命令を入れないといけません。
どうしてNOP命令を入れなければならないかと言うと、chien1
とchien2
を入れたせいです。
このせいで、計算結果をストアする時はストアする前にNOP命令を入れて、1サイクル分待たなければなりません。
これをかっこよく解決する方法としては、BNZ
で分岐した際には、その前の計算結果をどこかレジスタに保持しておいて、それをストアするものが考えられます。
しかし、全体に変更が及ぶと考えられるので、今回はこのままにしておきます。
条件分岐の使用例
BNZ命令を使って、簡単な繰り返し処理を考えてみましょう。
ビルド&実行コマンド
ここからのビルドと実行コマンドは全て以下の形となります。
これは、回路に変更はなく、命令メモリとデータメモリの中身だけが変わるためです。
なお、alu4.v
やparameters.vh
は再利用できます。
iverilog testbench_accumulator_w_branch.v ./simple_datapath_w_branch.v ./simple_instruction_memory_w_branch.v ./simple_memory_w_branch.v ./alu4.v && vvp ./a.out
超簡単な例
ロードした値がゼロになるまで-1を行うという超簡単な処理は、以下のデータと命令で実現できます。
- 0x0番地の値をデータメモリからロード
- その値を-1する
- 結果が0でなければ、2に戻る
- 結果が0であれば、その結果をデータメモリにストア
imem.dat
命令メモリの中身
1 2 3 4 |
|
dmem.dat
データメモリの中身
1 2 |
|
テストベンチの出力結果
$ iverilog testbench_accumulator_w_branch.v ./simple_datapath_w_branch.v ./simple_instruction_memory_w_branch.v ./simple_memory_w_branch.v ./alu4.v && ./a.out
WARNING: ./simple_memory_w_branch.v:16: $readmemh(dmem.dat): Not enough words in the file for the requested range [0:7].
WARNING: ./simple_instruction_memory_w_branch.v:17: $readmemb(imem.dat): Not enough words in the file for the requested range [0:7].
VCD info: dumpfile testbench_accumulator_w_branch.vcd opened for output.
---DUMP Instruction MEM---
0, 0010000000000
1, 1000000000001
2, 1011000000001
3, 1001100000010
4, xxxxxxxxxxxxx
5, xxxxxxxxxxxxx
6, xxxxxxxxxxxxx
7, xxxxxxxxxxxxx
-------------------
---DUMP DATA MEM---
0, 0005
1, 0001
2, xxxx
3, xxxx
4, xxxx
5, xxxx
6, xxxx
7, xxxx
-------------------
pc | sel, we, data addr | alu2dmem, dmem2alu
00 | 0010, 0, 00000000 | 0005, 0005
01 | 1000, 0, 00000001 | 0004, 0001
02 | 1011, 0, 00000001 | xxxx, 0001
01 | 1000, 0, 00000001 | 0003, 0001
02 | 1011, 0, 00000001 | xxxx, 0001
01 | 1000, 0, 00000001 | 0002, 0001
02 | 1011, 0, 00000001 | xxxx, 0001
01 | 1000, 0, 00000001 | 0001, 0001
02 | 1011, 0, 00000001 | xxxx, 0001
01 | 1000, 0, 00000001 | 0000, 0001
02 | 1011, 0, 00000001 | xxxx, 0001
03 | 1001, 1, 00000010 | xxxx, xxxx
04 | xxxx, x, xxxxxxxx | xxxx, xxxx
05 | xxxx, x, xxxxxxxx | xxxx, xxxx
06 | xxxx, x, xxxxxxxx | xxxx, xxxx
07 | xxxx, x, xxxxxxxx | xxxx, xxxx
---DUMP DATA MEM---
0, 0005
1, 0001
2, 0000
3, xxxx
4, xxxx
5, xxxx
6, xxxx
7, xxxx
-------------------
testbench_accumulator_w_branch.v:105: $finish called at 160000 (1ps)
テストベンチの波形
do-whileループ
以下の様なdo-whileループ(必ず一回は処理が実行されるループ)も実現できます。 できますが、データの出し入れが大変なうえにNOP命令が必用になり、けっこう複雑になってしまいます。
do{
add;
loop_count--;
} while (loop_count<=0);
NOP命令とは、計算結果を保持するだけ何もしない命令になります。
各信号への値は以下のようになり、OP_THROUGH_A
でデータを回しているだけになります。
selector | write enable | ニーモニック |
---|---|---|
OP_THROUGH_A |
DISABLE |
NOP |
1001 |
0 |
以下の形のdo-whileループを実現する命令列を作ってみます。
- whileの継続条件:規定回だけ処理が行われたか
- doの処理:ただの足し算
imem.dat
命令メモリの中身
1 2 3 4 5 6 7 8 9 10 11 |
|
dmem.dat
データメモリの中身
1 2 3 4 |
|
テストベンチの出力結果
$ iverilog ./testbench_accumulator_w_branch.v ./simple_datapath_w_branch.v ./simple_instruction_memory_w_branch.v ./simple_memory_w_branch.v ./alu4.v && vvp ./a.out
WARNING: ./simple_memory_w_branch.v:16: $readmemh(dmem.dat): Not enough words in the file for the requested range [0:15].
WARNING: ./simple_instruction_memory_w_branch.v:17: $readmemb(imem.dat): Not enough words in the file for the requested range [0:15].
VCD info: dumpfile testbench_accumulator_w_branch.vcd opened for output.
---DUMP Instruction MEM---
0, 0010000000000
1, 0111000000001
2, 1001000000000
3, 1001100000000
4, 0010000000010
5, 1000000000011
6, 1001000000000
7, 1001100000010
8, 0010000000010
9, 1011000000000
10, 0010000001111
11, xxxxxxxxxxxxx
12, xxxxxxxxxxxxx
13, xxxxxxxxxxxxx
14, xxxxxxxxxxxxx
15, xxxxxxxxxxxxx
-------------------
---DUMP DATA MEM---
0, 0001
1, 0004
2, 0003
3, 0001
4, xxxx
5, xxxx
6, xxxx
7, xxxx
8, xxxx
9, xxxx
10, xxxx
11, xxxx
12, xxxx
13, xxxx
14, xxxx
15, xxxx
-------------------
pc | sel, we, data addr | alu2dmem, dmem2alu
00 | 0010, 0, 00000000 | 0001, 0001
01 | 0111, 0, 00000001 | 0005, 0004
02 | 1001, 0, 00000000 | xxxx, 0001
03 | 1001, 1, 00000000 | xxxx, 0001
04 | 0010, 0, 00000010 | 0003, 0003
05 | 1000, 0, 00000011 | 0002, 0001
06 | 1001, 0, 00000000 | xxxx, 0005
07 | 1001, 1, 00000010 | xxxx, 0003
08 | 0010, 0, 00000010 | 0002, 0002
09 | 1011, 0, 00000000 | xxxx, 0005
00 | 0010, 0, 00000000 | 0005, 0005
01 | 0111, 0, 00000001 | 0009, 0004
02 | 1001, 0, 00000000 | xxxx, 0005
03 | 1001, 1, 00000000 | xxxx, 0005
04 | 0010, 0, 00000010 | 0002, 0002
05 | 1000, 0, 00000011 | 0001, 0001
06 | 1001, 0, 00000000 | xxxx, 0009
07 | 1001, 1, 00000010 | xxxx, 0002
08 | 0010, 0, 00000010 | 0001, 0001
09 | 1011, 0, 00000000 | xxxx, 0009
00 | 0010, 0, 00000000 | 0009, 0009
01 | 0111, 0, 00000001 | 000d, 0004
02 | 1001, 0, 00000000 | xxxx, 0009
03 | 1001, 1, 00000000 | xxxx, 0009
04 | 0010, 0, 00000010 | 0001, 0001
05 | 1000, 0, 00000011 | 0000, 0001
06 | 1001, 0, 00000000 | xxxx, 000d
07 | 1001, 1, 00000010 | xxxx, 0001
08 | 0010, 0, 00000010 | 0000, 0000
09 | 1011, 0, 00000000 | xxxx, 000d
0a | 0010, 0, 00001111 | xxxx, xxxx
0b | xxxx, x, xxxxxxxx | xxxx, xxxx
0c | xxxx, x, xxxxxxxx | xxxx, xxxx
0d | xxxx, x, xxxxxxxx | xxxx, xxxx
0e | xxxx, x, xxxxxxxx | xxxx, xxxx
0f | xxxx, x, xxxxxxxx | xxxx, xxxx
---DUMP DATA MEM---
0, 000d
1, 0004
2, 0000
3, 0001
4, xxxx
5, xxxx
6, xxxx
7, xxxx
8, xxxx
9, xxxx
10, xxxx
11, xxxx
12, xxxx
13, xxxx
14, xxxx
15, xxxx
-------------------
./testbench_accumulator_w_branch.v:125: $finish called at 360000 (1ps)
テストベンチの波形
まとめ
ここでわかることとして、分岐を入れたとしても、アキュムレータマシンでちょっと込み入ったことをしようとすると、プログラムが突然難しくなることです。
原因は以下の通りです。
- ALUに入れられるものが1つしかないため、いちいち値を出し入れしないといけない。これがデカい
- 計算結果のストアの前に1サイクル待たなければいけない
- 分岐命令に使う値は直接比較できない
これではどうにもならないので、解決策としてレジスタを複数持った計算機が登場し、命令セットなどの計算機アーキテクチャの話題に発展していきます。
演習
以下のfor文を実現するimem.dat
を作成してください。
なお、for文はイテレーションが実行されない場合にも動作するようにしてください。
for(i = 0; i <= 5; i++)
n = n + 10;
for(i = 0; i <= 0; i++)
n = n + 10;
参考文献など
-
DigitalJSでうまく可視化するために、ここで示したコードそのままではありません。いろいろいじっています。 ↩