FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RocketChip RISC-V実装RTLにてベンチマークを計測する(3. -O3によるCoremarkコンパイル)

ずいぶん大昔の記事を引っ張り出してきたが、RASの解析などを行うにあたり、BOOMの実装を調べたくて、BOOMの実性能がどうなっているのか調べたくなってきたので調査している。

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

gem5を使えばBOOMのパイプライントレースの解析を行うことができる。これで自前でコンパイルしたCoremarkを解析して、BOOMの性能を解析したい。

-O3 -funroll-loops を加えたCoremarkのコンパイル

github.com

funroll-loopsと-O3を加えて、評価用のCoremarkプログラムをコンパイルした。RocketもBOOMも64bitで動作するし、自作ISSも64bitモードに対応しているのでこれで比較する。

start_time(), stop_time()の間の命令数

grep -e 80006a90 -e 80006aa0 coremark.sw.log
     15260:M:MBar:[0000000080006a90][P000080006a90] 00000033 : add        r00,r00,r00          r00=>0000000000000000 r00=>0000000000000000
    370114:M:MBar:[0000000080006aa0][P000080006aa0] 00000033 : add        r00,r00,r00          r00=>0000000000000000 r00=>0000000000000000

というわけで命令数的には35万命令程度なのだが、-funroll-loopsを加えると命令数が増えてしまった。不思議。

BOOM(2-way)での実行

BOOMで当該バイナリを実行して、同様にstart_time()stop_time()のサイクル数を解析した。

make CONFIG=BOOMConfig output/coremark.riscv.out
../boom/util/pipeview-helper.py -f output/coremark.riscv.out > cleaned_trace.out
/work/gem5/util/o3-pipeview.py -o pipeview.out --colorcleaned_trace.out

前の記事にも書いたが、output/coremark.riscv.outの先頭にある1行を削除しないと解析されないので注意。

f:id:msyksphinz:20170417000310p:plain

$ grep -e start_time -e stop_time coremark.dmp
0000000080006a90 <start_time>:
    80006a98:   1e02aa23                sw      zero,500(t0) # 8000dc88 <start_time_val>
0000000080006aa0 <stop_time>:
    80006aa8:   1e02a023                sw      zero,480(t0) # 8000dc84 <stop_time_val>

$ grep -e 80006a90 -e 80006aa0 pipeview.out
[..................f..di...cr....................................................]-(     1168480000) 0x0080006a90.0 add zero, zero, zero      [    619748]
[====================f==di=======================================================]-(     1168480000) 0x0080006aa0.0 -----add zero, zero, zero [    619752]
[=========================================================f======================]-(     1662480000) 0x0080006a90.0 -----add zero, zero, zero [   1284475]
[.....................................................................f..di...cr.]-(     1666800000) 0x0080006aa0.0 add zero, zero, zero      [   1291132]
[======================f==di=====================================================]-(     1671200000) 0x0080006a90.0 -----add zero, zero, zero [   1296453]
[========================f=======================================================]-(     1671200000) 0x0080006aa0.0 -----add zero, zero, zero [   1296457]

ちなみにpipeiew.outの見方だが、------アセンブリ命令の前についているのは、パイプラインフラッシュされる対象の命令なので無視。こう見ると、start_time()からstop_time()までのサイクル数は大よそ { \displaystyle
1666800-1168480 = 498320
} あれ、サイクル数的にはだいぶ増えたぞ?

途中のサイクルを見ていると、

