少しRISC-VのVector Extensionについて調査した。
Vector Extensionは、いわゆるベクトル命令だ。1命令で複数のデータを扱う方法といえば、SIMD(Single Instruction Multiple Data)もあるが、RISC-Vではまずはベクトル命令が定義された。現代のアーキテクチャでは、ベクトル拡張よりもSIMD拡張が一般的だと思うが、RISC-Vではまずはベクトル拡張が定義されている。これはV拡張といわれ、MISAレジスタのビットではVビットで規定されている。ベクトル命令のエンコーディングはまだ規定されていない。いわゆるSIMD命令は、ベクトルレジスタのサイズは命令の中に組み込まれているが、ベクトル命令では、実装形態によってベクトルレジスタのサイズが決まる。
RISC-Vのベクトル拡張仕様は、日進月歩で改定が進んでいる。RISC-V User-Level ISA V2.2をもとに使用の概要を説明するが、この仕様が現在の公式だが、常にアップデートが行われており、現在はGitHub上で議論が行われている。ベクトル拡張についてより深く学びたいという型は、RISC-V Vector Extensionの仕様書を作成しているGitHubのリポジトリを参照すること。
RISC-Vのベクトル拡張では少し面白い仕様が取り込まれている。「動的レジスタ・タイピング」(dynamic type field)という機能によって、ベクトルレジスタが取り扱える型は動的に決定される仕様になっている。これは、例えばベクトルレジスタ0は整数型、ベクトルレジスタ1は単精度浮動小数点型、というように、ベクトルレジスタ毎にデータの型を指定できるようになる。この方式のメリットは、ベクトル命令と、ベクトル命令の取り扱えるデータ型を分離する、と言う事だ。たとえば、通常のSIMD命令であれば、
というように、命令に対してデータ型と演算幅が決まる。これに対して、RISC-Vのベクトル拡張では、命令にデータ型が付属しているのではなく、レジスタに対してデータ型が付随している。
RISC-Vのベクトル拡張を有効にすると、以下のレジスタが有効になる。
これを実現するために、RISC-Vのベクトル拡張では、以下に示したCSRが追加された。
| CSR名 | 数 | ベースISA | 説明 |
|---|---|---|---|
| vl | 0x020 | RV32, RV64, RV128 | ベクトル長を示す。 |
| vxrm | 0x020 | RV32, RV64, RV128 | 浮動小数点演算命令における丸めモード |
| vxsat | 0x020 | RV32, RV64, RV128 | 固定小数点演算命令におけるSaturation状態を示す。 |
| vcsr | 0x020 | RV32, RV64, RV128 | |
| vcnpred | 0x020 | RV32, RV64, RV128 | 有効なプレディケートレジスタの数を示す。0から8までの値が格納される。 |
| vcmaxw | 0x020 | RV32, RV64, RV128 | ベクトルレジスタがサポートできる最大ビットサイズを示す。4×32=128ビットが定義されている。 |
| vcmaxw1 | 0x020 | RV32 | RV32において、vcmaxwの[63:32]を参照するためのレジスタ。 |
| vcmaxw2 | 0x020 | RV32, RV64 | RV32において、vcmaxwの[95:64]を参照するためのレジスタ。RV64において、vcmaxwの[127:64]を参照するためのレジスタ。 |
| vcmaxw3 | 0x020 | RV32 | RV32において、vcmaxwの[127:96]を参照するためのレジスタ。 |
| vctype | 0x020 | RV32, RV64, RV128 | ベクトルレジスタが格納しているデータ型を示す。4×32=128ビットが定義されている。 |
| vctype1 | 0x020 | RV32 | RV32において、vctypeの[63:32]を参照するためのレジスタ。 |
| vctype2 | 0x020 | RV32, RV64 | RV32において、vctypeの[95:64]を参照するためのレジスタ。RV64において、vctypeの[127:64]を参照するためのレジスタ。 |
| vctype3 | 0x020 | RV32 | RV32において、vctypeの[127:96]を参照するためのレジスタ。 |
| vctypev0 | 0x020 | RV32, RV64, RV128 | |
| vctypev1 | 0x020 | RV32, RV64, RV128 | |
| ... | 0x020 | RV32, RV64, RV128 | |
| vctypev31 | 0x020 | RV32, RV64, RV128 |
これらを使用して、ベクトル拡張では「最大ベクトル長(maximum vector length: MVL)」という値が定義される。
vlレジスタは、ベクトル拡張の「アクティブベクトル長(active vector length)」を格納する。これは、計算ターゲットとなるベクトルレジスタの長さを指定する。このvlレジスタへは、専用命令setvlを使って格納する。setvlレジスタは、MVLよりも大きな値を設定することはできない。vlはMVLよりも大きな値を設定しようとすると、自動的にトランケートしてMVLよりも小さな値に抑える。AVLの値が2$$\times$$MVLよりも小さな場合が少し特殊だが、これはベクトル演算のパイプライン化により性能を最大限に引き出すための工夫だ。
vcmaxwのフィールドは、ベクトルレジスタに格納できる最大データ幅を示する。これは、以下の表に従って設定される。例えば、ベクトルレジスタでサポートされるデータが最大で64ビットならば、vcmaxwは1011となる。vcmaxwは4ビットのフィールドがベクトルレジスタの32個分、つまり128ビット用意する必要がある。RV128ならば128ビットのデータに対して一度に参照できるのだが、RV64とRV32ではそうもいかない。
そこで、RV64ではvcmaxw2レジスタ、さらにRV32のためにvcmaxw1,vcmaxw2, vcmaxw3レジスタが用意されている。
| 幅 | エンコーディング |
|---|---|
| 無効 | 0000 |
| 8 | 1000 |
| 16 | 1001 |
| 32 | 1010 |
| 64 | 1011 |
| 128 | 1100 |
なぜこのようなエンコーディングになっているかというと、次に登場するCSRのvctypeとのつじつまを合わせるためだ。各ベクトルレジスタには、4ビット幅のvctypeレジスタが付属している。このCSRは各ベクトルレジスタがどのようなサイズのデータを格納しているかを示しているので、vcmaxwレジスタとは異なり、明確に整数型と浮動小数点型を区別する。
vctypeレジスタのエンコーディングが、整数型を取り扱う時は最上位ビットが1、浮動小数点型を取り扱う時は最上位ビットを0に設定する。この2つのレジスタの関係は、常にvctypeのレジスタがvcmaxwレジスタの値よりも小さくなければならない。
さらに、プレディケートレジスタについて説明しておく。これは、ベクトルレジスタのマスクをするためのレジスタだ。このプレディケートレジスタを使用することで、ベクトルレジスタの中で処理を適用するエレメントと、そうでないエレメントを自由に制御することができるようになる。
vcnpredレジスタは、プレディケートレジスタが何本使用できるかを指定する。プレディケートレジスタは、前述の通りvp0からvp7まで8本存在するが、vpnpredは0から8までの値を取ることがでく。0を書き込むと全てのプレディケートレジスタを使用できなくなる。一方で8以上の値を書き込むと不正命令例外が発生する。
さらに、これまで説明したベクトルレジスタの設定をより高速に実行するための命令が存在する。これはvcfgd命令で、csrrw命令の一種なのだが、どのベクトルレジスタをどのデータ型に割り当てるのかを一気に設定することができる。
ベクトル命令の例
最後に、ベクトルレジスタをどのようにして使うのかを例で見てみる。プログラム[refs:vec_daxpy]のコードは、RISC-Vのベクトル拡張を使用して書いたベクトル同士の加算を行うためのコードだ(RISC-V User-Level ISA V2.2より抜粋)。
# 32ビット整数型どうしのベクトル配列の加算コード # ベクトルレジスタは32ビット整数型をサポートするように構成されているものとする。 # a0 : ベクトル長Nを保持している。 # a1 : 加算結果を格納するベクトルのポインタを保持している。 # a2 : 加算するベクトル1つ目のポインタを保持している。 # a3 : 加算するベクトル2つ目のポインタを保持している。 loop: setvl t0, a0 # Nをvlに設定する。t0に現在のvlの値を設定する。 vld v0, a2 # v0に1つ目のベクトルをロードする。 sll t1, t0, 2 # t1はベクトル長Nに相当するバイト数を返す(32ビット整数なので×4) add a2, t1 # a2をロードしたバイト数だけ進める vld v1, a3 # v1に2つ目のベクトルをロードする。 add a3, t1 # a3をロードしたバイト数だけ進める。 vadd v0, v1 # v0とv1を加算し、その結果をv0に格納する。 sub a0, t0 # ベクトル命令で処理した分だけa0の値を減らす。 vst v0, a1 # 加算した結果をメモリにストアする。 add a1, t1 # 結果ベクトルのポインタを進め®う。 bnez a0, loop # ベクトル要素が残っていればループを繰り返す。そうでなければ終了する。
setvl命令により、まず1回のループ(つまり、1回のベクトル命令で処理できるデータの数)が設定される。次に加算対象となるベクトルをv0, v1にロードする。そしてベクトル演算により加算を行い、その結果をメモリに格納する。そしてベクトルサイズを1回のループで実行したデータ数で減算し、残りの処理すべきデータ数を計算する。残りのデータ数が0よりも小さくなるまでこの処理を繰り返して、全てのベクトルが処理されるまで続ける。