🐳

RISC-VのVector Extensionをざっくりと理解する

2023/02/22に公開

こちらから仕様書のPDFをダウンロードできます。
https://github.com/riscv/riscv-v-spec/releases

SIMDとベクター演算の違い

SIMDはレジスタのbit長が固定されていて、そのサイズに合わせた命令が定義されています。後からより大きなサイズのSIMDレジスタが追加されたときには、それを使用する命令も同時に追加しなくてはなりません。その新しい大きなサイズのSIMDレジスタを使おうとすると新しいコンパイラで再コンパイルすることが必要になりますし、従来のものと共存するにはコードを振り分ける必要があります。
それに対してベクター演算ではレジスタの幅がシステムによって異なることが織り込まれた仕様になっています。つまり同じ命令列でベクターレジスタのbit長が異なるシステム間でバイナリ互換にできるようになっています。

RISC-V Vector Extention v1.0

v1.0の仕様書を見てざっくりと自分の言葉で整理すると以下のようになります。

Vector register 32本。v0 - v31。そのbit長はシステムごとに異なる固定長を持つ。
bit長はVLEN。VLENはシステムごとの定数。
7つの制御レジスタが追加。

  • vstart, vxsat, vxrm, vscr が読み書き可能。
  • vl, vtype, vlenb がリードオンリー。

vtypeレジスタとvlレジスタへの設定はvset{i}vl{i}命令を使用する。
vlenbレジスタはVLEN/8, つまりベクターレジスタのバイト長を保持する。
vstartレジスタはtrapが発生したときにどこまで実行していたかをハードウェアによって書き込まれる。これによってtrapからの復帰するときに再開するべきところがわかるようになっている。
vxrmはRounding Mode(丸め)の設定。
vxsatは固定小数命令で飽和が生じたことが記録される。
v0はマスクに使われる。現在はv0だけがマスクレジスタとして使用可能だが、将来への拡張のためアセンブラ表記としてはv0に限定していない。

組み込み向けCPUでのベクター拡張

Zve*: Vector Extensions for Embedded Processors

回路規模を小さくおさえたい組み込み向けCPUでのベクター拡張。
最小のVLENが32, 64。
Zve32x, Zve64x: 整数のみサポート
Zve32f, Zve64f: 整数とfp32のみサポート
Zve64d: 整数、fp32, fp64をサポート

これは実行効率を上げるというよりもコードの互換性のためにあるのかもしれません。一度に処理できる個数は少ないけれどもベクターレジスタを使うようにビルドされたライブラリをそのまま使用することができます。
ArmのSVE(Scalable Vector Extension)はなんか仰々しくていかにもスーパーコンピュータ向けという感じですが、RISC-VのVector Extensionはもっとずっとカジュアルなので、数年したらFPUと同じくらいの扱いでさまざまなプロセッサに普通に搭載されるのではないでしょうか。

Arm のScalable Vector Extensionとの比較

こちらの記事が興味深いです。

https://erik-engheim.medium.com/arm-vs-risc-v-vector-extensions-992f201f402f
この記事によると
SVEではFPUとVector registersが共用。
RISC-VではFPUとVector registersは別々。

SVEは命令数が多くて複雑。理解するのが難しい。
RISC-Vはシンプル。ベクター演算の入門に向いている。
Intel のAVXはArmの10倍複雑だそうです。

仕様書に載っているサンプルプログラム

SAXPY: Single-Precision A·X Plus Y

saxpy.c
void
saxpy(size_t n, const float a, const float *x, float *y)
{ 
    size_t i;
    for (i=0; i<n; i++)
        y[i] = a * x[i] + y[i];
} 
# register arguments:
#     a0      n
#     fa0     a
#     a1      x
# a2 y
saxpy:
    vsetvli a4, a0, e32, m8, ta, ma  // a4 <- 一度に処理できる個数
    vle32.v v0, (a1)  // Load x by the size of vector register
    sub a0, a0, a4    // n -= a4
    slli a4, a4, 2     // a4 <- a4 * sizeof(float)
    add a1, a1, a4    // x += a4
    vle32.v v8, (a2)  // Load y by the size of vector register
    vfmacc.vf v8, fa0, v0        // y += a * x
    vse32.v v8, (a2)  // Store y by the size of vector register
    add a2, a2, a4    // y += a4
    bnez a0, saxpy
    ret

vectorレジスタのbit長によって一度に処理できる個数が変わりますが、このコードの自体はそのまま使えますね。これがSIMDとの決定的な違いです。

また、memcpyもベクターレジスタを使って一気に行うことができます。

# void *memcpy(void* dest, const void* src, size_t n)
# a0=dest, a1=src, a2=n
#
memcpy:
    mv a3, a0 # Copy destination
loop:
  vsetvli t0, a2, e8, m8, ta, ma
  vle8.v v0, (a1)
    add a1, a1, t0
    sub a2, a2, t0
  vse8.v v0, (a3)
    add a3, a3, t0
    bnez a2, loop
    ret

ベクター命令以外を一段インデント下げています

clang 16でコンパイルして生成されたコード

