LoginSignup
7
2

More than 3 years have passed since last update.

RISC-V シングルサイクルインオーダースーパースカラCPUの考察

Last updated at Posted at 2021-04-24

ASFRV32IM-super・ASFRV32IM-super2

ASFRV32IM-superはRISC-V RV32IMシングルサイクルインオーダースーパースカラCPUの実装集です。スーパースカラについて2命令発行7種類3命令発行5種類合計12種類のアプローチで実装しています。RISC-V Unpriviledged ISA 20191213 に準拠しています。

ASFRV32IM-super2も同様にRISC-V RV32IMシングルサイクルインオーダースーパースカラCPUの実装集です。2命令発行2種類3命令発行3種類合計5種類のアプローチで実装しています。ほぼRV32IMに準拠していますが、4バイト境界にアラインメントしていないメモリアクセスが失敗します。また、ASFRV32IM-superとは異なり命令メモリとデータメモリを分離するハーバードアーキテクチャでZifence拡張のFENCE.I命令には未対応となっています。

一般的にCPUの学習は、1サイクルですべてを処理するシングルサイクル、複数サイクルで処理するマルチサイクル、マルチサイクルを並行動作させることで性能を上げるパイプライン、命令順を維持したまま可能な範囲で複数の命令を同時実行するインオーダースーパースカラ、処理内容が変わらない範囲で命令順を入れ替えて可能な限り複数の命令を同時実行するアウトオブオーダースーパースカラの順に学びます。ですが、並行動作させるパイプライン化は仕組みが複雑です。CPUの性能を向上させるためには命令レベルの並列性(Instruction Level Parallelism, ILP)が重要で、現代のCPUを学習するためにはスーパースカラへの理解が必要ですが、この順番だとパイプラインの難しさと合わせてスーパースカラの構造を学習することになります。そこでパイプラインではなくシングルサイクルのまま複数命令を同時実行するインオーダースーパースカラ型のCPUを設計実装してみました。

ASFRV32IM-super・ASFRV32IM-super2では、複数命令同時実行を制限する各ハザードに対して投機的実行やMacro Op Fusionなど様々なアプローチでCPUを実装しベンチマークがどのように変化するか示すことで、並行処理のための様々な技法を定量的に考察しています。

CPUの学習書の定番のコンピュータの構成と設計(パタヘネ本)もコンピュータアーキテクチャ 定量的アプローチ(ヘネパタ本)も、自分にとってはスーパースカラの記述がいまいち具体性に欠け不十分に感じられたので、これを補えるような資料になればと思っています。

スーパースカラでのハザード

最初にスーパースカラで複数命令を同時実行する際にどのように並行実行が制限されるか説明します。その上でASFRV32IM-super・ASFRV32IM-super2でどのように制限を緩和しているか説明していきます。

パイプラインと同様に、データハザード、構造ハザード、制御ハザードの3つの原因に分類することができます。パイプラインでのハザードの解説は東邦大学の資料を参考にしてください。

構造ハザード

コンピュータ資源は有限なのでCPUがすべての機能を常に使えるわけではありません。例えば、乗算や除算演算は回路が大きいので4命令同時実行のスーパースカラでも1つしか演算ユニットがない場合がほとんどだと思います。メモリアクセスはメモリ回路の関係で1サイクルに処理できるのは1アドレスに対して読み込みか書き込みのどちらかのみで複数のアドレスへの同時アクセスができないなど通常は同時アクセスに対する制限がつきます。これらの制限があると乗算命令やメモリアクセス命令が連続する場合は1サイクルでの並行実行はできません。

データハザード

データの依存関係によって生じるハザードです。

Read After Write(RAW)

先行命令Aの結果を後続命令Bが使用する場合命令Aと命令Bは同時に実行できません。


x1=2, x2=3として

A: add x3, x1, x2 (x1+x2をx3に入れる)
B: add x4, x1, x3 (x1+x3をx4に入れる) 

BはAの演算x1+x2の結果5を利用して5+1を演算してx4に入れるので、A実行後にしかBを実行できません。RAWはインオーダーでもアウトオブオーダーでも発生する本質的なデータ依存です(真の依存)。

Write after read (WAR)

アウトオブオーダー型スーパースカラでは命令の順番を入れ替えられます。もし先行命令Aが使用するレジスタに後続命令Bが書き込みをするならば命令Aと命令Bの順番を入れ替えてはいけません。


x1=2, x2=3, x3=4として

A: add x3, x1, x2 (x1+x2をx3に入れる)
B: add x1, x1, x3 (x1+x3をx1に入れる) 

正しくはAで2+3=5をx3に入れ、Bで2+5=7をx1に入れるという処理になります。Bを先に実行してAがレジスタx1を読み込むより前にx1への書き込みをしてしまうとx1が2+4=6になり、Aは6+3=9をx3に入れてしまいます。

先行命令のレジスタ読み込みを遅らせる特別な理由があるマイクロアーキテクチャでない限りWARはインオーダースーパースカラでは起こらないと思います。

Write after write (WAW)

先行命令Aが結果を書き込む前に後続命令Bが同じレジスタに書き込みしてしまうとレジスタの値がおかしくなります。


x1=2, x2=3として

A: mul x3, x1, x2 (x1×x2をx3に入れる)
B: add x3, x1, x2 (x1+x2をx3に入れる)

正しくはAでx3が2×3=6になった後Bでx3が2+3=5になります。ただ、通常乗算の方が加算より処理サイクル数が大きいので、AとBを同時に実行した場合にBが先に処理が終わると2+3=5をx3に入れてしまい、その後Aが2×3=6をx3に入れてしまうので、x3が5ではなく6になってしまいます。

ASFRV32IM-superはシングルサイクルで同一サイクルで同じレジスタに書き込みが生じる場合命令順に書き込みするようブロッキング代入で実装することでWAWを回避していますが、WAWはインオーダースーパースカラでも発生するハザードになります。

制御ハザード

分岐命令があると次の命令のアドレスが確定しません。


x1=2, x2=3, x3=4として

A: jal x0, jump1 (jump1に無条件ジャンプ)
B: add x3, x1, x2 (x1+x2をx3に入れる)
...
jump1:
X: add x4, x1, x2

もしAとBを並行実行すると次に命令Xを実行するのはいいのですがBの2+3をx3に入れる命令も実行してしまうので、x3が4ではなく5になってしまいます。

これを制限せず分岐命令直後の命令を実行することを遅延分岐といい、実行される分岐命令後の命令を遅延スロットといいます。古い世代のRISC CPUのMIPSやSPARCは遅延分岐を仕様としていますが、現在の多くのCPUは遅延分岐を採用していませんので、これもスーパースカラの並行実行を制限する原因の一つとなります。

ASFRV32IM-super

以上のハザードに対して様々な手法で対応しているのがASFRV32IM-super・ASFRV32IM-super2になります。

RV32IM-1

リファレンス用のスカラーCPUです。スーパースカラではありません。ASFRV32IMとほぼ同一ですが、デコーダとALUを一つのmoduleとして分離していて、メインのRV32IMから利用する形で実装しています。Coremark/MHzが3.24、DMIPが1.05です。

RV32IM-1.png

RV32IM-2

2命令同時実行のスーパースカラCPUです。

命令ラインは2本で、ライン1のALUは加算シフト+乗算除算剰余、ライン2のALUは加算シフトのみ、メモリユニットはライン1のみ接続です。

乗算除算剰余はライン1のみ実行可能で、ライン2に来た場合はライン1の命令のみ実行、同様にメモリ命令はライン1のみ実行可能で、ライン2に来た場合はライン1の命令のみ実行することで構造ハザードに対応しています。

ライン1の命令が書き込むレジスタをライン2が使用するときはライン1の命令のみ実行することでRAWハザードに対応しています。

分岐命令がライン1に来た場合無条件分岐JAL・JARL条件分岐B命令どれでもライン1の命令のみ実行することで制御ハザードに対応しています。

