FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://sites.google.com/site/fpgadevelopindex/

RocketChip RISC-V実装RTLにてベンチマークを計測する

この記事は ハードウェア開発、CPUアーキテクチャ Advent Calendar 2016 - Qiita の14日目の記事です。

Advent-Calendarを埋めてくれるかた、今からでも募集中です!是非参加してください! 僕一人では、クオリティのある記事を続けられそうにありません。。。(弱音)

1. Rocket ChipをRTLで動かすには

Rocket Chipは様々な環境で動作させることができるが、有料のEDAツールを使わずにシミュレーションするとしたら、Verilatorでシミュレーションするのが良い。

まず、Rocket Chipをリポジトリをクローンし、DefaultのRocketChipを構成してRTLシミュレーションの準備をする。詳細は、Rocket ChipのリポジトリのReadmeを参照して欲しい。

github.com

ツールセットが整っているならば、emulatorディレクトリに移動してmakeを実行するだけで良い。

cd emulator
make

ちなみに上記のリポジトリには、RISC-V用のテストセットが入っており、このなかに命令テスト用のリグレッションテストと、いくつかのベンチマークプログラムを動作させるための環境が構成されている。これらを参考に、Rocket用のCoremarkをビルドするためにはどのようにすれば良いのか考えてみる。

github.com

riscv-testsに含まれるベンチマークプログラムのコンパイル方法

まずは参考のために、riscv-testsに含まれているベンチマークプログラムはどのようにコンパイルされているのか調べてみよう。まずは当該ディレクトリに移動してコンパイルしてみる。

cd rocket-chip/riscv-tools/riscv-tests/benchmarks/
make
$ make
riscv64-unknown-elf-gcc -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -DPREALLOCATE=1 -DHOST_DEBUG=0 \
             -c -I./../env -I./common -I./median -I./qsort -I./rsort -I./towers -I./vvadd -I./multiply -I./mm -I./dhrystone -I./spmv -I./mt-vvadd -I./mt-matmul ./median/median_main.c -o median_main.o
riscv64-unknown-elf-gcc -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -DPREALLOCATE=1 -DHOST_DEBUG=0 \
             -c -I./../env -I./common -I./median -I./qsort -I./rsort -I./towers -I./vvadd -I./multiply -I./mm -I./dhrystone -I./spmv -I./mt-vvadd -I./mt-matmul ./median/median.c -o median.o
riscv64-unknown-elf-gcc -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -DPREALLOCATE=1 -DHOST_DEBUG=0 \
             -c -I./../env -I./common -I./median -I./qsort -I./rsort -I./towers -I./vvadd -I./multiply -I./mm -I./dhrystone -I./spmv -I./mt-vvadd -I./mt-matmul ./common/syscalls.c -o syscalls.o
riscv64-unknown-elf-gcc -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -DPREALLOCATE=1 -DHOST_DEBUG=0 -D__ASSEMBLY__=1 \
             -c -I./../env -I./common -I./median -I./qsort -I./rsort -I./towers -I./vvadd -I./multiply -I./mm -I./dhrystone -I./spmv -I./mt-vvadd -I./mt-matmul ./common/crt.S -o crt.o
riscv64-unknown-elf-gcc -T ./common/test.ld -I./../env -I./common -I./median -I./qsort -I./rsort -I./towers -I./vvadd -I./multiply -I./mm -I./dhrystone -I./spmv -I./mt-vvadd -I./mt-matmul  median_main.o  median.o  syscalls.o  crt.o -o median.riscv -nostdlib -nostartfiles
 -ffast-math -lgcc
riscv64-unknown-elf-objdump --disassemble-all --disassemble-zeroes --section=.text --section=.text.startup --section=.data median.riscv > median.riscv.dump
...

これは、makeのログの中からmedian.riscvをコンパイルするために必要なログを抜き出したものだ。これらを見ていくと、以下のような特徴が分かる。

  • 各ソースのコンパイルにはgccを利用する。また、いくつかのプションを指定している。
  • 共通ライブラリとして、syscalls.cとcrt.Sを利用している。また、crt.Sのコンパイルにもgccを利用している(これはおそらく内部でマクロを使用しているため、cppを通過させたい意図があると思われる)。
  • リンクにもgccを利用している。またリンカスクリプトとして./common/test.ldを使用しており、crtにはcrt.Sを使用し、デフォルトのcrtは利用しない(これは-nostartfilesを追加していることから分かる)。