[................................................................f..di..c.r......]-(     1279680000) 0x0080003690.0 lh a2, 0(s8)              [    764124]
[................................................................f..di...cr......]-(     1279680000) 0x0080003694.0 addw a4, s10, s6          [    764125]
[.................................................................f..di...cr.....]-(     1279680000) 0x0080003698.0 addiw a5, a5, 4           [    764126]
[.................................................................f..d.i...cr....]-(     1279680000) 0x008000369c.0 mulw s10, ra, s7          [    764127]
[==================================================================f==d==========]-(     1279680000) 0x00800036a0.0 -----addw t6, t6, a3      [    764128]
[==================================================================f==d==========]-(     1279680000) 0x00800036a4.0 -----mulw ra, t5, t0      [    764129]
[===================================================================f==d=========]-(     1279680000) 0x00800036a8.0 -----addw s7, t6, s10     [    764130]
[===================================================================f==d=========]-(     1279680000) 0x00800036ac.0 -----mulw a6, t2, a2      [    764131]
[====================================================================f==d========]-(     1279680000) 0x00800036b0.0 -----addw t4, s7, ra      [    764132]
[====================================================================f==d========]-(     1279680000) 0x00800036b4.0 -----addw t6, t4, a6      [    764133]
[=====================================================================f==d=======]-(     1279680000) 0x00800036b8.0 -----bne t3, a5, pc - 224 [    764134]
[======================================================================f==d======]-(     1279680000) 0x00800035d8.0 -----addw s8, a4, s6      [    764135]
[======================================================================f==d======]-(     1279680000) 0x00800035dc.0 -----slli s7, a5, 32      [    764136]
[=======================================================================f==d=====]-(     1279680000) 0x00800035e0.0 -----slli t2, a4, 32      [    764137]
[=======================================================================f==d=====]-(     1279680000) 0x00800035e4.0 -----addiw t5, a5, 1      [    764138]
[========================================================================f==d====]-(     1279680000) 0x00800035e8.0 -----srli a3, s7, 32      [    764139]
[========================================================================f==d====]-(     1279680000) 0x00800035ec.0 -----srli s10, t2, 32     [    764140]
[=========================================================================f==d===]-(     1279680000) 0x00800035f0.0 -----addw a2, s8, s6      [    764141]
[=========================================================================f==d===]-(     1279680000) 0x00800035f4.0 -----slli ra, t5, 32      [    764142]
[==========================================================================f=====]-(     1279680000) 0x00800035f8.0 -----slli a4, s8, 32      [    764143]
[==========================================================================f=====]-(     1279680000) 0x00800035fc.0 -----addiw t0, a5, 2      [    764144]
[.............f..di...cr.........................................................]-(     1279760000) 0x0000000800.0 j pc + 0x3c               [    764145]
[...............f.......di...cr..................................................]-(     1279760000) 0x000000083c.0 csrw dscratch, s0         [    764146]

RISC-Vは通常命令領域は0x0080000000から始まるのだが、mulw命令を実行後に突然0x0000000800に飛んでいたりしている。これは何だろう? それはともかく、現在のCoremarkの解析において気になっている部分を見てみよう。

load –> arith –> storeのループ

例えば、

loop 
lw     a0, 0(a1)
add    a0, a0, 1
sw     a0, 0(a3)
...
bltz   loop

みたいなループがある場合、swのメモリオーダを厳密に守ると、次のループのロードがswよりも先に出せず、パイプラインを活用することができない。 これはBOOMではどのように実行しているだろう。

f:id:msyksphinz:20170417001825p:plain

例えばこういうループだな。lbuとsbに依存関係があって、次のループのlbuの実行を遅らせていると確実にパイプラインに影響があるのだが、BOOMではほとんど影響なくストールを生じさせていない。 このあたりは、ストアバッファの中を探索して、無関係なアドレスであれば先にロードを通すなどの処理が行われていると予想する。

ランニング用のイヤホンを買い替えた

ランニング時に音楽を聴くため、Bluetoothのイヤホンの非常に安いものを購入していたのだが、何度か使用しているうちに問題が分かってきた。

msyksphinz.hatenablog.com

www2.elecom.co.jp

このイヤホンは非常に軽くて良いのだが、コントローラの部分が左右非対称の場所に配置されており、走っている際にそれが大きく揺れて位置がずれる。 これが気になって集中できなくなってしまい、何度も位置を直さなければならない。

これを防ぐために、今度は明確なコントローがついていないイヤホンを購入してみた。

まだランニングには使用していないが、付け心地は悪くなく、また音質も問題ない。

これを使って今度から走ってみよう。

プロセッサの高速化技法: リターンアドレススタックの実装方法検討

リターンアドレススタック(Return Address Stack: RAS)は、関数呼び出しなどの命令(Call命令)が実行された場合、その関数の戻り先をあらかじめスタックに記憶しておき、Return文が実行された場合にスタックから戻りアドレスを取り出し、 そこからフェッチを行う機構である。