$ clang-16 --version
clang version 16.0.0 (https://github.com/llvm/llvm-project.git 434575c026c81319b393f64047025b54e69e24c2)
Target: aarch64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
$ clang-16 -c -march=rv64gcv -target riscv64 -O2 saxpy.c
$ llvm-objdump -dr saxpy.o

この記事に書いてあった通り、clang-16ではベクタレジスタを持っているアーキテクチャ向けには-O2の最適化でもうベクタレジスタを使うようにauto vectorizeされます。

注目するべきなのは一番最後のブロック。

saxpy.o:	file format elf64-littleriscv

Disassembly of section .text:

0000000000000000 <.text>:
       0: 01 00        	nop
		0000000000000000:  R_RISCV_ALIGN	*ABS*+0x2

0000000000000002 <saxpy>:
       2: 01 c1        	beqz	a0, 0x2 <saxpy>
		0000000000000002:  R_RISCV_RVC_BRANCH	.LBB0_8
       4: 73 23 20 c2  	csrr	t1, vlenb
       8: 93 56 13 00  	srli	a3, t1, 1
       c: 21 47        	li	a4, 8
       e: 63 60 d7 00  	bltu	a4, a3, 0xe <saxpy+0xc>
		000000000000000e:  R_RISCV_BRANCH	.LBB0_3
      12: a1 46        	li	a3, 8

0000000000000014 <.LBB0_3>:
      14: 63 60 d5 00  	bltu	a0, a3, 0x14 <.LBB0_3>
		0000000000000014:  R_RISCV_BRANCH	.LBB0_5
      18: 93 16 25 00  	slli	a3, a0, 2
      1c: 33 07 d6 00  	add	a4, a2, a3
      20: ae 96        	add	a3, a3, a1
      22: b3 36 d6 00  	sltu	a3, a2, a3
      26: 33 b7 e5 00  	sltu	a4, a1, a4
      2a: f9 8e        	and	a3, a3, a4
      2c: 81 c2        	beqz	a3, 0x2c <.LBB0_3+0x18>
		000000000000002c:  R_RISCV_RVC_BRANCH	.LBB0_9

000000000000002e <.LBB0_5>:
      2e: 81 48        	li	a7, 0

0000000000000030 <.LBB0_6>:
      30: 33 05 15 41  	sub	a0, a0, a7
      34: 8a 08        	slli	a7, a7, 2
      36: 46 96        	add	a2, a2, a7
      38: c6 95        	add	a1, a1, a7

000000000000003a <.LBB0_7>:
      3a: 07 a0 05 00  	flw	ft0, 0(a1)
      3e: 87 20 06 00  	flw	ft1, 0(a2)
      42: 43 70 05 08  	fmadd.s	ft0, fa0, ft0, ft1
      46: 27 20 06 00  	fsw	ft0, 0(a2)
      4a: 7d 15        	addi	a0, a0, -1
      4c: 11 06        	addi	a2, a2, 4
      4e: 91 05        	addi	a1, a1, 4
      50: 01 e1        	bnez	a0, 0x50 <.LBB0_7+0x16>
		0000000000000050:  R_RISCV_RVC_BRANCH	.LBB0_7

0000000000000052 <.LBB0_8>:
      52: 82 80        	ret

0000000000000054 <.LBB0_9>:
      54: 93 52 13 00  	srli	t0, t1, 1
      58: 93 86 f2 ff  	addi	a3, t0, -1
      5c: 33 78 d5 00  	and	a6, a0, a3
      60: b3 08 05 41  	sub	a7, a0, a6
      64: d7 76 00 0d  	vsetvli	a3, zero, e32, m1, ta, ma
      68: 57 54 05 5e  	vfmv.v.f	v8, fa0
      6c: 93 13 13 00  	slli	t2, t1, 1
      70: 46 8e        	mv	t3, a7
      72: b2 86        	mv	a3, a2
      74: 2e 87        	mv	a4, a1

0000000000000076 <.LBB0_10>:
      76: 87 64 87 02  	vl1re32.v	v9, (a4)
      7a: b3 07 67 00  	add	a5, a4, t1
      7e: 07 e5 87 02  	vl1re32.v	v10, (a5)
      82: 87 e5 86 02  	vl1re32.v	v11, (a3)
      86: b3 87 66 00  	add	a5, a3, t1
      8a: 07 e6 87 02  	vl1re32.v	v12, (a5)
      8e: d7 15 94 b2  	vfmacc.vv	v11, v8, v9
      92: 57 16 a4 b2  	vfmacc.vv	v12, v8, v10
      96: a7 85 86 02  	vs1r.v	v11, (a3)
      9a: 27 86 87 02  	vs1r.v	v12, (a5)
      9e: 1e 97        	add	a4, a4, t2
      a0: 33 0e 5e 40  	sub	t3, t3, t0
      a4: 9e 96        	add	a3, a3, t2
      a6: 63 10 0e 00  	bnez	t3, 0xa6 <.LBB0_10+0x30>
		00000000000000a6:  R_RISCV_BRANCH	.LBB0_10
      aa: 63 10 08 00  	bnez	a6, 0xaa <.LBB0_10+0x34>
		00000000000000aa:  R_RISCV_BRANCH	.LBB0_6
      ae: 01 a0        	j	0xae <.LBB0_10+0x38>
		00000000000000ae:  R_RISCV_RVC_JUMP	.LBB0_8

.LBB0_10のところがベクターレジスタを使って一気に演算しているところです。
1回のループでベクターレジスタ2個分の演算をするようになっていますね。

余談

他のCPUでは関数内の相対ブランチはコンパイル時に飛び先が確定しているのが普通ですが、RISC-Vではそれの解決もリンカーの仕事になっていますね。これは以前のkernel/VM探検隊でmoldリンカーを作ったRuiさんが話していた、「RISC-Vはリンク時にコードセクションのサイズが変わることがある」ためなのでしょうね。

関連

https://zenn.dev/tetsu_koba/articles/732279afbb1759
https://zenn.dev/tetsu_koba/articles/e176172371bf6a
https://zenn.dev/tetsu_koba/articles/a650999ce34230

Discussion