FPGA開発日記

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

RISC-V 5th Workshopの発表紹介 (RISC-V Vector Extension)

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

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

ところで、RISC-V 5th Workshopの資料がやっとアップロードされました。今回は、その中でも一番気になっていた、RISC-V Vectore Extensionについて見ていきたいと思います。

1. RISC-V Vector Extensionの歴史

RISC-V のVector Extension v0.1

まず、このVector Extensionについては、今回の5th Workshopが初登場で無いことについて言及しておきたい。過去、2nd RISC-V Workshopにおいて、Vector Extensionの最初のProposalが行われている。

ちなみに、Vector ExtensionはSIMDとは別物と考えたほうが良いかもしれない。これは"V" Extensionだが、別に"P"拡張 Packed Extensionも存在する。

https://riscv.org/wp-content/uploads/2015/06/riscv-vector-workshop-june2015.pdf

次に、既にVector Extensionを実装したプロジェクトとして、Hwachaというプロジェクトがある。Hwachaとは、古代の武器のことで、槍のようなものを同時平行に数十本同時に放つ武器のことだ。 Vector処理ということで、同時平行に演算を実行することからこの名前を付けたらしい。 Google画像検索をすると、恐しい写真が大量に出てくる。

The Hwacha Project

こちらにも、Vector Extensionに関する資料が大量に出てくるので、興味のある人は調査して欲しい。

  • The Hwacha Vector-Fetch Architecture Manual, Version 3.8.1

https://www2.eecs.berkeley.edu/Pubs/TechRpts/2015/EECS-2015-262.pdf

  • The Hwacha Microarchitecture Manual, Version 3.8.1

https://www2.eecs.berkeley.edu/Pubs/TechRpts/2015/EECS-2015-263.pdf

そして今回、5th WorkshopにてRISC-V Vector Extension Proposal v0.2が公開された。

  • Vector Extension Proposal v0.2

https://riscv.org/wp-content/uploads/2016/12/Wed0930-RISC-V-Vectors-Asanovic-UC-Berkeley-SiFive.pdf

2. RISC-V Vector Extension仕様概要

まず、RISC-VのVector命令は、汎用レジスタ浮動小数レジスタ群と分離した専用のベクトルレジスタを持っている。いくつかのシステムレジスタ、さらに最大で8エントリ分のPredicateレジスタで構成されている。

ベクトルレジスタのサイズは、Maximum Vector Length(MVL)によって定義される。これはベクトルレジスタ毎に(おそらく)共通で、物理的に実装されているベクトルレジスタのサイズを表わしている。

f:id:msyksphinz:20161214011632p:plain (5th Workshop Vector Extension資料より抜粋)

これ、ちょっと面白いのだが、Vectorレジスタのサイズは、各レジスタエレメント毎に調整することができる。また、Predicateレジスタの量もコンフィギャラブルだ。最適なサイズを選択できるようなアーキテクチャにしているという訳だ(汎用プロセッサにこの機能がどれだけ必要なのかは分からないが...)

f:id:msyksphinz:20161214011756p:plain (5th Workshop Vector Extension資料より抜粋)

各ベクトルレジスタの設定

各ベクトルレジスタには、vcmaxwとvctypeが設定可能だ。それぞれ、

  • vcmaxw 当該ベクトルレジスタで使用可能な最大エレメントサイズ(最大データタイプとでも言おうか)
  • vctype 当該ベクトルレジスタで現在使用しているエレメントサイズ(現在のデータタイプとでも言おうか)

もちろん、vctypeはvcmaxwよりも大きな値を設定は出来ないだろうし、vcmaxwを設定すると、デフォルトで該当するデータサイズがvctypeに設定されるようだ。

では、このような設定を具体的にどのように行うのであろうか?資料には例が載っていた。

f:id:msyksphinz:20161214013050p:plain (5th Workshop Vector Extension資料より抜粋)

これをパッっと見ただけでは分かりにくいのだが、例えばRV32の場合を見てみると、どうやら若いベクトルレジスタアドレス順に、小さなデータタイプから設定可能なようだ。

  • X32=1 であるため、v0-v1(0は初期値)までは、X32で使用する。
  • F32=12 であるため、v2-v12まではF32で使用する。
  • F64=18 であるため、v13-v18まではF64で使用する。
  • v19以降は使用しない

と解釈できる。つまり、同一データタイプのレジスタをバラバラに配置することは出来ず、若い方から順に配置しなさい、ということか。