f:id:msyksphinz:20170413234223p:plain

この場合、実際にどのような機構を実装すればよいかという話になるが、RASへの格納はCall命令がコミットされた時点でよかろう。 そうして、フェッチが行われる毎にReturn命令かどうかを判定し、Return命令ならばRASからPopするという方式になる。

f:id:msyksphinz:20170413235004p:plain

ただし、この実装をしていて問題を発見した。 すぐに戻ってくるような関数でテストしていると、どうやらCall命令がコミットする前に(分岐予測のため)Return命令がフェッチされてしまい、RASに積む前にPopが必要となってしまう。 実装は簡単だが、これで背右脳を出すのは難しいのだろうか。

もう一方の実装方法として、Call命令がコミットされる前、つまり命令をフェッチしてきた段階でRASにスタックし、Popと同じステージで制御する。 これも実装としては簡単だが、やはり投機的な命令まで使って判定してしまうため、スタックを汚してしまい、性能に影響が出る。

f:id:msyksphinz:20170414004627p:plain

一般的なRASの実装はどのようになっているのだろう?調査する必要がある。

即値ジャンプ命令の分岐予測実装

即値命令、RISC-VにおけるJAL命令は分岐予測の実装としては非常に単純だ。

即値命令は比較の必要もなく、またジャンプ先も常に固定だ。予測が容易であるため、実装も簡単だ。 一つだけ難しいのは、レジスタ参照の即値ジャンプの場合。これはレジスタの内容によってジャンプ先が変わるので、予測が難しく、 実装も難しいのでここではパスする。 (ただ、自己書き換えが起こるような状況では、別のことも考慮しなければならない。これは、分岐予測全般に言えることだ)。

即値ジャンプ命令の分岐予測実行

RISC-V自作プロセッサにおいて、即値命令に対して分岐予測を実装した場合のサイクル数を比較した。

Coremarkの比較において、1割程度改善した。こいつはいいぞ!

サイクル数 比率
JAL即値分岐予測無し 429650 1.00
JAL即値分岐予測 387781 0.90

リザベーションステーションの1サイクル先出し命令発行

リザベーションステーション(Reservation Station: RS)というのは、アウトオブオーダプロセッサにおける命令待ち合わせ機構の一つで、その命令が必要とするオペランドデータが、リザルトバスに流れてくるの監視している機構である。

リザルトバスに自命令の欲しいオペランドが流れてくるとそれを回収し、命令実行に必要なすべてのオペランドが揃うと命令を発行するという仕組みだ。

Reservation Station - Wikipedia

https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/Intel_Nehalem_arch.svg/800px-Intel_Nehalem_arch.svg.png

(https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/Intel_Nehalem_arch.svg/800px-Intel_Nehalem_arch.svg.png より画像引用)

命令を発行するには、RS内でリザルトバスを監視し、すべてのオペランドが揃わないと発行できないが、一般的にこれを高速化する方法の一つとしてタグの先読みという方法がある。

  • Understanding the detailed Architecture of AMD’s 64 bit Core

http://www.chip-architect.com/news/2003_09_21_Detailed_Architecture_of_AMDs_64bit_Core.html#1

The Tag busses run one cycle in advance of the result data-busses. The Reservation Station does not need to look at all the busses. The Tag’s sub-index identifies which of the three ALU’s will produce the result. It also knows if the data will come from one of the two cache read ports. It can select the Tag bus in advance rather then having to test all the Tags.

なるほど、オペランドのタグのみ1サイクル早く先出しし、タグが一致した時点で命令を発行、実行ユニット内で実際のリザルトデータを受け取ることで、RSから1サイクル命令を先出しすることができる。

とまあ、この機構は現在のRISC-V自作CPUでも使用している。ただし、ALUx2、LSU、BRUのうち使用しているのはALUx2の互いのリザルトバスだけだ。これを拡張してみるとどうなるのか。

f:id:msyksphinz:20170412010736p:plain

これを、LSUとBRU側にも実装した。LSUからのロードデータの先出をし、タグをLSUからも読めるようにする。 また、BRUに対しても先出のデータを参照できるようにした。

f:id:msyksphinz:20170412011007p:plain

前の実装と比べて、1割程度高速化された。しかしこの時点でCoremarkスコアはまだ3.0を切っている。どうにかして改善せねば。。。

