RISC-Vベクタ拡張プログラミングでは条件分岐を書くかわりにベクトルマスクレジスタを用いる

RISC-Vベクタ拡張(RVV)プログラミング記事の第2弾です．

# コード例

```c
#include <stdint.h>

#if ! ( defined(__riscv_vector) && defined(ASM_ROUTINE) )

void v_abs(uint64_t n, int64_t *v)
{
    int64_t *p = v;

    for(uint64_t i = 0; i < n; i++) {
        int64_t t = *p;
        if(t < 0) {
            t = -t;
        }
        *p++ = t;
    }
}

#else // ( defined(__riscv_vector) && defined(ASM_ROUTINE) )

void v_abs(uint64_t n, int64_t *v)
{
    int64_t *p = v;

    asm volatile (
        "mv t1, %[n]\n\t"
        "loop%=:\n\t"
        "vsetvli t0, t1, e64, m8\n\t"
        "vle64.v v8, %[p]\n\t"
        "vmslt.vx v0, v8, x0\n\t"
        "vrsub.vx v8, v8, x0, v0.t\n\t"
        "vse64.v v8, %[p]\n\t"
        "sub t1, t1, t0\n\t"
        "slli t0, t0, 2\n\t"
        "add %[v], %[v], t0\n\t"
        "bnez t1, loop%=\n\t"
        : [p] "=rm" (*p), [v] "=r" (p)
        : [n] "r" (n)
        : "t0", "t1"
    );
}

#endif // ( defined(__riscv_vector) && defined(ASM_ROUTINE) )
```

# コード解説

```c
#if ! ( defined(__riscv_vector) && defined(ASM_ROUTINE) )
```

前回も解説しました．マクロ`__riscv_vector`はGCCによって設定され，RVVをサポートしている時に真になります．これを用いて `#ifdef`等で分岐することができます．ここでは．`__riscv_vector`が真で，かつマクロ`ASM_ROUTINE`も真である時にRVV用のコードが発動するようにしています．

```c
    asm volatile (
        ...
        : [p] "=rm" (*p), [v] "=r" (p)
        : [n] "r" (n)
        : "t0", "t1"
    );
```

* `asm volatile`はインラインアセンブリコードを指示します．`volatile`はコード最適化の対象にしないことを示しています．
* `...`部分にアセンブリコードを書きます．
* `: [p]`の行は出力として変化するレジスタと変数を指定しています．`=rm`とすることでメモリを参照することを示しています．`*p`と`p`を呼び分けているのは，苦肉の策です(どうまとめたらいいかがわからなかったので，より良い方法をご存知の方は教えてください)．
* `: [n]`の行は入力として使用するレジスタと変数を指定しています．
* `"t0"`の行は，内部で使用するレジスタを指定しています．ベクタレジスタは指定しなくて良いようです．

```c
    asm volatile (
        "mv t1, %[n]\n\t"
        ...
    );
```

プロローグとして，`t1`レジスタに`n`の値を入れてカウンタとしています．

```c
    asm volatile (
        ...
        "loop%=:\n\t"
        "vsetvli t0, t1, e64, m8\n\t"
        "vle64.v v8, %[p]\n\t"
        "vmslt.vx v0, v8, x0\n\t"
        "vrsub.vx v8, v8, x0, v0.t\n\t"
        "vse64.v v8, %[p]\n\t"
        "sub t1, t1, t0\n\t"
        "slli t0, t0, 2\n\t"
        "add %[v], %[v], t0\n\t"
        "bnez t1, loop%=\n\t"
        ...
    );
```

* ラベルを指定する時には `%=`を末尾につけます．こうすることで，コード展開によって同名のラベルが現れたときに区別することができます．
* `vsetvli`命令によってベクトルレジスタを初期化します．`e64`は1要素のサイズが`64`ビットであることを示します．`m8`はベクトルレジスタを`8`つまとめて同時に使うことで，より効率の良いコードにします．スカラレジスタ`t0`には各イテレーション(反復)で一度に処理する要素数が格納されます．
* `vle64.v v8, %[p]`命令によって，ベクトルレジスタ`v8`に`*p`の値をロードします．
* `vmslt.vx v0, v8, x0`とすることで，ベクトルレジスタ`v8`の各要素とスカラレジスタ`x0`すなわち`0`を比較して，ベクトルレジスタ`v8`の要素の値が小さい場合，その添字に対応する要素を1にして，そうでない場合にその添字に対応する要素を0にしたベクトルマスクを，ベクトルレジスタ`v0`に代入します．以後，`v0`はベクトルマスクレジスタ`v0.t`として使用します．
* `vrsub.vx v8, v8, x0`だった場合には，ベクトルレジスタ`v8`のすべての要素の値について，スカラレジスタ`x0`すなわち`0`から引いた値とします．つまり，ベクトルレジスタ`v8`のすべての要素について符号を反転させます．
* このコード例では`vrsub.vx v8, v8, x0, v0.t`ですので，ベクトルレジスタ`v8`の中で，`v0.t`の値が`1`であるような添字の要素について，符号を反転させます．
* そういうわけで，`vmslt.vx v0, v8, x0` `vrsub.vx v8, v8, x0, v0.t`の一連の流れで，`if(t < 0) { t = -t; }`の条件分岐を実現することができます．これがこのコードの肝となります．
* `vse64.v v8, %[p]`でベクトルレジスタ`v8`の値を`*p`にストアします．
* `sub t1, t1, t0`命令によって，カウンタ`t1`から，このイテレーション(反復)で一度に処理する要素数`t0`を減算します．
* `slli t0, t0, 2`と`add %[v], %[v], t0`により，ポインタをインクリメントします．
* `bnez t1, loop%=`とすることで，カウンタ`t1`の値が`0`より大きい時にループします．

# おわりに

RVVについての日本語ドキュメントはこちらが便利です．

https://msyksphinz-self.github.io/riscv-v-spec-japanese/html/index.html

今回紹介したべクトルマスクレジスタを使うことで条件分岐相当を実現できるというのは，条件分岐による制御ハザードが起きにくくなる点で，大変優れていると思いました．
しかし一方で，べクトルマスクレジスタには`v0`しか指定できないのが，コード生成やコード最適化において，面倒なことになりそうな予感がします．一応`vmmv.m`命令を用いることで，他のベクトルレジスタに退避することはできます．