このオブジェクトファイルをダンプしたのが、median.riscv.dumpだ。これを見てみよう。

less median.riscv.dump

median.riscv:     file format elf64-littleriscv


Disassembly of section .text:

0000000080001048 <median>:
    80001048:   00251793                slli    a5,a0,0x2
    8000104c:   00f607b3                add     a5,a2,a5
    80001050:   00062023                sw      zero,0(a2)
    80001054:   fe07ae23                sw      zero,-4(a5)
    80001058:   00200793                li      a5,2
    8000105c:   06a7d263                ble     a0,a5,800010c0 <median+0x78>
    80001060:   ffd5071b                addiw   a4,a0,-3
    80001064:   02071713                slli    a4,a4,0x20
    80001068:   02075713                srli    a4,a4,0x20
    8000106c:   00270713                addi    a4,a4,2
    80001070:   00271713                slli    a4,a4,0x2
    80001074:   00460793                addi    a5,a2,4
    80001078:   00e60633                add     a2,a2,a4
    8000107c:   01c0006f                j       80001098 <median+0x50>
    80001080:   02a74863                blt     a4,a0,800010b0 <median+0x68>
    80001084:   04d54063                blt     a0,a3,800010c4 <median+0x7c>
    80001088:   00a7a023                sw      a0,0(a5)
    8000108c:   00478793                addi    a5,a5,4
    80001090:   00458593                addi    a1,a1,4
    80001094:   02f60663                beq     a2,a5,800010c0 <median+0x78>
    80001098:   0005a683                lw      a3,0(a1)
...

テキスト領域は8000_0000から始まっているようだ。これは./common/test.ldによって制御されている。

さて、ここまで分かれば実装の方針は分かってきた。Coremarkのコンパイルを実行してみよう。

2. RocketChip用Coremarkのコンパイル

まず、Coremarkのアーカイブを展開する。本当はいつも私が利用しているMIPS移植用のコンパイルセットを移植しても良いのだが、ここは一からRISC-V用Coremarkの環境を構築してみよう。

RISC-V Rocket用のコンパイルディレクトリを用意する

まず、ベースとなるコンフィグレーションをコピーして、RocketChip用に改造しよう。今回はbarebonesのディレクトリから移植する。

cp barebones barebones_rocket_m64

ちなみに、Coremarkをコンパイルするときは、どのコンフィグレーションを利用するか指定しなければならない。cleanの時も必要なので注意。

make PORT_DIR=barebones_rocket_m64 clean
make PORT_DIR=barebones_rocket_m64

barebones_rocket_m64/core_portme.makの改造

core_portme.makには、コンパイル時のオプションや定義などを指定する領域だ。上記のテストパタンのコンパイル手順を見るに、gas,ldなどを使わずに、全てgccを使った方が良さそうだ。 以下のようにcore_portme.makを変更する。

2a3,6
> GCC_TARGET=riscv64-unknown-elf
> MACHINE_TARGET=
>
8c12
< CC            = gcc
---
> CC            = $(GCC_TARGET)-gcc
11c15
< LD            = gld
---
> LD            = $(GCC_TARGET)-gcc
14c18
< AS            = gas
---
> AS            = $(GCC_TARGET)-gcc
17c21,22
< PORT_CFLAGS = -O0 -g
---
> PORT_CFLAGS = -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -DPREALLOCATE=1 -DHOST_DEBUG=0 -D__riscv_xlen=64
>
27,28c32,33
< LFLAGS        =
< ASFLAGS =
---
> LFLAGS        = -nostartfiles -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -T$(PORT_DIR)/test.ld
> ASFLAGS = -c -D__riscv_xlen=64 $(MACHINE_TARGET) -I$(PORT_DIR) -I.

ソースコードの修正