f:id:msyksphinz:20170412011402p:plain

命令フェッチラインに基づく最適化

マイコンでは一般的なのかもしれないが、プログラムの最適化の1つの方法として命令フェッチラインに基づく最適化というものがある。

これは早い話が、ループの境界がなるべく命令フェッチの範囲を超えないようにし、フェッチ回数を減らすというものだ。

f:id:msyksphinz:20170411005821p:plain

例えば、4命令をループさせるプログラムがあったとして、

data_ready_loop:
    sw      x2,0x00(x1)
    // addi    x2,x2,0x01
    addi    x1,x1,4
    addi    x4,x4,1
    bltu    x4,x5,data_ready_loop

このプログラムをコンパイルすると、以下のようなアセンブラが生成された。

80000090 <data_ready_loop>:
80000090:       0020a023                sw      sp,0(ra) # 80002000 <_sp+0xffffde38>
80000094:       00408093                addi    ra,ra,4
80000098:       00120213                addi    tp,tp,1 # 1 <_tbss_end+0x1>
8000009c:       fe526ae3                bltu    tp,t0,80000090 <data_ready_loop>

現在、自作RISC-Vプロセッサのフェッチラインは128ビットだ。つまり、これが0x10の境界に入っていれば、一度にフェッチしてこれる。 これが、フェッチ境界を跨いでしまったらどうだろう。

800000c8 <data_ready_loop_hit2>:
800000c8:       0020a023                sw      sp,0(ra) # 80002000 <_sp+0xffffde78>
800000cc:       00408093                addi    ra,ra,4
800000d0:       00120213                addi    tp,tp,1 # 1 <_tbss_end+0x1>
800000d4:       fc526ee3                bltu    tp,t0,800000b0 <data_ready_loop_hit>

この場合、このループを実行するのに、2回のフェッチが必要になる。この2種類のループを、RTLシミュレーションで比較した。

  • フェッチアラインに合わせたもの: 151cycle
  • フェッチ境界を跨いだもの : 239cycle

結構な差が出てきた。これはフェッチを2回したことにより、無駄な命令発行をしているということでもある。

自作RISC-VのSystemVerilog化と命令発行方式の変更

ちまちまRISC-Vの自作プロセッサを改造している。まずは拡張性の向上のためにRISC-Vプロセッサの実装をSystemVerilogに置き換えている。

SystemVerilogの詳細についてはここでは述べないが、いくつか便利な記述があるので紹介。

packed struct

制御信号線周りについて、でコード信号を一度追加すると後でもう一本追加するのが大変だ。そこで、構造体を定義して信号線の拡張を容易にする。

typedef struct packed {
  logic         vld;
  logic [31: 0] inst;
  logic [31: 0] pc;
  logic         pr_en;
  logic         pr_taken;
} ic_type;

また、logic宣言した回路は、reg,wireの識別を行う必要がなく、組み合わせ回路の時にはalways_comb,順序回路の時はalways_ffで記述できるので便利だ。

wire w_signal;
reg  r_signal;

always @ (posedge clk, negedge reset_n) begin
  if (!reset_n) begin
     r_signal <= 1'b0;
  end else begin
     r_signal <= ...
  end
end

assign w_signal = ...

しかし、SystemVerilogの場合は、logic宣言をしていればalways_comb, always_ffのどちらを利用するかで組み合わせ回路か順序回路化を指定できる。

logic w_signal, r_signal;

always_ff @ (posedge clk, negedge reset_n) begin
  if (!reset_n) begin
     r_signal <= 1'b0;
  end else begin
     r_signal <= ...
  end
end

always_comb begin
  w_signal = ...
end

自作CPUの命令発行方式の変更

これらの記述方法を変更して拡張性を上げたのと、命令発行制御方式を変更した。これについては後日詳細を書きたい。

これの影響もあり、Coremarkスコアについては若干下がってしまった。以下はCoremarkのサイクル数だ。

サイクル数 stop_time()-start_time()
通常版 427167
発行方式変更版 472111

全体的にサイクル数が落ちている。これの解析を行う。 青が旧方式で、赤が新方式だ。

f:id:msyksphinz:20170409223813p:plain