338-354行目でこれらスーパースカラで実行できるか判定し、2命令同時実行時に信号線superscalarを1にしています。

  assign isBranch1 = (op1 == SBFORMAT) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == ECALLEBREAK); 
  assign isMemOp2 = (op2 == SFORMAT) || (op2 == IFORMAT_LOAD);
  assign isMULDIV2 = (op2 == MULDIV) && (tmpopcode2[25] == 1'b1);
  assign isRegD = (regd != 5'b0) &&
                  ((op1 == RFORMAT) || (op1 == IFORMAT_ALU) || (op1 == IFORMAT_LOAD) || (op1 == UFORMAT_LUI) || 
                   (op1 == UFORMAT_AUIPC) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == MULDIV));
  assign isReg1 = (reg1 != 5'b0) &&
                  ((op2 == RFORMAT) || (op2 == IFORMAT_ALU) || (op2 == IFORMAT_LOAD) || (op2 == SFORMAT) || 
                   (op2 == SBFORMAT) || (op2 == IFORMAT_JALR) || (op2 == MULDIV));
  assign isReg2 = (reg2 != 5'b0) &&
                  ((op2 == RFORMAT) || (op2 == SFORMAT) || (op2 == SBFORMAT) || (op2 == MULDIV));

  wire superscalar;
  assign superscalar = ((isBranch1 != 1'b1) && (isMemOp2 != 1'b1) && (isMULDIV2 != 1'b1) &&
                        ((isRegD != 1'b1) || 
                         (((isReg1 != 1'b1) || (regd != reg1)) &&
                          ((isReg2 != 1'b1) || (regd != reg2))))) ? 1'b1 : 1'b0;

Coremark/MHzが4.88でRV32IM-1より51%性能向上、DMIPが1.565でRV32IM-1より49%性能向上しています。

RV32IM-2.png

RV32IM-2mul

2命令同時実行のスーパースカラCPUです。

構造ハザードを改善するために乗算除算剰余演算器を2つにしてみました。ライン1ライン2両方のALUが加算シフト+乗算除算剰余です。メモリユニットはライン1のみ接続です。これ以外はRV32IM-2と同じです。

Coremark/MHzが5.04でRV32IM-1より56%向上、RV32IM-2より3.28%向上、DMIPが1.565でRV32IM-1より49%向上でRV32IM-2から向上していません。Coremarkのみ性能に反映されるようです。

RV32IM-2とRV32IM-2mulのdiffの抜粋

@@ -155,135 +155,6 @@
   assign pc_sel2 = BRANCH_EXEC(funct3, r_data1, r_data2, pc_sel);
 endmodule 

-module DECODE_EXEC_NOMULDIV(input wire [31:0] opcode, input wire [31:0] pc,
(略)
-endmodule
-
 module RV32IM(input wire clock, input wire reset_n, output wire [31:0] pc_out, output wire [63:0] op_out, output wire [63:0] alu_out, output wire [8:0] uart_out);
   // REGISTER
   reg [31:0] pc;
@@ -331,13 +202,11 @@

   wire isBranch1; 
   wire isMemOp2; // MEMORY OPERATION ONLY on dec_exec1
-  wire isMULDIV2; // MULDIV ONLY on dec_exec1 
   wire isRegD;
   wire isReg1;
   wire isReg2;
   assign isBranch1 = (op1 == SBFORMAT) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == ECALLEBREAK); 
   assign isMemOp2 = (op2 == SFORMAT) || (op2 == IFORMAT_LOAD);
-  assign isMULDIV2 = (op2 == MULDIV) && (tmpopcode2[25] == 1'b1);
   assign isRegD = (regd != 5'b0) &&
                   ((op1 == RFORMAT) || (op1 == IFORMAT_ALU) || (op1 == IFORMAT_LOAD) || (op1 == UFORMAT_LUI) || 
                    (op1 == UFORMAT_AUIPC) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == MULDIV));
@@ -348,7 +217,7 @@
                   ((op2 == RFORMAT) || (op2 == SFORMAT) || (op2 == SBFORMAT) || (op2 == MULDIV));

   wire superscalar;
-  assign superscalar = ((isBranch1 != 1'b1) && (isMemOp2 != 1'b1) && (isMULDIV2 != 1'b1) &&
+  assign superscalar = ((isBranch1 != 1'b1) && (isMemOp2 != 1'b1) &&
                         ((isRegD != 1'b1) || 
                          (((isReg1 != 1'b1) || (regd != reg1)) &&
                           ((isReg2 != 1'b1) || (regd != reg2))))) ? 1'b1 : 1'b0;
@@ -386,7 +255,7 @@
   assign r_data1_2 = (r_addr1_2 == 5'b00000) ? 32'b0 : regs[r_addr1_2]; 
   assign r_data2_2 = (r_addr2_2 == 5'b00000) ? 32'b0 : regs[r_addr2_2]; 

-  DECODE_EXEC_NOMULDIV dec_exe2(opcode2, pc + 4, r_addr1_2, r_addr2_2, w_addr_2, r_data1_2, r_data2_2, alu_data_2, mem_val_2, mem_rw_2, rf_wen_2, wb_sel_2, pc_sel2_2);
+  DECODE_EXEC dec_exe2(opcode2, pc + 4, r_addr1_2, r_addr2_2, w_addr_2, r_data1_2, r_data2_2, alu_data_2, mem_val_2, mem_rw_2, rf_wen_2, wb_sel_2, pc_sel2_2);

   assign alu_out = {alu_data_2, alu_data}; // for DEBUG


DECODE_EXEC_NOMULDIVを削除してDECODE_EXEC2つにし、isMULDIV2で乗算除算命令を制限している箇所の削除しています。

RV32IM-2mul.png

RV32IM-2mem

2命令同時実行のスーパースカラCPUです。

構造ハザードを改善するためにメモリアクセスユニットを増加させました。

ライン1のALUは加算シフト+乗算除算剰余、ライン2のALUは加算シフトのみですが、メモリユニットはライン1とライン2の両方にあり、同時に2アドレス読み書きができるようにしています。ただしライン1の命令がSTOREするのと同じアドレスにライン2の命令がLOADする場合はライン2のLOADが古い値になってしまうのでライン1の命令のみ実行するように制限しています。それ以外はRV32IM-2と同じです。

Coremark/MHzが5.13でRV32IM-1より58%向上、RV32IM-2より5.12%向上、DMIPが1.66でRV32IM-1より58%向上でRV32IM-2より6%向上です。メモリアクセスを並行実行できると性能が向上するようです。

RV32IM-2とRV32IM-2memのdiff

@@ -330,13 +330,13 @@
   assign reg2 = tmpopcode2[24:20];

   wire isBranch1; 
-  wire isMemOp2; // MEMORY OPERATION ONLY on dec_exec1
+  wire isMemOp2; // MEMORY OPERATION RESTRICTED on dec_exec2
   wire isMULDIV2; // MULDIV ONLY on dec_exec1 
   wire isRegD;
   wire isReg1;
   wire isReg2;
   assign isBranch1 = (op1 == SBFORMAT) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == ECALLEBREAK); 
-  assign isMemOp2 = (op2 == SFORMAT) || (op2 == IFORMAT_LOAD);
+  assign isMemOp2 = (op1 == SFORMAT) && (op2 == IFORMAT_LOAD); // op1 MEMORY STORE & op2 MEMORY LOAD DEPENDANCY
   assign isMULDIV2 = (op2 == MULDIV) && (tmpopcode2[25] == 1'b1);
   assign isRegD = (regd != 5'b0) &&
                   ((op1 == RFORMAT) || (op1 == IFORMAT_ALU) || (op1 == IFORMAT_LOAD) || (op1 == UFORMAT_LUI) || 
@@ -390,12 +390,12 @@

   assign alu_out = {alu_data_2, alu_data}; // for DEBUG

-  // MEMORY 
+  // MEMORY 1
   wire [31:0] mem_data;
   wire [31:0] mem_addr;
   assign mem_addr = alu_data;

-  // MEMORY READ
+  // MEMORY READ 1
   assign mem_data = (mem_rw == 1'b1) ? 32'b0 : // when MEMORY WRITE, the output from MEMORY is 32'b0
            ((mem_val == 3'b010) && (mem_addr == COUNTER_MMIO_ADDR)) ? counter : // MEMORY MAPPED IO for CLOCK CYCLE COUNTER
            ((mem_val[1:0] == 2'b00) && (mem_addr == UART_MMIO_FLAG)) ? 8'b1 : // MEMORY MAPPED IO for UART FLAG(always enabled(8'b1))
@@ -405,6 +405,23 @@
                      (mem_val == 3'b100) ? {24'h000000, mem[mem_addr]} : // LBU
                      (mem_val == 3'b101) ? {16'h0000, mem[mem_addr + 1], mem[mem_addr]} : // LHU
                                            32'b0;
+
+  // MEMORY 2
+  wire [31:0] mem_data_2;
+  wire [31:0] mem_addr_2;
+  assign mem_addr_2 = alu_data_2;
+  
+  // MEMORY READ 2
+  assign mem_data_2 = (mem_rw_2 == 1'b1) ? 32'b0 : // when MEMORY WRITE, the output from MEMORY is 32'b0
+             ((mem_val_2 == 3'b010) && (mem_addr_2 == COUNTER_MMIO_ADDR)) ? counter : // MEMORY MAPPED IO for CLOCK CYCLE COUNTER
+             ((mem_val_2[1:0] == 2'b00) && (mem_addr_2 == UART_MMIO_FLAG)) ? 8'b1 : // MEMORY MAPPED IO for UART FLAG(always enabled(8'b1))
+                      (mem_val_2 == 3'b000) ?  (mem[mem_addr_2][7] == 1'b1 ? {24'hffffff, mem[mem_addr_2]} : {24'h000000, mem[mem_addr_2]}) : // LB
+                      (mem_val_2 == 3'b001) ?  (mem[mem_addr_2 + 1][7] == 1'b1 ? {16'hffff, mem[mem_addr_2 + 1], mem[mem_addr_2]} : {16'h0000, mem[mem_addr_2 + 1], mem[mem_addr_2]}) : // LH
+                      (mem_val_2 == 3'b010) ? {mem[mem_addr_2 + 3], mem[mem_addr_2 + 2], mem[mem_addr_2 + 1], mem[mem_addr_2]} : // LW
+                      (mem_val_2 == 3'b100) ? {24'h000000, mem[mem_addr_2]} : // LBU
+                      (mem_val_2 == 3'b101) ? {16'h0000, mem[mem_addr_2 + 1], mem[mem_addr_2]} : // LHU
+                                           32'b0;
+                                           
   // MEMORY WRITE
   // intentionally blocking statement
   always @(posedge clock) begin
@@ -418,9 +435,21 @@
           {mem[mem_addr + 3], mem[mem_addr + 2], mem[mem_addr + 1], mem[mem_addr]} = r_data2;
         default: begin end // ILLEGAL
       endcase
+    if (mem_rw_2 == 1'b1)
+      case (mem_val_2)
+        3'b000: // SB
+          mem[mem_addr_2] = r_data2_2[7:0];
+        3'b001: // SH
+          {mem[mem_addr_2 + 1], mem[mem_addr_2]} = r_data2_2[15:0];
+        3'b010: // SW
+          {mem[mem_addr_2 + 3], mem[mem_addr_2 + 2], mem[mem_addr_2 + 1], mem[mem_addr_2]} = r_data2_2;
+        default: begin end // ILLEGAL
+      endcase
     // MEMORY MAPPED IO to UART
     if ((mem_rw == 1'b1) && (mem_addr == UART_MMIO_ADDR))
       uart = {1'b1, r_data2[7:0]};
+    else if ((mem_rw_2 == 1'b1) && (mem_addr_2 == UART_MMIO_ADDR))
+      uart = {1'b1, r_data2_2[7:0]}; 
     else
       uart = 9'b0;
   end
@@ -432,7 +461,8 @@
                     (wb_sel   == 2'b01) ? mem_data :
                     (wb_sel   == 2'b10) ? pc + 4 : 32'b0; // ILLEGAL
   wire [31:0] w_data_2;
-  assign w_data_2 = (wb_sel_2 == 2'b00) ? alu_data_2 : 
+  assign w_data_2 = (wb_sel_2 == 2'b00) ? alu_data_2 :
+                    (wb_sel_2 == 2'b01) ? mem_data_2 :
                     (wb_sel_2 == 2'b10) ? pc + 8 : 32'b0; // ILLEGAL

   always @(posedge clock) begin

メモリへの読み書き回路が増えています。

RV32IM-2mem.png

RV32IM-2mem2

2命令同時実行のスーパースカラCPUです。

RV32IM-2memで制限した先行STOREと後続LOADが同じアドレスへアクセスする場合に、同じデータ幅(sbとlb、shとlh、swとlwの場合)ならフォワーディングすることで同時実行できるように修正したものです。

Coremark/MHzが5.14でRV32IM-1より59%向上、RV32IM-2より5.32%向上、RV32IM-2memより0.19%向上、DMIPが1.67でRV32IM-1より59%向上でRV32IM-2より7.05%向上、RV32IM-2memより0.6%向上です。やはりメモリアクセスをより多く並行実行できると性能がより向上するようです。

RV32IM-2memとRV32IM-2mem2のdiff

@@ -336,7 +336,7 @@
   wire isReg1;
   wire isReg2;
   assign isBranch1 = (op1 == SBFORMAT) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == ECALLEBREAK); 
-  assign isMemOp2 = (op1 == SFORMAT) && (op2 == IFORMAT_LOAD); // op1 MEMORY STORE & op2 MEMORY LOAD DEPENDANCY
+  assign isMemOp2 = ((op1 == SFORMAT) && (op2 == IFORMAT_LOAD) && (tmpopcode1[14:12] != tmpopcode2[14:12])); // op1 MEMORY STORE & op2 MEMORY LOAD DEPENDANCY except sw & lw
   assign isMULDIV2 = (op2 == MULDIV) && (tmpopcode2[25] == 1'b1);
   assign isRegD = (regd != 5'b0) &&
                   ((op1 == RFORMAT) || (op1 == IFORMAT_ALU) || (op1 == IFORMAT_LOAD) || (op1 == UFORMAT_LUI) || 
@@ -393,7 +393,9 @@
   // MEMORY 1
   wire [31:0] mem_data;
   wire [31:0] mem_addr;
-  assign mem_addr = alu_data;
+  assign mem_addr = (mem_val == 3'b010) ? {alu_data[31:2], 2'b00} : 
+                    (mem_val[1:0] == 2'b01) ? {alu_data[31:1], 1'b0} :
+                    alu_data;

   // MEMORY READ 1
   assign mem_data = (mem_rw == 1'b1) ? 32'b0 : // when MEMORY WRITE, the output from MEMORY is 32'b0
@@ -409,7 +411,9 @@
   // MEMORY 2
   wire [31:0] mem_data_2;
   wire [31:0] mem_addr_2;
-  assign mem_addr_2 = alu_data_2;
+  assign mem_addr_2 = (mem_val_2 == 3'b010) ? {alu_data_2[31:2], 2'b00} : 
+                      (mem_val_2[1:0] == 2'b01) ? {alu_data_2[31:1], 1'b0} :
+                      alu_data_2;

   // MEMORY READ 2
   assign mem_data_2 = (mem_rw_2 == 1'b1) ? 32'b0 : // when MEMORY WRITE, the output from MEMORY is 32'b0
@@ -421,6 +425,14 @@
                       (mem_val_2 == 3'b100) ? {24'h000000, mem[mem_addr_2]} : // LBU
                       (mem_val_2 == 3'b101) ? {16'h0000, mem[mem_addr_2 + 1], mem[mem_addr_2]} : // LHU
                                            32'b0;
+  wire [31:0] mem_buf;
+  assign mem_buf = ((opcode1[6:0] == SFORMAT) && (opcode2[6:0] == IFORMAT_LOAD) && (mem_val == mem_val_2) && (mem_addr == mem_addr_2)) ? 
+                   (mem_val_2 == 3'b010) ? r_data2 :
+                   (mem_val_2 == 3'b000) ? {{24{r_data2[7]}}, r_data2[7:0]} :
+                   (mem_val_2 == 3'b100) ? {24'h00000, r_data2[7:0]} :
+                   (mem_val_2 == 3'b001) ? {{16{r_data2[15]}}, r_data2[15:0]} :
+                   (mem_val_2 == 3'b101) ? {16'h0000, r_data2[15:0]} :
+                   mem_data_2 : mem_data_2;

   // MEMORY WRITE
   // intentionally blocking statement
@@ -462,7 +474,7 @@
                     (wb_sel   == 2'b10) ? pc + 4 : 32'b0; // ILLEGAL
   wire [31:0] w_data_2;
   assign w_data_2 = (wb_sel_2 == 2'b00) ? alu_data_2 :
-                    (wb_sel_2 == 2'b01) ? mem_data_2 :
+                    (wb_sel_2 == 2'b01) ? mem_buf :
                     (wb_sel_2 == 2'b10) ? pc + 8 : 32'b0; // ILLEGAL

   always @(posedge clock) begin

メモリアクセスによるスーパスカラへの制限の緩和とフォワーディング回路(mem_buf)を実装しています。

RV32IM-2mem2.png

RV32IM-2bp

2命令同時実行のスーパースカラCPUです。

制御ハザードの改善のために、先行命令が条件分岐命令の場合に、後続命令を制限するのではなく後続命令を実行し、先行命令の条件未成立時はそのまま結果を書き込み、先行命令条件分岐成立時には結果を捨てる投機的実行を実装しています。これ以外はRV32IM-2と同じです。

Coremark/MHzが5.00でRV32IM-1より54%向上、RV32IM-2より2.46%向上、DMIPが1.57でRV32IM-1より50%向上でRV32IM-2から0.64%向上です。

RV32IM-2とRV32IM-2bpのdiffの抜粋

@@ -330,12 +330,14 @@
   assign reg2 = tmpopcode2[24:20];

   wire isBranch1; 
+  wire isBP1; 
   wire isMemOp2; // MEMORY OPERATION ONLY on dec_exec1
   wire isMULDIV2; // MULDIV ONLY on dec_exec1 
   wire isRegD;
   wire isReg1;
   wire isReg2;
-  assign isBranch1 = (op1 == SBFORMAT) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == ECALLEBREAK); 
+  assign isBranch1 = (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == ECALLEBREAK); 
+  assign isBP1 = (op1 == SBFORMAT);
   assign isMemOp2 = (op2 == SFORMAT) || (op2 == IFORMAT_LOAD);
   assign isMULDIV2 = (op2 == MULDIV) && (tmpopcode2[25] == 1'b1);
   assign isRegD = (regd != 5'b0) &&
@@ -386,7 +388,7 @@
   assign r_data1_2 = (r_addr1_2 == 5'b00000) ? 32'b0 : regs[r_addr1_2]; 
   assign r_data2_2 = (r_addr2_2 == 5'b00000) ? 32'b0 : regs[r_addr2_2]; 

-  DECODE_EXEC_NOMULDIV dec_exe2(opcode2, pc + 4, r_addr1_2, r_addr2_2, w_addr_2, r_data1_2, r_data2_2, alu_data_2, mem_val_2, mem_rw_2, rf_wen_2, wb_sel_2, pc_sel2_2);
+  DECODE_EXEC dec_exe2(opcode2, pc + 4, r_addr1_2, r_addr2_2, w_addr_2, r_data1_2, r_data2_2, alu_data_2, mem_val_2, mem_rw_2, rf_wen_2, wb_sel_2, pc_sel2_2);

   assign alu_out = {alu_data_2, alu_data}; // for DEBUG

@@ -438,13 +440,15 @@
   always @(posedge clock) begin
     if ((rf_wen   == 1'b1) && (w_addr   != 5'b00000))
       regs[w_addr  ] = w_data;
-    if ((rf_wen_2 == 1'b1) && (w_addr_2 != 5'b00000))
+    if ((rf_wen_2 == 1'b1) && (w_addr_2 != 5'b00000) && 
+        !((isBP1 == 1'b1) && (pc_sel2 == 1'b1))) // if not BRANCH
       regs[w_addr_2] = w_data_2;
   end

   // NEXT PC
   wire [31:0] next_pc;
-  assign next_pc = (superscalar == 1'b1) ? (pc_sel2_2 == 1'b1) ? {alu_data_2[31:1], 1'b0} : pc + 8 :
+  assign next_pc = (superscalar == 1'b1) ? ((isBP1 == 1'b1) && (pc_sel2 == 1'b1)) ? {alu_data  [31:1], 1'b0} : // BRANCH Prediction failed
+                                           (pc_sel2_2 == 1'b1) ? {alu_data_2[31:1], 1'b0} : pc + 8 :
                                   (pc_sel2   == 1'b1) ? {alu_data  [31:1], 1'b0} : pc + 4; 

   // NEXT PC WRITE BACK and CYCLE COUNTER

十数行程度の修正で実装できます。先行命令の条件成立信号線pc_sel2を使って後続命令のALUの結果w_data_2を書き込むかどうか判定し(後続命令はメモリアクセスできない制限があるのでALUの結果を利用する命令しか実行されない)、次のPCも同様にpc_sel2を使って、分岐未成立時は後続命令のPC+4を、分岐成立時は分岐先アドレスである先行命令のALU出力を次のPCとします。

RV32IM-2bp.png

RV32IM-2mof

2命令同時実行のスーパースカラCPUです。

先行命令の演算結果を後続命令が使用する場合(RAW)を改善するために、複数の命令を1つの命令に変換して実行するMacro Op Fusionを実装しました。RISC-V Macro-Op fusionの論文を参考にslli+add, add+load, lui+addi,lui+load, auipc+loadの5つの融合命令を実装しています。

命令先行命令用のDECODE_EXECを拡張し、PREDECODEが先行命令後続命令両方を見て融合可能だった場合に、融合命令を意味する7'b0001011に先行命令のopcodeのopcode[6:0]を置換え、通常の32bitのopcodeに加え18bitのopcode2を合わせてDECODE_EXECに送ることで、これら融合命令を実行できるようにしています。

Coremark/MHzが4.88でRV32IM-1より51%向上、RV32IM-2から変化せずCoremarkの実行時間も同じ、DMIPが1.56でRV32IM-1より50%向上でRV32IM-2から変化せずDhystone値は2739から2747なのでわずかに向上しています。2命令同時実行のインオーダースーパースカラでは実装した5つの融合命令によるMacro Op Fusionはほとんど性能に影響しないようです。

Load Effective Address (LEA)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| 0000000shamt | rs1  |funct3 |  rd     |0001011| +|000  |000000rs2   |010|
先行命令 slli rd, rs1, {1,2}
後続命令 add rd, rd, rs2
融合命令 lea rd, rs1, rs2, {1,2}

rs1を1bitまたは2bitシフトしrs2を加えた値をrdに入れます。
rd <= ((rs << 1) or (rs1 << 2) + rs2

この命令の組み合わせは配列にアクセスする際のアドレス計算で使われるものです。

8bit配列の場合
char array[1024];
int offset;
offset = 3;

d = array[offset];

8bit単位のchar配列arrayのoffset番目の要素にアクセスする場合、アドレス&(array[offset])は
arrayの(先頭)アドレス+offsetです。arrayのアドレスが1000でoffsetが3なら&(array[offset])は1000+3=1003です。つまりarrayのアドレスをrs2、offsetをrs1に入れ求めるアドレスをrdに入れるならば

add rd, rs1, rs2

で計算できます。

16bit配列の場合
short array[1024];
int offset;
offset = 3;

d = array[offset];

16bit単位のshort 配列arrayのoffset番目の要素にアクセスする場合、アドレス&(array[offset])は
arrayの(先頭)アドレス+offset << 1 (またはoffset * 2) です。arrayのアドレスが1000でoffsetが3なら&(array[offset])は1000+(3<<1)=1000+6=1006です。
rrayのアドレスをrs2、offsetをrs1に入れ求めるアドレスをrdに入れるならば

slli rd, rs1, 1
add rd, rd, rs2

で計算できます。

この組み合わせは先行命令の演算結果rdを後続命令で利用しているため、RAWによりそのままでは同時実行できません。これをMacro Op Fusionで1つの融合命令として処理することで並行実行できるようになります。

32bit配列の場合

同様に32bit単位のlong配列ならばoffset<<2が必要です。

long array[1024];
int offset;
offset = 3;

d = array[offset];
slli rd, rs1, 2
add rd, rd, rs2

64bit単位のlong long配列ならばoffset<<3が必要です。

ASFRV32IMは32bit CPUで32bit単位でのメモリアクセスなのでoffset<<1とoffset<<2のみを実装しています。

実装

一般のシフト演算はそれなりの回路が必要です。なので必要な1bit左シフトと2bit左シフトだけALUへのセレクタを拡張して処理できるようにしました。

レジスタファイルからALUへの入力1を、r_data1とpcに加え、r_data1を1bit左シフトしたものとr_data1を2bit左シフトしたものを追加しています。

RV32IM-2のセレクタを

  // SELECTOR  
  wire [31:0] s_data1, s_data2;
  assign s_data1 = (op1sel == 1'b1) ? pc : r_data1;

RV32IM-2mofではこのように変更しています。

  assign s_data1 = (op1sel == 2'b11) ? (r_data1 << 2) : (op1sel == 2'b10) ? (r_d
ata1 << 1) : // LEA
                   (op1sel == 2'b01) ? pc : r_data1;

この回路を利用してLEA融合命令を実装しています。

Indexed Load

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| 0x00000| rs2 | rs1  |funct3 |  rd     |0001011| +|memop|000000000000|011|
先行命令 add rd, rs1, rs2
後続命令 lb rd, 0(rd) (lb以外のlbu, lh, lhu, lwも可)
融合命令 ilb rd, rs1, rs2

rs1+rs2のアドレスから値を読み込みをrdに書き込みます。

この命令の組み合わせも基本的には配列関係です。

char array[1024];
int offset;
offset = 3;

d = array[offset];

8bit単位のchar配列arrayのoffset番目の要素にアクセスする場合、アドレスはarrayのアドレス+offsetです。arrayのアドレスが1000でoffsetが3なら&(array[offset])は1000+3=1003で、1003番地の内容を読み込めばいいのです。arrayのアドレスをrs2、offsetをrs1に入れ、求める値をメモリからロードしてrdに入れるならば

add rd, rs1, rs2
lb rd, 0(rd)

となります。

この組み合わせは先行命令の演算結果rdを後続命令lbで利用しているため、RAWによりそのままでは同時実行できません。これをMacro Op Fusionで1つの融合命令として処理することで並行実行できるようになります。

この命令の実装に追加回路は不要です。通常のLOAD命令はI-FORMATでレジスタ+即値でALUを使ってアドレスを計算しますが、これをR-FORMATのadd命令の用にレジスタ+レジスタでALUを使ってアドレス計算するようにするだけで実装できます。

さらに3命令同時実行のスーパースカラならば、Load Effective AddressとIndex Loadを組み合わせて3命令融合するFused Indexed Loadも可能ですが、ASFRV32IM-superでは実装していません。ASFRV32IM-super2の方で実装しています(後述)。

Load Immediate 1

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[31:12]                   |  rd    |0001011| +|000  |imm[11:0]   |100|
先行命令 lui rd, imm[31:12]
後続命令 addi rd, rd, imm[11:0]
融合命令 lii1 rd, rs1, 符号拡張imm[31:12]+符号拡張imm[11:0] (imm[31:0]ではない)

rdに32bitの即値を入れる命令です。擬似命令liに対応する融合命令です。ただし、imm[31:0]をそのままrdにいれるわけではありません。この命令について元の論文にはミスがあります。

RISC-Vの即値は符号拡張なので、12bitの+1は12'b0000_0000_0001ですが、-1は12'b1111_1111_1111です。したがって、x1に+1を代入するときは

addi x1, x0, 1

とすると、x1は32'b0000_0000_0000_0000_0000_0000_0000_0001になりますが、x1に+1を代入するときは、

addi x1, x0, -1

とするとx1は32'b1111_1111_1111_1111_1111_1111_1111_1111になります。

なので、x1に32'h0000_1001を入れたいときは

li x1, 0x00001001
lui x1, x0, 1
addi x1, x1, 1

とすれば、x1が32'h0000_1000となり
32'h0000_1000+32'h0000_0001で32'h0000_1001となります。

luiのimm[31:12] 20'h00001とaddiのimm[11:0] 12'h001をそのまま融合してx1に32'h0000_1001を入れればOKです。

が、x1に32'h0000_1fffを入れたいときは

li x1, 0x00001fff

lui x1, x0, 2
addi x1, x1, -1

とする必要があります。32'h0000_2000+32'hffff_ffff=33'h1_0000_1fff=32'h0000_1ffffだからです。これ以外に2命令でx1に32'h0000_1fffを入れることはできません。

つまり、luiのimm[31:12] 20'h00002とaddiのimm[11:0] 12'hfffをそのまま融合した32'h0000_2fffではないのです。imm[31:0]をx1に入れる実装にしてしまうと正しい演算結果になりません。

おそらく元のMacro Op Fusion論文時にはriscv-testがなかったからだと思いますが、lui +addiのimm[31:0]をレジスタにそのまま入れる実装をするとriscv-testでエラーが出るのですぐに気づきます。

lii1と次のlii2はALUのみでの実装が可能なのですが、次の次のauipc+addiの実装にはマイクロアーキテクチャの変更が必要です。この関係でPREDECODEが符号拡張imm[31:12]+符号拡張imm[11:0]を演算してimmとしてDECODE_EXECを送る実装しています。445-446行目のこの部分になります。

  wire [31:0] mop_imm;
  assign mop_imm = {tmpopcode1[31:12], 12'b0} + {{20{tmpopcode2[31]}}, tmpopcode2[31:20]};

好ましい実装ではないのでASFRV32IM-super2ではauipc+addiを実装せずALUのみで実装できるlui+addiのみ実装しています。

Load Immediate 2

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[31:12]                   |  rd    |0001011| +|memop|imm[11:0]   |101|
先行命令 lui rd, imm[31:12]
後続命令 lb rd, rd, imm[11:0] (lbuやlh、lhu、lwも可)
融合命令 lii2 rd, rs1, 符号拡張imm[31:12]+符号拡張imm[11:0] (imm[31:0]ではない)

32bit即値アドレス指定のメモリ読み込みです。符号拡張imm[31:12]+符号拡張imm[11:0]のアドレスのメモリを読み込みrdに入れます。

Load Immediate 1と同様にPREDECODEが符号拡張imm[31:12]+符号拡張imm[11:0]を演算してimmとしてDECODE_EXECを送りそのアドレスをLOADするように実装しています。

Load Global

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[31:12]                   |  rd    |0001011| +|memop|imm[11:0]   |110|
先行命令 auipc rd, imm[31:12]lui rd, imm[31:12]
後続命令 lb rd, rd, imm[11:0] (lbuやlh、lhu、lwも可)
融合命令 lgb rd, rs1, pc+符号拡張imm[31:12]+符号拡張imm[11:0] (imm[31:0]ではない)

PC相対32bit即値アドレス指定のメモリ読み込み命令です。pc+符号拡張imm[31:12]+符号拡張imm[11:0]のアドレスのメモリを読み込みrdに入れます。

3つのデータの加算なのでそのままのマイクロアーキテクチャでは実装できません。Load Immediate 1と同様にPREDECODEが符号拡張imm[31:12]+符号拡張imm[11:0]を演算してimmとしてDECODE_EXECを送り、ALUでpc+immを演算して、その演算結果のアドレスをLOADするようにしています。

Macro Op Fusionのテスト

riscv-testのみでは十分なテストができないので、これらslli+add, add+load, lui+addi, lui+load, auipc+loadの5つの融合命令をテストできるmoptest.Sを用意してあります。手動でテストする必要がありますが、opcode2が0でない場合は融合命令を実行しているので検証できます。

moptest.Sのdumpの一部

   0:   01000093            li  ra,16
   4:   00500113            li  sp,5
   8:   003232b7            lui t0,0x323
   c:   a0a28293            addi    t0,t0,-1526 # 322a0a <_end+0x32290a>
  10:   30a00193            li  gp,778
  14:   03a00213            li  tp,58

RV32IM-2のmoptest.Sの実行結果の一部

                   0: PC = 00000000, OPCODE = 0050011301000093, ALU_DATA = 0000000500000010, UART = 00
                   2: PC = 00000008, OPCODE = 00000000003232b7, ALU_DATA = 0000000000323000, UART = 00
                   4: PC = 0000000c, OPCODE = 30a00193a0a28293, ALU_DATA = 0000030a00322a0a, UART = 00
                   5: PC = 0000000c, OPCODE = 30a00193a0a28293, ALU_DATA = 0000030a00322414, UART = 00
                   6: PC = 00000014, OPCODE = 0030033703a00213, ALU_DATA = 003000000000003a, UART = 00

0番地のli ra,16と4番地のli sp,5は同時実行、次に8番地のlui t0,0x323のみ実行、次にc番地のaddi t0,t0,-1526と10番地のli gp,778を実行、そのあと14番地と18番地を実行しています。

RV32IM-2mofのmoptest.Sの実行結果の一部

                   0: PC = 00000000, OPCODE = 0050011301000093, OPCODE2 = 00000, ALU_DATA = 0000000500000010, UART = 00
                   2: PC = 00000008, OPCODE = 000000000032228b, OPCODE2 = 05054, ALU_DATA = 0000000000322a0a, UART = 00
                   4: PC = 00000010, OPCODE = 03a0021330a00193, OPCODE2 = 00000, ALU_DATA = 0000003a0000030a, UART = 00

0番地のli ra,16と4番地のli sp,5は同時実行、次の8番地のlui t0,0x323とc番地のaddi t0,t0,-1526を融合命令lii1として同時実行,次に10番地と14番地を実行していています。RV32IM-2では8番地とc番地はRAWにより同時実行できませんでしたが、RV32IM-2mofではMacro Op Fusionにより同時実行できるようになっています。

RV32IM-2から大幅に変更されているのでdiffは貼りません。

RV32IM-2mof.png

RV32IM-2all

2命令同時実行のスーパースカラCPUです。

RV32IM-2mul、RV32IM-2mem2、RV32IM-2bp、RV32IM-2mofの要素をすべて取り入れて、データハザードに対してMacro Op Fusionで対応し、構造ハザードに対して乗算除算剰余回路とメモリアクセス回路を2つ実装することで対応し、制御ハザードに対して投機的実行で対応したものです。

Coremark/MHzが5.47でRV32IM-1より69%向上、RV32IM-2より12.1%向上、DMIPが1.82でRV32IM-1より73%向上でRV32IM-2より16.7%向上しています。シミュレータ上ではなく実機でそのまま実装するのは難しい構造になっていますが、2命令同時実行のインオーダースーパースカラでも命令レベルの並列度を大幅に向上させることができるのは興味深いです。

RV32IM-2all.png

RV32IM-3

3命令同時実行のスーパースカラCPUです。

命令ラインは3本で、ライン1のALUは加算シフト+乗算除算剰余、ライン2とライン3のALUは加算シフトのみ、メモリユニットはライン1のみ接続です。

乗算除算剰余はライン1のみ実行可能で、ライン2に来た場合はライン1の命令のみ実行、ライン3に来た場合はライン1とライン2の命令を実行で、同様にメモリ命令はライン1のみ実行可能で、ライン2に来た場合はライン1の命令のみ実行、ライン3に来た場合はライン1とライン2の命令を実行することで構造ハザードに対応しています。

ライン1の命令が書き込むレジスタをライン2が使用するときはライン1の命令のみ実行、ライン1の命令が書き込むレジスタをライン3が使用するときはライン1とライン2の命令を実行、ライン2の命令が書き込むレジスタをライン3が使用するときはライン1とライン2の命令を実行することでRAWハザードに対応しています。
分岐命令がライン1に来た場合無条件分岐JAL・JARL条件分岐B命令どれでもライン1の命令のみ実行、分岐命令がライン2に来た場合無条件分岐JAL・JARL条件分岐B命令どれでもライン1とライン2の命令を実行することで制御ハザードに対応しています。

353-387行目

  assign isBranch1 = (op1 == SBFORMAT) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == ECALLEBREAK); 
  assign isBranch2 = (op2 == SBFORMAT) || (op2 == UJFORMAT) || (op2 == IFORMAT_JALR) || (op2 == ECALLEBREAK); 
  assign isMemOp2 = (op2 == SFORMAT) || (op2 == IFORMAT_LOAD);
  assign isMemOp3 = (op3 == SFORMAT) || (op3 == IFORMAT_LOAD);
  assign isMULDIV2 = (op2 == MULDIV) && (tmpopcode2[25] == 1'b1);
  assign isMULDIV3 = (op3 == MULDIV) && (tmpopcode3[25] == 1'b1);`
  assign isRegD_1 = (regd_1 != 5'b0) &&
                  ((op1 == RFORMAT) || (op1 == IFORMAT_ALU) || (op1 == IFORMAT_LOAD) || (op1 == UFORMAT_LUI) || 
                   (op1 == UFORMAT_AUIPC) || (op1 == UJFORMAT) || (op1 == IFORMAT_JALR) || (op1 == MULDIV));
  assign isRegD_2 = (regd_2 != 5'b0) &&
                  ((op2 == RFORMAT) || (op2 == IFORMAT_ALU) || (op2 == IFORMAT_LOAD) || (op2 == UFORMAT_LUI) || 
                   (op2 == UFORMAT_AUIPC) || (op2 == UJFORMAT) || (op2 == IFORMAT_JALR) || (op2 == MULDIV));
  assign isReg1_2 = (reg1_2 != 5'b0) &&
                  ((op2 == RFORMAT) || (op2 == IFORMAT_ALU) || (op2 == IFORMAT_LOAD) || (op2 == SFORMAT) || 
                   (op2 == SBFORMAT) || (op2 == IFORMAT_JALR) || (op2 == MULDIV));
  assign isReg2_2 = (reg2_2 != 5'b0) &&
                  ((op2 == RFORMAT) || (op2 == SFORMAT) || (op2 == SBFORMAT) || (op2 == MULDIV));
  assign isReg1_3 = (reg1_3 != 5'b0) &&
                  ((op3 == RFORMAT) || (op3 == IFORMAT_ALU) || (op3 == IFORMAT_LOAD) || (op3 == SFORMAT) || 
                   (op3 == SBFORMAT) || (op3 == IFORMAT_JALR) || (op3 == MULDIV));
  assign isReg2_3 = (reg2_3 != 5'b0) &&
                  ((op3 == RFORMAT) || (op3 == SFORMAT) || (op3 == SBFORMAT) || (op3 == MULDIV));
  wire [1:0] superscalar;
  assign superscalar = (!isBranch1 && !isBranch2 && // not BRANCH on 1,2
                        !isMemOp2 && !isMemOp3 && // not MemOP on 2,3
                        !isMULDIV2 && !isMULDIV3 && // not MULDIV on 2,3
                        (!isRegD_1 || (!isReg1_2 || (regd_1 != reg1_2)) && (!isReg2_2 || (regd_1 != reg2_2))) && // not REGISTER DEPENDANCY 1-2
                        (!isRegD_1 || (!isReg1_3 || (regd_1 != reg1_3)) && (!isReg2_3 || (regd_1 != reg2_3))) && // not REGISTER DEPENDANCY 1-3
                        (!isRegD_2 || (!isReg1_3 || (regd_2 != reg1_3)) && (!isReg2_3 || (regd_2 != reg2_3)))) ? 2'b10 : // not REGISTER DEPENDANCY 2-3
                       (!isBranch1 && // not BRANCH1
                        !isMemOp2 && // not MemOP2
                        !isMULDIV2 && /// not MULDIV2
                        (!isRegD_1 || (!isReg1_2 || (regd_1 != reg1_2)) && (!isReg2_2 || (regd_1 != reg2_2)))) ? 2'b01 : // not REGISTER DEPENDANCY 1-2
                        2'b00;

Coremark/MHzが5.44でRV32IM-1より68%性能向上、RV32IM-2より11.5%性能向上、RV32IM-2allより0.55%性能低下、DMIPが1.90でRV32IM-1より81%性能向上、RV32IM-2より21.8%性能向上、RV32IM-2allより4.40%性能向上しています。

Coremarkが2命令同時実行のRV32IM-2allより低下しているのが興味深いです。むやみに同時命令実行数を増やしても様々なハザードで同時実行が制限されるため命令レベルの並列度の向上につながらないことがあることを示しています。

RV32IM-3.png

RV32IM-3mul

3命令同時実行のスーパースカラCPUです。

乗算除算剰余演算器が3つです。これ以外はRV32IM-3と同じです。

Coremark/MHzが5.44でRV32IM-3と変わらず、DMIPが1.91でRV32IM-3より0.53%向上です。ほとんどRV32IM-3と変わりません。RV32IM-2からRV32IM-2mulでは多少性能が向上していましたが、3命令同時実行では乗算除算剰余演算器を多くしてもあまり性能向上につながらないようです。

RV32IM-3mul.png

RV32IM-3mem

3命令同時実行のスーパースカラCPUです。

メモリユニットがライン1とライン2とライン3すべてにあり、同時に3アドレス読み書きができるようにしています。ただしライン1の命令がSTOREするのと同じアドレスにライン2やライン3の命令がLOADする場合やライン2やライン3のLOADが古い値になってしまうのでライン1の命令のみ実行するように制限しています。それ以外はRV32IM-3と同じです。

Coremark/MHzが5.56でRV32IM-1より72%向上、RV32IM-2より13.9%向上、RV32IM-3より2.2%向上、DMIPが2.12でRV32IM-1より102%向上、RV32IM-2より35.9%向上、RV32IM-3より11.6%向上です。3命令同時実行の場合でもメモリアクセスを複数並行実行できると性能が向上するようです。

RV32IM-3mem.png

RV32IM-3bp

3命令同時実行のスーパースカラCPUです。

第1命令が条件分岐命令の場合に、第2第3命令を制限するのではなく第2第3命令を実行し、第1命令の条件未成立時はそのまま結果を書き込み、第1命令条件分岐成立時には結果を捨てる投機的実行を実装しています。第2命令が条件分岐命令の場合にも第3命令を制限するのではなく第3命令を実行し、第2命令の条件未成立時はそのまま結果を書き込み、第2命令条件分岐成立時には結果を捨てています。これ以外はRV32IM-3と同じです。

Coremark/MHzが5.77でRV32IM-1より78%向上、RV32IM-2より18.2%向上、RV32IM-3より6.1%向上、DMIPが1.91でRV32IM-1より85%向上、RV32IM-2より24.3%向上、RV32IM-3より2.1%向上です。3命令同時実行の場合でも投機的実行により性能が向上するようです。

RV32IM-3all

3命令同時実行のスーパースカラCPUです。
RV32IM-3mul、RV32IM-3mem、RV32IM-3bpの要素をすべて取り入れて、構造ハザードに乗算除算剰余回路とメモリアクセス回路を3つ実装することで対応し、制御ハザードに投機的実行で対応したものです。

Coremark/MHzが6.48でRV32IM-1より100%向上、RV32IM-2より32.8%向上、RV32IM-3より19.1%向上、DMIPが2.18でRV32IM-1より108%向上、RV32IM-2より39.7%向上、RV32IM-3より14.7%向上しています。

RV32I-2allよりFPGA等の現実の回路として実装するのはさらに無理な構造になっていますが、3命令同時実行にした場合2命令同時実行より命令レベルの並列度を向上させることができています。このCoremark/MHzの6.48はアウトオブオーダーのBOOM v3の6.2を超えています。アウトオブオーダー実行でなくてもインオーダーであっても命令レベルの並列度を向上させる余地があるということを示していると考えられます。

ASFRV32IM-super2

ASFRV32IM-superを踏まえ、より実機で再現しやすい構造への修正(とはいってもまだまだFPGAでそのまま実装できないとは思いますが)と、より多くの命令のMacro Op Fusionに対応したシングルサイクルインオーダースーパースカラのCPUがASFRV32IM-super2になります。

ASFRV32IM-superは8bit単位でメモリアクセスができ、命令メモリとデータメモリを共有していますが、複数アドレスからの同時メモリアクセスは実機での実装が難しくなるので、ASFRV32IM-super2では32bitアラインメントでのメモリアクセスとし、命令メモリとデータメモリを分離しました。例外処理非対応なためalignment例外を扱えない関係上ASFRV32IM-super2はRV32IM完全準拠ではなくなっています。Zifencei拡張のFENCE.I命令にも非対応になります。また、ASFRV32IM-superではメモリアクセスの改善が性能向上につながっているようなので、(無関係な複数アドレスではなく)連続するアドレスに対して64bitで読み書きできるようにし、Macro Op Fusionによる融合命令で64bitメモリアクセスを行うことができるようにしました。

RV32IM-1new

リファレンス用のスカラーCPUです。スーパースカラではありません。ASFRV32IMとは異なり、PC、命令メモリ、デコーダー、レジスタファイル、ALU,分岐ユニット、データメモリ、メインの8つのmoduleに分割して実装しています。

Coremark/MHzは3.24、DMIPは1.05で、ASFRV32IMやASFRV32IM-superのRV32IM-1と同じです。

RV32IM-1new.png

RV32IM-2new-notopt

2命令同時実行のスーパースカラCPUです。

ASFRV32IM-superのRV32IM-2と同様の構造です。命令ラインは2本で、乗算除算剰余とメモリアクセスははライン1のみ。ライン1の命令が書き込むレジスタをライン2が使用するときはライン1の命令のみ実行、分岐命令がライン1に来た場合ライン1の命令のみ実行することで制御ハザードに対応しています。

Coremark/MHzが4.88で、DMIPが1.565で、ASFRV32IM-superのRV32IM-2と同じです。

RV32IM-2new-notopt.png

RV32IM-2new

2命令同時実行のスーパースカラCPUです。

制御ハザードの改善のために、先行命令が条件分岐命令の場合に、後続命令を制限するのではなく後続命令を実行し、先行命令の条件未成立時はそのまま結果を書き込み、先行命令条件分岐成立時には結果を捨てる投機的実行と、RAWの改善と乗算除算剰余ユイットとメモリアクセスに関する構造ハザードの改善のために、slli+add, add+load, lui+addi, mulh+mulまたはdiv+rem, load pair(連続4バイト+4バイトロード), store pair(連続4バイト+4バイトストア)の6つの融合命令をMacro Op Fusionとして実装しています。

Coremark/MHzが5.01でRV32IM-1newより55%向上、RV32IM-2new-notoptより2.66%向上、DMIPが1.60でRV32IM-1newより52%向上でRV32IM-2new-notoptより2.56%向上しています。

Load Effective Address (LEA)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| 0000000shamt | rs1  |funct3 |  rd     |0001011| +|000  |000000rs2   |010|
先行命令 slli rd, rs1, {1,2}
後続命令 add rd, rd, rs2
融合命令 lea rd, rs1, rs2, {1,2}

ASFRV32IM-superのRV32IM-2mofのLEAと同じです。

Indexed Load

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| 0x00000| rs2 | rs1  |funct3 |  rd     |0001011| +|memop|000000000000|011|
先行命令 add rd, rs1, rs2
後続命令 lb rd, 0(rd) (lb以外のlbu, lh, lhu, lwも可)
融合命令 ilb rd, rs1, rs2

ASFRV32IM-superのRV32IM-2mofのIndexed Loadと同じです。

Load Immediate 1

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[31:12]                   |  rd    |0001011| +|000  |imm[11:0]   |100|
先行命令 lui rd, imm[31:12]
後続命令 addi rd, rd, imm[11:0]
融合命令 lii1 rd, rs1, 符号拡張imm[31:12]+符号拡張imm[11:0] (imm[31:0]ではない)

ASFRV32IM-superのRV32IM-2mofのLoad Immediate 1と命令の意味は同じです。

ASFRV32IM-superのRV32IM-2mofと異なり、ALUのセレクタを拡張し、ALUに符号拡張したimm[31:12]と符号拡張したimm[11:0]を入力してADD演算した出力をrdに入れるように実装しています。

DECODEの283行目

  assign imm_0 = {{20{op2_imm[11]}}, op2_imm};

ALUの406-411行目

  assign s_data1 = (op1sel == 3'b110) ? (r_data1 << 2) :
                   (op1sel == 3'b101) ? (r_data1 << 1) :
                   (op1sel == 3'b010) ? imm_0 : 
                   (op1sel == 3'b001) ? pc : 
                                        r_data1;

Load Immediate 2

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[31:12]                   |  rd    |0001011| +|memop|imm[11:0]   |101|
先行命令 lui rd, imm[31:12]
後続命令 lb rd, rd, imm[11:0] (lbuやlh、lhu、lwも可)
融合命令 lii2 rd, rs1, 符号拡張imm[31:12]+符号拡張imm[11:0] (imm[31:0]ではない)

ASFRV32IM-superのRV32IM-2mofのLoad Immediate 2と命令の意味は同じです。
Load Immediate 1と同様にALUでアドレスを計算してメモリから読み込みをしています。

Wide Multiply/Divide & Remainder

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| funct7   | rs2 | rs1  |funct3|  rdh   |0001011| +|000  |0000000rdl  |001|
先行命令 mulh[[s]u] rdh, rs1, rs2
後続命令 mul        rdl, rs1, rs2
先行命令 div[u]     rdh, rs1, rs2
後続命令 rem[u]     rdl, rs1, rs2
融合命令 WM rdh, rdl, rs1, rs2
融合命令 DR rdh, rdl, rs1, rs2

32bit×32bit=64bitの乗算を一度に行います。ALUから演算出力を32bitから64bitに拡張し、ライン1のレジスタ書き込み回路はそのままALUの上位32bitを出力してrdhに書き込み、ライン2のレジスタ書き込みの回路経由でALUの下位32bit出力をrdlに書き込むようにしています。

32bit÷32bit=商32bit剰余32bitの除算を一度に行います。ALUから演算出力を32bitから64bitに拡張し、ライン1のレジスタ書き込み回路はそのまま商32bitを出力してrdhに書き込み、ライン2のレジスタ書き込みの回路経由でALUの剰余32bit出力をrdlに書き込むようにしています。

Load-pair/Store-pair (load1)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[11:0]      | rs1  |010  |  rd1    |0001011| +|000  |0000000rd2  |110|
先行命令 lw rd1, imm(rs1)
後続命令 lw rd2, imm+4(rs1)
融合命令 lwp rd1, rs2, imm(rs1)

メモリから連続する64bitのデータを読み出します。

rs1+immのアドレスから32bit+32bit=64bitのデータを1サイクルで読み出して、rs1+immのデータをrd1に書き込み、rs1+imm+4のデータをライン2のレジスタ書き込みの回路経由でrd2に書き込みます。

ASFRV32IM-superのRV32IM-2memやRV32IM-3memのCormark/MHz値か、らメモリアクセス命令を並列実行できると高速化につながりそうだが任意の2アドレスから32bit+32bit同時読み込みするのは回路実装が困難になると思ったので、連続するアドレスから32bit+32bit読み出す命令を実装してみました。

Load-pair/Store-pair (load2)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[11:0]      | rs1  |010  |  rd1    |0001011| +|010  |0000000rd2  |110|
先行命令 lw    rd1, imm(rs1)
後続命令 lw    rd2, imm-4(rs1)
融合命令 lwpb rd1, rs2, imm(rs1)

lwpとほぼ同じですが、rs1+imm-4のアドレスから32bit+32bit=64bitのデータを1サイクルで読み出して、rs1+immのデータをrd1に書き込み、rs1+imm-4のデータをライン2のレジスタ書き込みの回路経由でrd2に書き込みます。

Load-pair/Store-pair (save1)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
|imm[11:5] | rs2 | rs1  |010  |imm[4:0] |0001011| +|001  |0000000rs3  |110|
先行命令 sw    rs2, imm(rs1)
後続命令 sw    rs3, imm+4(rs1)
融合命令 swp rs2, rs3, imm(rs1)

lwpのストア版です。1サイクルでメモリに対して連続するアドレスに64bitのデータを書き込みます。

rs1+immのアドレスにrs2の32bitデータを書き込み、rs1+imm+4のアドレスにライン2のレジスタ読み込み回路経由でrs3の32bitデータを読み込みメモリに書き込みます。

Load-pair/Store-pair (save2)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0

先行命令 sw    rs2, imm(rs1)
後続命令 sw    rs3, imm-4(rs1)
融合命令 swpb rs2, rs3, imm(rs1)

swpとほぼ同じですが、rs1+immのアドレスにrs2の32bitデータを書き込みrs1+imm-4のアドレスにrs3の32bitデータを書き込みます。

Macro Op Fusionのテスト

mulh+mulの融合命令dwをテストできるmoptest2.S、div+remの融合命令drをテストできるmoptest3.S、連続するlw+lwの融合命令lwp,lwpbと連続するsw+swの融合命令swp,swpbをテストできるmoptest4.Sを用意してあるので、手動ですがMacro Op Fusionのテストが可能です。

RV32IM-2new.png

RV32IM-3new-notopt

3命令同時実行のスーパースカラCPUです。

ASFRV32IM-superのRV32IM-3と同様の構造です。命令ラインは3本で、乗算除算剰余とメモリアクセスははライン1のみ。ライン1の命令が書き込むレジスタをライン2が使用するときはライン1の命令のみ実行、ライン1の命令が書き込むレジスタをライン3が使用するときはライン1ライン2の命令を実行、ライン2の命令が書き込むレジスタをライン3が使用するときはライン1ライン2の命令を実行することでデータハザードに対応。分岐命令がライン1に来た場合ライン1の命令のみ実行、分岐命令がライン2に来た場合ライン1ライン2の命令を実行することで制御ハザードに対応しています。

Coremark/MHzが5.44で、DMIPが1.90で、ASFRV32IM-superのRV32IM-3と同じです。

RV32IM-3new-notopt.png

RV32IM-3new

3命令同時実行のスーパースカラCPUです。

RV32IM-2new-notoptに対するRV32IM-2newと同様に、投機的実行とslli+add, add+load, lui+addi, mulh+mulまたはdiv+rem, load pair(連続4バイト+4バイトロード), store pair(連続4バイト+4バイトストア)の6つの融合命令に加え、slli+add+loadを加え合計7つの融合命令をMacro Op Fusionとして実装しています。

Coremark/MHzが5.79でRV32IM-1newより79%向上、RV32IM-3new-notoptより6.43%向上、DMIPが2.00でRV32IM-1newより90%向上でRV32IM-3new-notoptより5.26%向上しています。

Fused Indexed Load

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| 0000000shamt | rs1  |funct3 |  rd     |0001011| +|memop|0000000rs2  |111|
先行命令  slli rd, rs1, {1,2}
後続命令1 add  rd, rd, rs2
後続命令2 lb rd, 0(rd) (lbuやlh、lhu、lwも可)
融合命令 fil rd, rs1, rs2, {1,2}

Load Effective AddressとIndex Loadを組み合わせて3命令融合するFused Indexed Loadです。

16bit配列の場合
short array[1024];
int offset;
offset = 3;

d = array[offset];

16bit単位のshort 配列arrayのoffset番目の要素を読み込む場合、読み込むアドレス&(array[offset])は
arrayの(先頭)アドレス+(offset << 1)で、このアドレスから16bit読み込みます。

slli rd, rs1, 1
add rd, rd, rs2
lh rd, 0(rd)

で実現できます。

この組み合わせは先行命令の演算結果rdを後続命令1、後続命令2で利用しているため、RAWによりそのままでは同時実行できません。これをMacro Op Fusionで1つの融合命令として処理することで並行実行できるようになります。

同様に32bit配列の読み込みも1つの融合命令で実現できます。

Macro Op Fusionのテスト

連続するslli+add+lwの融合命令filはmoptest5.Sでテストできます。

RV32IM-3new.png

RV32IM-3new2

3命令同時実行のスーパースカラCPUです。

RV32IM-3newのメモリアクセスを連続64bitから連続96bitに拡張し、連続するメモリアクセス3命令を1つの融合命令で実行できるようにしました。

Coremark/MHzが5.80でRV32IM-1newより80%向上、RV32IM-3new-notoptより6.62%向上、RV32IM-3newより0.17%向上、DMIPが2.03でRV32IM-1newより93%向上、RV32IM-3new-notoptより6.84%向上、RV32IM-3newより1.50%向上しています。

Load-trio/Store-trio (loadt1)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[11:0]      | rs1  |010  |  rd1    |0001011| +|000  |00rd3rd2    |000|
先行命令  lw    rd1, imm(rs1)
後続命令1 lw    rd2, imm+4(rs1)
後続命令2 lw    rd3, imm+8(rs1)
融合命令 lwt rd1, rd2, rd3, imm(rs1)

メモリから連続する96bitのデータを読み出します。

rs1+immのアドレスから32bit+32bit+32bit=96bitのデータを1サイクルで読み出して、rs1+immのデータをrd1に書き込み、rs1+imm+4のデータをライン2のレジスタ書き込みの回路経由でrd2に書き込み、rs1+imm+8のデータをライン3のレジスタ書き込みの回路経由でrd3に書き込みます。

Load-trio/Store-trio (loadt2)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
| imm[11:0]      | rs1  |010  |  rd1    |0001011| +|000  |00rd3rd2    |000|
先行命令  lw    rd1, imm(rs1)
後続命令1 lw    rd2, imm-4(rs1)
後続命令2 lw    rd3, imm-8(rs1)
融合命令 lwtb rd1, rd2, rd3, imm(rs1)

lwtとほぼ同じですが、rs1+imm-8のアドレスから32bit+32bit+32bit=96bitのデータを1サイクルで読み出して、rs1+immのデータをrd1に書き込み、rs1+imm-4のデータをライン2のレジスタ書き込みの回路経由でrd2に書き込み、rs1+imm-8のデータをライン3のレジスタ書き込みの回路経由でrd3に書き込みます。

Load-trio/Store-trio (storet1)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
|imm[11:5] | rs2 | rs1  |010  |  rd1    |0001011| +|001  |00rs4rs3    |000|
先行命令  sw    rs2, imm(rs1)
後続命令1 sw    rs3, imm+4(rs1)
後続命令2 sw    rs4, imm+8(rs1)
融合命令 swt rs2, rs3, rs4, imm(rs1)

lwtのストア版です。1サイクルでメモリに対して連続するアドレスに96bitのデータを書き込みます。

rs1+immのアドレスにrs2の32bitデータを書き込み、rs1+imm+4のアドレスにライン2のレジスタ読み込み回路経由でrs3の32bitデータを読み込みメモリに書き込み、rs1+imm+8のアドレスにライン3のレジスタ読み込み回路経由でrs4の32bitデータを読み込みメモリに書き込みます。

Load-trio/Store-trio (storet2)

|opcode                                         |  |opcode2               |
31      25 24 20 19  15 14  12 11      7 6     0   17  1514          3 2  0
|imm[11:5] | rs2 | rs1  |010  |  rd1    |0001011| +|011  |00rs4rs3    |000|
先行命令  sw    rs2, imm(rs1)
後続命令1 sw    rs3, imm-4(rs1)
後続命令2 sw    rs4, imm-8(rs1)
融合命令 swtb rs2, rs3, rs4, imm(rs1)

swtと同様に、連続する96bitのメモリへストアします。

rs1+immのアドレスにrs2の32bitデータを書き込み、rs1+imm-4のアドレスにライン2のレジスタ読み込み回路経由でrs3の32bitデータを読み込みメモリに書き込み、rs1+imm-8のアドレスにライン3のレジスタ読み込み回路経由でrs4の32bitデータを読み込みメモリに書き込みます。

Macro Op Fusionのテスト

連続するlw+lw+lwの融合命令lwt,lwtb及びに連続するsw+sw+swの融合命令swt,swtbをテストできるmoptest6.Sを用意してあるので、手動ですがMacro Op Fusionのテストが可能です。

RV32IM-3new2.png

まとめ

実装 COREMARK(秒) COREMARK/MHz 比率 DHRYSTONE DMIPS 比率
RV32IM-1 3083 3.24 100% 1844 1.05 100%
--------------- ----------- ------------ ----- --------- ----- -----
RV32IM-2 2050 4.88 151% 2739 1.56 149%
RV32IM-2mul 1986 5.04 156% 2739 1.56 149%
RV32IM-2mem 1949 5.13 158% 2923 1.66 158%
RV32IM-2mem2 1945 5.14 159% 2941 1.67 159%
RV32IM-2bp 2002 5.00 154% 2762 1.57 150%
RV32IM-2mof 2050 4.88 151% 2747 1.56 149%
RV32IM-2all 1829 5.47 169% 3194 1.82 173%
--------------- ----------- ------------ ----- --------- ----- -----
RV32IM-3 1839 5.44 168% 3344 1.90 181%
RV32IM-3mul 1839 5.44 168% 3355 1.91 182%
RV32IM-3mem 1798 5.56 172% 3731 2.12 202%
RV32IM-3bp 1733 5.77 178% 3412 1.94 185%
RV32IM-3all 1544 6.48 200% 3831 2.18 208%
--------------- ----------- ------------ ----- --------- ----- -----
RV32IM-1new 3083 3.24 100% 1844 1.05 100%
RV32IM-2new-notopt 2050 4.88 151% 2739 1.56 149%
RV32IM-2new 1995 5.01 155% 2808 1.60 152%
RV32IM-3new-notopt 1839 5.44 168% 3344 1.90 181%
RV32IM-3new 1727 5.79 179% 3521 2.00 190%
RV32IM-3new2 1725 5.80 179% 3571 2.03 193%

様々な手法で命令レベルの並列度を向上させて性能評価を行いました。CPUの構造をより深く理解するための資料として参考にしてみてください。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2