#errorで始まる場所は不要だし、面倒なのでとりあえずコメントアウトした。

barebones_rocket_m64/core_portme.c
34:  // #error "You must implement a method to measure time in barebones_clock()! This function should return current time.\n"
101:  // #error "Call board initialization routines in portable init (if needed), in particular initialize UART!\n"

barebones_rocket_m64/ee_printf.c
581:  // #error "You must implement the method uart_send_char to use this file!\n";

ユーテリティファイルの移植

RocketChipでシミュレーションを行うためには、crt.S(スタートアップファイル)とsyscalls.c、util.hをビルドディレクトリにコピーする。以下のファイルをbarebones_rocket_m64ディレクトリにコピーした。

  • ./riscv-tools/riscv-tests/benchmarks/common/crt.S
  • ./riscv-tools/riscv-tests/benchmarks/common/syscalls.c
  • ./riscv-tools/riscv-tests/benchmarks/common/util.h
  • ./riscv-tools/riscv-tests/benchmarks/common/test.ld

さらに、コンパイル対象に上記のファイルを追加するために、core_portme.makを変更する。crt.Sをコンパイル対象にするために、%.s%.Sに変更しているが、これはcrt.Scrt.sに変更しても良い気がする。

> # Flag: PORT_OBJS
> # Port specific object files can be added here
> PORT_OBJS = $(PORT_DIR)/core_portme$(OEXT) $(PORT_DIR)/ee_printf$(OEXT) $(PORT_DIR)/syscalls$(OEXT) $(PORT_DIR)/crt$(OEXT)
> PORT_CLEAN = $(PORT_DIR)/*$(OEXT) *$(OEXT)
>
36c46,47
< PORT_SRCS = $(PORT_DIR)/core_portme.c $(PORT_DIR)/ee_printf.c
---
> # PORT_SRCS = $(PORT_DIR)/core_portme.c $(PORT_DIR)/ee_printf.c
> PORT_SRCS = $(PORT_DIR)/core_portme.c $(PORT_DIR)/ee_printf.c $(PORT_DIR)/syscalls.c $(PORT_DIR)/crt.S
38c49
< vpath %.s $(PORT_DIR)
---
> vpath %.S $(PORT_DIR)
58c69
< $(OPATH)$(PORT_DIR)/%$(OEXT) : %.s
---
> $(OPATH)$(PORT_DIR)/%$(OEXT) : %.S
65c76,81
< port_pre% port_post% :
---
> port_pre% :
>
> port_postbuild :
>       $(GCC_TARGET)-objdump -D coremark$(EXE) > coremark.dmp
>       $(GCC_TARGET)-objcopy -F srec coremark$(EXE) coremark.srec
>       $(GCC_TARGET)-nm coremark$(EXE) > coremark.nm

最後にtest.ldの内容を変更する。crt.oの場所が違うのでこれを書き換えるだけだ。

25c25
<   .text.init : { crt.o(.text) }
---
>   .text.init : { ./barebones_rocket_m64/crt.o(.text) }
51c51
<     crt.o(.tdata.begin)
---
>     ./barebones_rocket_m64/crt.o(.tdata.begin)
53c53
<     crt.o(.tdata.end)
---
>     ./barebones_rocket_m64/crt.o(.tdata.end)
58c58
<     crt.o(.tbss.end)
---
>     ./barebones_rocket_m64/crt.o(.tbss.end)

3. コンパイルおよびバイナリファイルの確認

下記でコンパイル可能だ。また、同時にcoremark.dmp, coremark.nm, coremark.srec も出力されるように細工をしている。また、通常はイタレーション数を十分取って計測するのだが、今回はRTLシミュレーションであるので、短く終了させるためにITERATIONS=1を設定する。

make ITERATIONS=1 PORT_DIR=barebones_rocket_m64

計測ポイントはどこか?

Coremarkのスコアは、関数start_time()からstop_time()までの時間を計測すれば良い。このアドレスはどこだろう?あらかじめ調べておこう。

grep -e start_time$ -e stop_time$ coremark.nm
0000000080002a1c T start_time
0000000080002a28 T stop_time

4. RocketChipのRTLシミュレーションを実行する

続いて、RocketChipのディレクトリに戻ってRTLシミュレーションを実行してみよう。rocket-chip/emulatorに移り、outputディレクトリにCoremarkを登録しよう。

以下のように、Coremarkのバイナリのリンクを張っておく。

~/rocket-chip/emulator/output$ ls -lt coremark.rocket
lrwxrwxrwx 1 vagrant vagrant 64 Dec 13 12:29 coremark.rocket -> /home/vagrant/work/benchmarks/archive/coremark_v1.0/coremark.bin

また、デフォルトではシミュレーション時間が長すぎるので、細工をする。

diff --git a/Makefrag b/Makefrag
index 4a64558..1729917 100644
--- a/Makefrag
+++ b/Makefrag
@@ -35,7 +35,8 @@ ifneq ($(which_disasm),)
        disasm := 3>&1 1>&2 2>&3 | $(which_disasm) $(DISASM_EXTENSION) >
 endif

-timeout_cycles = 100000000
+# timeout_cycles = 100000000
+timeout_cycles = 1000000

emulatorディレクトリに戻り、シミュレーションを実行しよう。

make output/coremark.rocket.out
./emulator-rocketchip-DefaultConfig +max-cycles=1000000 +verbose output/coremark.rocket 3>&1 1>&2 2>&3 | /home/vagrant/riscv/bin//spike-dasm  > output/coremark.rocket.out && [ $PIPESTATUS -eq 0 ]

f:id:msyksphinz:20161213222147p:plain

無事に終了したようだ。計測ポイントを見る前に、有効な命令のみ抽出したい。このためには、ログ内にある[1]が付加されている行だけ抽出しよう。そして、ログ中からstart_time()stop_time()の場所を抽出した。

grep "\[1\] pc" output/coremark.rocket.out > output/coremark.rocket.inst
grep -e "\[0080002a1c\]" -e "\[0080002a28\]" output/coremark.rocket.inst
C                   0:     236739 [1] pc=[0080002a1c] W[r15=0000000080005a1c][1] R[r 0=0000000000000000] R[r 0=0000000000000000] inst=[00003797] auipc   a5, 0x3
C                   0:     718511 [1] pc=[0080002a28] W[r15=0000000080005a28][1] R[r 0=0000000000000000] R[r 0=0000000000000000] inst=[00003797] auipc   a5, 0x3

5. 計測結果からCoremark値を計算する

Coremarkのスコアの計算方法は、基本的にある特定時間内に何回Iterationを繰替えすことが出来るかということを測定している。つまり、Higher is Betterの関係だ。

今回は1回だけIterationを繰替えした時のサイクル数を計測したので、その逆数で分かるはずだ。/MHzということで、1000000を掛ける。

{ \displaystyle
\text{CMK/MHz} = 1000000 / \left(718511 - 236739 \right) = 2.08
}

カタログスペックは?

BOOMの資料によると、RocketChipのCoremark/MHzは2.32だそうだ。少し大きな誤差があるなあ。

  • The Berkeley Out-of-Order Machine (BOOM): An IndustryCompetitive, Synthesizable, Parameterized RISC-V Processor

http://digitalassets.lib.berkeley.edu/techreports/ucb/text/EECS-2015-167.pdf

6. おまけ

ちなみに、ITERATIONS=5で計測した結果がこちら。

make ITERATIONS=5 PORT_DIR=barebones_rocket_m64

...

grep -e "\[0080002a1c\]" -e "\[0080002a28\]" output/coremark.rocket.inst
C                   0:     237504 [1] pc=[0080002a1c] W[r15=0000000080005a1c][1] R[r 0=0000000000000000] R[r 0=0000000000000000] inst=[00003797] auipc   a5, 0x3
C                   0:    2643785 [1] pc=[0080002a28] W[r15=0000000080005a28][1] R[r 0=0000000000000000] R[r 0=0000000000000000] inst=[00003797] auipc   a5, 0x3

Coremark/MHzとしてはITERATIONS=1とほぼ一緒だなあ。