シンプルなCPUを作ってみよう (その5)
−パイプライン処理の導入−
井澤 裕司
1.はじめに
CPUを高速化する手法のひとつに,「 パイプライン処理 」があります.
本章では,これまで設計してきたCPUに,この「 パイプライン処理 」を導入した例を紹介します.
2.パイプライン処理による高速化
下の図は,CPUの「 パイプライン処理 」の概念を表しています.
この「 パイプライン処理 」は,ベルトコンベアによる 分業作業 に例えることができます.
すなわち, @フェッチ , Aデコード , B実行 , Cライトバック の4つの作業を,4人の作業員がそれぞれ分担し,
流れ作業で処理する方式です.
すべての処理を1人で行う場合に比べ,作業効率は 最高4倍 まで向上します.
3.パイプラインハザードとリソース(資源)の競合
3.1 パイプライン・ハザードとは
ベルトコンベアによる組み立て作業を導入することにより,効率は大幅に改善されますが,
問題が全くないわけではありません.
例えば,不良部品が混入している場合,ベルトコンベアを一旦停止させ,正常な部品に交換した後,
ベルトコンベアを再び起動します.
最悪の場合,ベルトコンベアの上を整理し,最初からやり直す必要があります.
CPUの処理も同様で, Jump 等の命令により, @フェッチ , Aデコード , B実行 , Cライトバック の4つの作業の
流れに乱れが生じます.
このような乱れを, パイプライン・ハザード (あるいは パイプライン・ストール )と呼びます.
下の図を用いて, パイプライン・ハザード の概念を説明しましょう.
例えば Jump 命令をライトバックのフェーズで実行すると,指定されたアドレスにプログラムカウンタ(PC)が
セットされます.
その時点で, Jump 命令に続く次の命令がそれぞれ, フェッチ , デコード , 実行 の作業を完了しているわけで,
これをそのまま実行すると,計算結果に 誤り が生じます.
このため,一旦白紙の状態に戻し, Jump 先の命令から,再度 @フェッチ , Aデコード , B実行 , Cライトバック の
処理を行う必要があります.
3.2 リソース(資源)の競合とは
Jump 命令の実行時でなくても, パイプライン・ハザード が生じることがあります.
リソース(資源)の競合による パイプライン・ハザード です.
例えば,組み立て作業でボルトを締め付けるスパナが1つしかない場合,2つの締め付け作業を同時に
行うことは不可能です.
スパナであれば2セット用意することにより解決しますが,データを保存するレジスタやメモリの場合,
正しいデータが読み込まれない状況が生じます.
データが書き込まれるのは,Cのライトバックのフェーズであり,データを読み込むのはAのデコードの
フェーズです.この間に2クロックの時間のずれがあります.
例えば,[ レジスタ 0 ]の値" 2 "に" 10 "を加算した直後に[ レジスタ 0 ]を読み出す場合について考えてみましょう.
第4章までの設計手法では,データを書き込んだ後で読み出すので,"12"という値が得られます.
ところが,パイプライン処理を導入すると,上記の時間のずれにより,書き込む前の[ レジスタ 0 ]の値" 2 "が
読み出され,正しい結果が得られません.
このようなパイプライン・ハザードを防ぐ最も簡単な方法は,プログラミング上の工夫によるものです.
次節では,その方法を紹介します.
4.プログラミングの工夫
パイプライン・ハザードを避ける最も簡単な方法は, Jump 等の命令の後や資源が競合する命令コードの間に,
何もしない命令( NOP 命令 )を必要なクロックの数だけ挿入することです.
これにより,CPUの動作に無駄が生じ,処理の効率は低下します.
しかし,もともと RISC型のCPU は,命令を極力単純化することにより,動作クロックの周波数を上げて,
処理の回数で全体的な性能能力を改善しようとする考え方に基づいて,開発されてきました.
一般のプログラムの場合, Jump 等の命令の頻度はそれほど多くなく, NOP 命令 の挿入による
性能の低下はそれほど大きいものではありません.
パイプライン処理の導入に伴い,プログラミングを以下のように訂正する必要があります.
以下に,具体的なプログラムのソースコードを示します.
赤いコードが,挿入した NOP 命令です.
なお,従来の手法では,プログラムカウンタ(PC)を更新しない hlt 命令により,
CPUを停止させることができました.
しかし,今回採用したパイプライン処理では,この hlt 命令は使えません.
hlt 命令の実行では, その後に続く同じ命令(例えば nop )が2度フェッチされるだけで,プログラムカウンタが更新
され続けるからです.
このため,下のプログラミングの例では,自分のアドレスにジャンプする jmp 命令を用いて,実質的にCPUを停止させています.
prom2.vhd
-- magafunction wizard: %LPM_ROM%
-- GENERATION: STANDARD
-- VERSION: WM1.0
-- MODULE: lpm_rom
-- (略)
0: ldl Reg0 0 -- Reg0 ← 0 1: ldl Reg1 1 -- Reg1 ← 1 2: ldl Reg2 0 -- Reg2 ← 0 3: ldl Reg3 10 -- Reg3 ← 10 4: nop -- リソースの競合を防ぐため 5: add Reg2 Reg1 -- Reg2 ← Reg2 + Reg1 6: nop -- リソースの競合を防ぐため 7: nop -- リソースの競合を防ぐため 8: add Reg0 Reg2 -- Reg0 ← Reg0 + Reg2 9: cmp Reg2 Reg3 -- Reg2 と Reg1 の比較 10: je 16 -- 一致したら 16番地に Jump 11: nop -- パイプライン・ハザードを防ぐため 12: nop -- パイプライン・ハザードを防ぐため 13: jmp 5 -- 無条件に 5番地に Jump 14: nop -- パイプライン・ハザードを防ぐため 15: nop -- パイプライン・ハザードを防ぐため 16: st Reg0 64 -- RAMの 64番地に Reg0の内容を出力 17: jmp 17 -- Jump命令によるCPUの停止(無条件に 17番地に Jump) 18: nop -- 19: nop --
architecture SYN of prom2 is signal sub_wire0 : std_logic_vector(14 downto 0); compnent lpm_rom GENERIC ( intended_device_family : string; (略) lpm_type : string; ) ; port ( outclock : in std_logic ; address : in std_logic_vector( 4 downto 0); inclock : in std_logic ; q : out std_logic_vector(14 downto 0) ); end compnent ;
begin q <= sub_wire0(14 downto 0); lpm_rom_component : lpm_rom GENERIC MAP ( intended_device_familt => "FLEX10KE" , lpm_width => 15, lpm_widthad => 5 , lpm_address_control => "REGISTERED" , lpm_outdata => "REGISTERED" , lpm_file => "prom.mif" , lpm_type => "LPM_ROM" ) port_map ( outclock => outclock, address => address, inclock => inclock, q => sub_wire0 ); end SYN;
-- CNX file retrival info
-- (略)
ROMの記述に,外部のmifファイルを用いる場合は,以下のように修正します.
-- prom2.mif
-- 15bit RISC processor
-- cpu15pipe.vhd用
-- Y.Izawa
-- H18.5.8
depth = 32;
width = 15;
address_radix = HEX;
data_radix = BIN;
content begin [00..1F] : 000000000000000 ; 00 : 100000000000000 ; -- ldl Reg0 0 01 : 100000100000001 ; -- ldl Reg1 1 02 : 100001000000000 ; -- ldl Reg2 0 03 : 100001100001010 ; -- ldl Reg3 10 04 : 000000000000000 ; -- nop 05 : 000101000100000 ; -- add Reg2 Reg1 06 : 000000000000000 ; -- nop 07 : 000000000000000 ; -- nop 08 : 000100001000000 ; -- add Reg0 Reg2 09 : 101001001100000 ; -- cmp Reg2 Reg3 0A : 101100000010000 ; -- je 16 0B : 000000000000000 ; -- nop 0C : 000000000000000 ; -- nop 0D : 110000000000101 ; -- jmp 5 0E : 000000000000000 ; -- nop 0F : 000000000000000 ; -- nop 10 : 111000001000000 ; -- st Reg0 64 11 : 110000000010001 ; -- jmp 17 12 : 000000000000000 ; -- nop 13 : 000000000000000 ; -- nop 14 : 000000000000000 ; -- nop 15 : 000000000000000 ; -- nop 16 : 000000000000000 ; -- nop 17 : 000000000000000 ; -- nop 18 : 000000000000000 ; -- nop 19 : 000000000000000 ; -- nop 1A : 000000000000000 ; -- nop 1B : 000000000000000 ; -- nop 1C : 000000000000000 ; -- nop 1D : 000000000000000 ; -- nop 1E : 000000000000000 ; -- nop 1F : 000000000000000 ; -- nop end ;
5. 命令セットの修正
パイプライン処理を実現するためには,一部の命令セットを修正する必要があります.
具体的には,これまで レジスタ間データのコピー” MOV ”に対応する命令を,何もしない ” NOP ”命令に変更します.
以下に,修正した命令セットを示します.
VHDLのソースコードは以下のように修正します.
赤で示す部分が修正箇所です.exec2.vhd
-- exec2.vhd
-- Y.Izawa
-- H18.4.25
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.std_logic_unsigned.all;
entity exec2 is port ( CLK : in std_logic ; RESET : in std_logic; OP_CODE : in std_logic_vector (3 downto 0); PC_IN : in std_logic_vector (7 downto 0); REG_A : in std_logic_vector (15 downto 0); REG_B : in std_logic_vector (15 downto 0); OP_DATA : in std_logic_vector (7 downto 0); RAM_OUT : in std_logic _vector (15 downto 0); PC_OUT : out std_logic_vector (7 downto 0); REG_IN : out std_logic_vector (15 downto 0); RAM_IN : out std_logic_vector (15 downto 0); REG_WEN : out std_logic; RAM_WEN : out std_logic ); end exec2; architecture RTL of exec2 is
signal CMP_FLAG : std_logic;
begin process (CLK, RESET) begin if (RESET = '1') then PC_OUT <= "00000000"; elsif (CLK'event and CLK = '1') then case OP_CODE is when "0000" => -- NOP REG_WEN <= ' 0 '; RAM_WEN <= ' 0 '; PC_OUT <= PC_IN + 1; when "0001" => -- ADD REG_IN <= REG_A + REG_B; REG_WEN <= '1'; REG_WEN <= '0'; PC_OUT <= PC_IN + 1; when "0010" => -- SUB (略) when "1111 " => when others => end case ; end if ; end process ; end RTL;
6. タイミング設計の修正
パイプライン処理を導入するためには,タイミング系の設計も修正する必要があります.
具体的には,ライトバックフェーズにおけるレジスタの番号と,RAMのアドレスが,デコードの
タイミングで生成されているため,1クロック分遅延させる必要があります.
この時間調整は,実行フェーズのクロックで,上記信号をラッチする処理に等価です.
これにより,それぞれの命令が,同じ位相で同時並行処理されるようになります.
パイプライン処理を行うCPUのブロック図を,以下に示します.
主な変更箇所は,
@レジスタの番号 (N_REG)
ARAMのアドレス (RAM_ADDR)
を1クロック遅延させる回路 (n_reg_dly.vhd,ram_addr_dly.vhd) を追加した点です.
以下,それらのコンポーネントのソースコードを示します.
6.1 n_reg_dly.vhd
-- n_reg_dly.vhd
-- Y.Izawa
-- H18.4.24
library IEEE;
use IEEE.std_logic_1164.all;
entity n_reg_dly is port ( CLK : in std_logic; DIN : in std_logic_vector (2 downto 0); QOUT : out std_logic_vector (2 downto 0) ); end n_reg_dly;
architecture RTL of n_reg_dly is
begin process (CLK) begin if (CLK'event and CLK = '1') then QOUT <= DIN ; end if ; end process ; end RTL;
6.2 ram_addr_dly.vhd
-- ram_addr_dly.vhd
-- Y.Izawa
-- H18.4.24
library IEEE;
use IEEE.std_logic_1164.all;
entity ram_addr_dly is port ( CLK : in std_logic; ADDR_IN : in std_logic_vector (7 downto 0); ADDR_OUT : out std_logic_vector (7 downto 0) ); end ram_addr_dly;
architecture RTL of ram_addr_dly is
begin process (CLK) begin if (CLK'event and CLK = '1') then ADDR_OUT <= ADDR_IN ; end if ; end process ; end RTL;
7. パイプライン処理のCPU
パイプライン処理を用いたCPUのソースコードは以下のようになります.
赤で示したコードが修正した部分です.
すべてのコンポーネントが,基本クロックCLKで動作している点に注意して下さい.
パイプライン処理によるCPUのソースコード(cpu15pipe.vhd)
-- cpu15pipe.vhd
-- Y.Izawa
-- H18.4.24
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.std_logic_unsigned.all;
entity cpu15pipe is
port
(CLK : in std_logic; RESET : in std_logic; IO65_IN : in std_logic_vector (15 downto 0); IO64_OUT : out std_logic_vector (15 downto 0) ); end cpu15pipe;
architecture RTL of cpu15pipe is
component prom2 (略) end component;
component decode (略) end component;
component reg_dc (略) end component;
component exec2 (略) end component;
component n_reg_dly (略) end component;
component ram_addr_dly (略) end component;
component reg_wb (略) end component
component ram port
(CLK : in std_logic; RAM_WEN : in std_logic ; ADDR : in std_logic_vector (6 downto 0); DATA_IN : in std_logic _vector (15 downto 0); DATA_OUT : out std_logic_vector (15 downto 0); IO65_IN : in std_logic_vector (15 downto 0); IO64_OUT : out std_logic_vector (15 downto 0) ); end component;
signal P_COUNT : std_logic_vector (7 downto 0); signal PROM_OUT : std_logic_vector (14 downto 0); signal OP_CODE : std_logic_vector (3 downto 0); signal OP_DATA : std_logic_vector (7 downto 0); signal N_REG_A : std_logic_vector (2 downto 0); signal N_REG_B : std_logic_vector (2 downto 0); signal N_REG_A_DLY : std_logic_vector (2 downto 0); signal REG_IN : std_logic_vector (15 downto 0); signal REG_A : std_logic_vector (15 downto 0); signal REG_B : std_logic_vector (15 downto 0); signal REG_WEN : std_logic; signal REG_0 : std_logic_vector (15 downto 0); signal REG_1 : std_logic_vector (15 downto 0); signal REG_2 : std_logic_vector (15 downto 0); signal REG_3 : std_logic_vector (15 downto 0); signal REG_4 : std_logic_vector (15 downto 0); signal REG_5 : std_logic_vector (15 downto 0); signal REG_6 : std_logic_vector (15 downto 0); signal REG_7 : std_logic_vector (15 downto 0); signal RAM_IN : std_logic_vector (15 downto 0); signal RAM_ADDR : std_logic_vector (7 downto 0); signal RAM_OUT : std_logic_vector (15 downto 0); signal RAM_WEN : std_logic ;
begin C1 : prom2 port map ( P_COUNT (4 downto 0), not CLK, CLK, PROM_OUT); C2 : decode port map ( CLK , PROM_OUT, OP_CODE, OP_DATA); C3 : reg_dc port map ( CLK , REG_0, REG_1, REG_2, REG_3, REG_4, REG_5, REG_6, REG_7, PROM_OUT(10 downto 8), N_REG_A, REG_A); C4 : reg_dc port map ( CLK , REG_0, REG_1, REG_2, REG_3, REG_4, REG_5, REG_6, REG_7, PROM_OUT(7 downto 5), N_REG_B, REG_B); C5 : exec2 port map ( CLK , RESET, OP_CODE, P_COUNT, REG_A, REG_B, OP_DATA, RAM_OUT, P_COUNT, REG_IN, RAM_IN, REG_WEN, RAM_WEN); C6 : n_reg_dly port map (CLK, N_REG_A, N_REG_A_DLY); C7 : ram_addr_dly port map (CLK, OP_DATA, RAM_ADDR); C8 : reg_wb port map ( CLK , N_REG_A_DLY , REG_IN, REG_WEN, REG_0, REG_1, REG_2, REG_3, REG_4, REG_5, REG_6, REG_7); C9 : ram port map ( CLK , RAM_WEN, RAM_ADDR(6 downto 0) , RAM_IN, RAM_OUT, IO65_IN, IO64_OUT); end RTL;
8. まとめ
このコンテンツでは,パイプライン処理を導入したシンプルなCPUの設計法について解説しました.
誤りや分かり難い点について,率直に指摘していただけると有難いです.
また,不明な点は遠慮なく質問して下さい.