たとえば、vcfgdを下記の図のように指定した場合、ベクトルレジスタは下記のように取り扱われる。

f:id:msyksphinz:20161216003155p:plain

3. 実際の演算例

32bit整数同士のVector加算

まず単純な、32bit整数の加算から。ここでは、Way数を4として図を書いてみた。

vcfgd  2*X32 # Only need two vector registers
stripmine:
vsetvl t0, a0 # a0 holds vector length
vld    v0, a1 # Get first vector
vld    v1, a2 # Get second vector
vadd   v1, v0 # Add vectors
vst    v1, a3 # Store result vector
sll    t1,t0,2 # Multiply count by 4 to get byte
add    a1, t1 # Bump pointers
add    a2, t1
add    a3, t1
sub    a0, t0 # Subtract number done
bnez   a0, stripmine # Any more?
vuncfg # Turn off vector unit by zeroing config

vcfgd 2*X32によって、v0-v1までをX32整数として利用することを宣言している。これにより、以下のようなレジスタ配置で演算されることが分かる。 この演算命令を見て分かるように、各命令には特に型が指定されない。どのような型でも、統一した宣言がなされる。

上記の例では、vld, vadd, vstは全てX32の整数として宣言される訳だ。

f:id:msyksphinz:20161214223017p:plain

16bit整数同士のVector加算

次に、16bit整数同士の演算ではどのようになるのか見てみる。

vcfgd  2*X16 # Only need two vector registers
stripmine:
vsetvl t0, a0 # a0 holds vector length
vld    v0, a1 # Get first vector
vld    v1, a2 # Get second vector
vadd   v1, v0 # Add vectors
vst    v1, a3 # Store result vector
sll    t1,t0,1 # Multiply count by 2 to get byte
add    a1, t1 # Bump pointers
add    a2, t1
add    a3, t1
sub    a0, t0 # Subtract number done
bnez   a0, stripmine # Any more?
vuncfg # Turn off vector unit by zeroing config

このソースコードを見て、最初は以下のようなものを想像した。つまり、32bit整数で4-wayなら、16bit整数で8-wayになるというからくりだ。ただ良く考えるとこれは違う。さらに後述する異なる型同士の演算も辻褄が合わなくなる。

f:id:msyksphinz:20161214222340p:plain

f:id:msyksphinz:20161214222959p:plain つまり、vcmaxwによって演算の最大型が決まるのであって、その中で実際にどの型を使うのかによってWay数が増減することは無い。そう考えるのが自然ではないか。そもそも、Way数はMVLで決まっちゃってるしね。

また、この場合もvadd, vld, vstの演算粒度は、vcfgdの設定によって自動的に決まっている。

16bit整数と32bit整数のVector加算

じゃあ最後に、異なる型同士の加算はどのようになるのだろうか。

vcfgd 1*X32|1*X16
stripmine:
vsetvl t0, a0 # a0 holds vector length
vld v0, a1 # Get first 16-bit vector
vld v1, a2 # Get second 32-bit vector
vadd v1, v0 # Add vectors
vst v1, a3 # Store result vector
sll t1,t0,1 # Multiply count by 2 to get byte
sll t2,t0,2 # Multiply count by 4 to get byte
add a1, t1 # Bump pointers
add a2, t2
add a3, t2
sub a0, t0 # Subtract number done
bnez a0, stripmine # Any more?
vuncfg # Turn off vector unit by zeroing config

下記のように考えるのが自然だろう。X16とX32の加算では、型変換や符号の拡張が必要になるため、おそらく混在の演算のバリエーションには制限がある。ただ一方で、簡単にそのような演算が出来るのならば良いのかもしれない(型変換が不要になる?)

f:id:msyksphinz:20161214224449p:plain

4. 考察. 各型で命令名が同一なのは良いこと?

今回のVectorアーキテクチャでは、X16で演算しようがX32で演算しようが、F32だろうがF64だろうが、同一のニーモニックだ。どの型で演算するかは、vcfgdの設定に依存してしまう。

ただし、はたしてこれは命令の解析をする場合に、良いことなのだろうか?命令のシンボルを見ても、ぱっと見ではどの型の演算か分からない訳だ。これはデバッガビリティに重大な影響を与える気がする。

もちろん、命令空間を節約するためには良い方法だ。だが、コンパイル結果を解析したり、チューニングする場合に逆アセンブル結果を見て、任意の命令1つに限定出来ないのは、とても解析がやりづらいように思う。