自作CPU & 自作OSをやっていく (3) - riscv/riscv-tests の挙動を追う
2020年1月から、趣味エンジニアリング活動として自作CPUと自作OSをやっていく。
今回は、自作CPUのパフォーマンスベンチマークとして利用するつもりの riscv-tests の挙動を追ってみる。
関心があるのは、命令セットやOSの機能をどこまで用意してあげればベンチマークが実行できるのかという点。とりわけ、以下の観点をチェックしていく。
- ISA (命令セット)
- RV64F (単精度浮動小数点演算), RV64D (倍精度浮動小数点演算) を利用しているベンチマークはあるか。あるとしたらどれか。
- RV64V (ベクトル演算) を利用しているベンチマークはあるか。あるとしたらどれか。
- RV64A (アトミック命令) を利用しているベンチマークはあるか。あるとしたらどれか。
- OS機能
- ヒープ領域は必要か(スタック領域のみで十分か)。
- スレッドをCPUコアに割り当てるスケジューラは必要か。
自作CPU & 自作OS タグで、この前後の進捗とか目指しているもの(初回記事)とかを追えるようにしている。
目次
- riscv-tests とは
- riscv-tests を Spike シミュレータで実行する
benchmarks/vvadd
(ベクトルの加算; シングルスレッド)の挙動を追うbenchmarks/mt-vvadd
(ベクトルの加算; マルチスレッド)の挙動を追う- 機能調査
riscv-tests とは
https://github.com/riscv/riscv-tests に公開されている、RISC-Vのテスト・ベンチマーク群です。
RISC-Vプロジェクトの本拠地UC Berkleyグループが作成しているもので、コミット履歴を見る限りは、「開発自体は落ち着いたが継続的にメンテナンスはされている」というステータスであるように見えます。
RISC-Vなプロセッサやシミュレータを作る人とっては重宝するのではないでしょうか。
ディレクトリ構造概説: riscv-tests/isa
重要
RISC-Vの各命令の単体テスト。アセンブリと便利なプリプロセッサマクロで書かれている。
サブディレクトリは rv64ui
(RV64I, 動作モードはUser) のように区切られており、所望の命令のテストファイルが探しやすくなっている。
ディレクトリ構造概説: riscv-tests/benchmarks
重要
ベンチマークプログラム。比較的シンプルなアプリケーションの集合。以下のものが含まれる:
- dhrystone: Dhrystone。整数演算を中心とした合成ベンチマーク。
- median: 1次元配列に対するメディアンフィルタ。画像のノイズ除去などに使われるアルゴリズムで、「ある要素とその両隣の3要素の中央値を、ある要素に上書きする」という挙動をするもの。
- mm: シングルスレッドの行列積。
- 結果出力は物理スレッドのIDを伴うが、データ同じ行列積を各物理スレッドで行っているだけなので注意。
- カーネル部分は性能が出るようにチューニングされている。
- mt-matmul: マルチスレッドの行列積。
- 並列計算をしている。上流で \(A \times B = C\) の \(A\) を物理スレッドの数だけ分割している。
- ただし、 各ベンチマークは
benchmarks/common/crt.S
をいじってビルドしないとシングルスレッド動作してしまうので要注意。 ベンチマークが標準でマルチスレッド予定があるかは “Benchmark runs in single-thread” のIssue で確認中。
- ただし、 各ベンチマークは
- シングルスレッドのカーネル部分は単純な3重ループで性能は出なさそう。
- 行列積、シングルスレッド版とマルチスレッド版があるのは良いのだが、カーネル部分があまりにも異なるのはちょっと…
- 並列計算をしている。上流で \(A \times B = C\) の \(A\) を物理スレッドの数だけ分割している。
- mt-vvadd: ベクトルの加算。
コアID mod コア数
番目の要素の足し算を各コアが担当する、単純な並列化が成されている。 - multiply: ハードウェアの32ビット乗算器をエミュレートしたようなプログラム。
- pmp: PMP (Physical Memory Protection; 物理メモリ保護) のテスト。ベンチマークではなくテスト。
- qsort: クイックソート。
- rsort: 基数ソート。
- コメントにはクイックソートと書かれているが.際のコードはどう見ても基数ソート。Issueでも指摘されている。
- spmv: 倍精度浮動小数の疎行列・ベクトル積。
- towers: ハノイの塔。
- vvadd: ベクトルの加算。シングルスレッド。
ディレクトリ構造概説: riscv-tests/env
重要
サブモジュール。riscv-tests/isa
が実行可能ファイルを作るためのリンカスクリプトとエントリポイント用のアセンブリや、 memcpy
などのユーティリティ関数が含まれている。
ディレクトリ構造概説: riscv-tests/mt
(たぶん)重要じゃない
マルチスレッドの行列積やベクトル和のプログラムがたくさんあるが、 コミット履歴 を見る感じ、ガッとどこか別のプロジェクトから引っ張ってきて2016年にメンテが途絶えている。
ディレクトリ構造概説: riscv-tests/debug
(たぶん)重要じゃない
あんまりしっかり見ていないが、 riscv-tests/isa
や riscv-tests/benchmarks
のプログラム自体をデバッグするための諸々に見える。
riscv-tests を Spike シミュレータで実行する
ISAやOSで特定機能のサポートをする必要があるかを調査するのが目的なので、いきなりコードの静的解析に入っても良いのですが、せっかくなら動かしてみましょう。
といってもRISC-Vなプロセッサの実機もまだ持ってない(これから作る)し、普段遣いのPCは x86_64 なので、RISC-Vのシミュレータの Spike を使います。
※QEMUでもRISC-Vのシミュレーションはできるはずですが、自分はQEMUでriscv-testsを動作させることはできませんでした…
Spike とRISC-V用のコンパイラツールチェインをDockerで用意する
ホストマシンの環境差異に悩まされたくないのでDockerを使います。Dockerfileは
に置いてあるものを使います。
riscv-tests リポジトリはコンテナの中でcloneしても良いですが、実行結果をファイルにまとめてホストと共有したりすると便利なので、ホストでcloneします。
1 | # riscv-tests のclone |
1 | # spike コマンドの確認 |
riscv-tests のビルド
1 | cd /riscv-tests |
benchmarks
(ベンチマーク)の実行
/riscv-tests/target/share/riscv-tests/benchmarks/*.riscv
が、RV64アーキテクチャの実行可能なELFファイルです。
1 | cd /riscv-tests/target/share/riscv-tests/ |
パフォーマンスカウンタの値(mcycle
: サイクル数, minstret
: 実行された命令数)がコンソール出力されています。
isa
(命令セットが正しく実装されているかの単体テスト)の実行
1 | cd /riscv-tests/target/share/riscv-tests/ |
成功実行のときは何もコンソール出力されません。
テストコードを改変すると失敗時の出力が見られます(試す必要は特にないです)。
1 | vim riscv-tests/isa/rv64ui/add.S |
1 | cd /riscv-tests |
benchmarks/vvadd
(ベクトルの加算; シングルスレッド)の挙動を追う
各ベンチマークの挙動を性格に把握できるようにするために、動作の最初から最後まで愚直にコードを読んでみます。vvadd
と mt-vvadd
はそれぞれシングルスレッドとマルチスレッドで、行っている計算もシンプルなので、ちょうどよい題材として本記事で取り上げます。まずは vvadd
を読みます。
Spikeにより、 0x80000000
番地に配置された .text.init
セクションのコードが実行されます。そのコードは https://github.com/riscv/riscv-tests/blob/3a98ec2e306938cce07ab15e3678d670611aa66d/benchmarks/common/crt.S#L15-L136 のもの。以下、インラインコメントの形で挙動を解説します。
1 | # .text.init セクションは、 |
マルチコアにおけるスタック領域が、コア番号が大きいほど大きくなるバグがありましたが、PRをマージしてもらって直りました😉
crt.S
では最後に _init
にジャンプしました。この _init
は https://github.com/riscv/riscv-tests/blob/3a98ec2e306938cce07ab15e3678d670611aa66d/benchmarks/common/syscalls.c#L106-L123 で定義されています。
1 | void _init(int cid, int nc) // cid = CPUコアID, nc = 1 |
まずは init_tls()
の定義を見てみます。名前からしてTLS (スレッドローカル変数) を初期化していそうですね。https://github.com/riscv/riscv-tests/blob/3a98ec2e306938cce07ab15e3678d670611aa66d/benchmarks/common/syscalls.c#L96-L104 に定義があります。
1 | static void init_tls() |
各コアのTLSに、 .tdata
(初期値を持つ読み書き可能なデータ) と .tbss
(ゼロ初期化された読み書き可能なデータ) を配置しているのがわかりました。
_init()
の処理は次に thread_entry()
を呼び出します。シングルスレッド動作する vvadd
においては、 https://github.com/riscv/riscv-tests/blob/3a98ec2e306938cce07ab15e3678d670611aa66d/benchmarks/common/syscalls.c#L82-L87 のデフォルト定義が使われます。
1 | // GNU拡張を使って weak シンボルとして thread_entry のデフォルト定義が与えられている。 |
シングルスレッドな vvadd
においては、コア0だけが _init
の処理を進め、 int ret = main(0, 0);
を呼び出します。 main()
の中身は後で見ましょう。 _init()
の残りをインラインコメントで解説します。
1 | void _init(int cid, int nc) // cid = CPUコアID, nc = 1 |
パフォーマンスカウンタ mcycle
, minstret
の値をコンソールに文字出力して、 exit(main関数の返り値)
を呼び出して終了しています。exit()
が最終的に行き着く tohost_exit()
は興味深いので実装を見てみます。
1 | void __attribute__((noreturn)) tohost_exit(uintptr_t code) |
これでどうしてベンチマークプログラムの実行が終わるのでしょうか?
これはSpikeの定めている HTIF (Host-Target Interface によるものです。特定の番地の64ビット符号なし整数に0以外の値が書き込まれていたら、Host側のSpikeはTarget側のベンチマークプログラムに何らかの介入をします。ターゲット側からみたら、 tohost
がSpikeに対する連絡手段となるのです。
Target側が無限ループしているのにHost側に制御が移る理由があまりわかっていないのですが、おそらくSpikeはタイマ割り込みはいつでも発生するように作っているのだと思います。タイ回り込みの処理において tohost
領域の値をチェックして、非ゼロの場合にTargetプログラムを終了させる挙動かと推察します。
このあたりは確信できるドキュメントなど見つからなかったので、詳しい方は教えていただけると嬉しいです。
tohost
の番地は .tohost
セクションの番地から取得できるように https://github.com/riscv/riscv-tests/blob/3a98ec2e306938cce07ab15e3678d670611aa66d/benchmarks/common/test.ld#L29 において設定されています。
ここまでで main()
周辺の仕組みが完全にわかったので、 main()
を読んでみます。
1 | void vvadd( int n, int a[], int b[], int c[] ) |
特筆すべきことはないですね。 PREALLOCATE
をコンパイル時にセットしておくと、パフォーマンスカウンタ計測の前に予め入力ベクトルを舐めてキャッシュに乗せるようです。
benchmarks/mt-vvadd
(ベクトルの加算; マルチスレッド)の挙動を追う
シングルスレッド版 vvadd
との違いはわずかです。 mt-vvadd
は thread_entry
関数を自前で定義しているので、すべてのコアがこの関数をエントリポイントとして実行することができます(実際には crt.S
でコア0以外は無限ループに嵌められていますが…)。
mt-vvadd
の thread_entry
は https://github.com/riscv/riscv-tests/blob/3a98ec2e306938cce07ab15e3678d670611aa66d/benchmarks/mt-vvadd/mt-vvadd.c#L47-L78 に定義があります。息切れしてきたので解説は省略します🙄
計算のコアの vvadd()
は https://github.com/riscv/riscv-tests/blob/3a98ec2e306938cce07ab15e3678d670611aa66d/benchmarks/mt-vvadd/vvadd.c#L9-L18 に定義があります。コアID mod コア数
番目の要素の足し算を各コアが担当していることがわかります。
機能調査
ベンチマークプログラムのコードがカーネル部分も周辺部分も読めるようになったので、自作CPU, OSで備えるべき機能を検討するために以下の観点を調べます。
RV64F (単精度浮動小数点演算), RV64D (倍精度浮動小数点演算) を利用しているベンチマークはあるか。あるとしたらどれか。
float
型または double
型を使っているかが肝になります。たとえ使っていたとしても、RV64Iの範囲で整数レジスタを使ってソフトウェア的に浮動小数点演算をするコードをコンパイラに吐いてもらうこともできますが、やはりハードウェア側でRV64F, RV64DのISAに対応しておいてFPUをハードウェアで作っておいたほうが圧倒的に速度が出るので、 float
や double
型を使っているベンチマークがあれば自作CPUはRV64F, RV64D対応したくなります。
mm, spmv は double
型を使っていて、それ以外はなさそうです。
RV64V (ベクトル演算) を利用しているベンチマークはあるか。あるとしたらどれか。
RV64Vはまだドラフト段階であり、調べている限り、RISC-Vでのベクトル演算アセンブリを吐くためのコンパイライントリンシックは今の所なさそうです。
となると直接アセンブリでベクトル命令を書いているベンチマークがあるかどうかが調査ポイントですが、 riscv-tests/benchmarks の中ではアセンブリを書いてなさそうです。
ただし、 mm は標準ライブラリの fmaf
を読んでいる形跡があります。
標準ライブラリが絡んでくるとコンパイル済みのアセンブリを見たほうが調査精度が高そうです。幸い riscv-tests/benchmarks は mm.riscv
などの実行可能ファイルだけではなく mm.riscv.dump
のようなアセンブリファイルも出力してくれるので、 *.dump
ファイルを対象にベクトル命令をgrepしてみます。
1 | cd /riscv-tests/target/share/riscv-tests/benchmarks |
どうやらどれもベクトル命令は吐いてなさそうです。
RV64A (アトミック命令) を利用しているベンチマークはあるか。あるとしたらどれか。
1 | % grep '\slr\s' *.dump |
アトミック命令が mm, mt-matmul, mt-vvadd, qsort, rsort で使われていますね。
ヒープ領域は必要か(スタック領域のみで十分か)。
ヒープの有無はリンカスクリプトからはわかりません。
ヒープがあるとしたら、 .data
や .bss
セクションの直後(高位)の領域をベースアドレス配置されるのが通常です。
(スタックはヒープから思い切り離れたところにそのトップアドレスを配置し、ヒープは高位へ、スタックは低位へ伸びていくのが慣例ですね)
ヒープはOSがシステムコールの形で動的な確保と開放をサポートします。Linuxにおけるヒープ操作のためのシステムコールは brk(2)
ですね。
riscv-tests は動作に特定のOSを必要としていない組み込みプログラムなので、ヒープ操作のシステムコール必要としておらず、呼び出していません。
したがってヒープ領域は不要です。
(一部のベンチマークプログラムは alloca
を呼び出していますが、これはスタック領域を動的に伸ばすための関数です)`
スレッドをCPUコアに割り当てるスケジューラは必要か。
コードを追いながら見たように、
crt.S
においてコアID0 以外は無限ループでストップするようになっている。- 仮に
crt.S
の上記制約がなくなったとしても、マルチスレッドのベンチマークの mt-matmul と mt-vvadd は、どのコアも同じ命令の実行を行っている(各コアが扱う入力データが分割されている、いわゆるデータ並列)。- 自分のコアの計算が早く終わった場合にも
barrier()
するだけで、ロードバランシングのためにスケジューラに制御を移したりはしない。
- 自分のコアの計算が早く終わった場合にも
という状況なので、スケジューラは不要です。
まとめ表
dhrystone | median | mm | mt-matmul | mt-vvadd | multiply | pmp | qsort | rsort | spmv | towers | vvadd | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
RV64F要否 | x | x | x | x | x | x | x | x | x | x | x | x |
RV64D要否 | x | x | o | x | x | x | x | x | x | o | x | x |
RV64A要否 | x | x | o | o | o | x | x | o | o | x | x | x |
RV64V要否 | x | x | x | x | x | x | x | x | x | x | x | x |
ヒープ要否 | x | x | x | x | x | x | x | x | x | x | x | x |
スケジューラ要否 | x | x | x | x | x | x | x | x | x | x | x | x |