FPGA開発日記

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

LLVM16でRISC-V Vector Supportはどのように変わったのか (3. Scalable Vectorizationとは)

LLVM16になって、RISC-VのVectorizationの状態について確認していきたい。

muxup.com

Scalable Vectorizationの機能について、テストプログラムを動かしながら動作確認をしていきたい。 以下のような、簡単なプログラムを作ってどのようにコンパイルされるのかを見てみた。

そもそもScalable Vectorizationで何ができるのかよく分かっていなかったのだが、以下のように分類していきたい。

以下のコードをどのようにコンパイルするのか、というのを考える:

typedef float data_t;
void vec_test (data_t *a, const data_t *b, const data_t c, const int N) {
    for (int i = 0; i < N; i++) {
        a[i] = b[i] * c;
    }
}
  • 自動ベクトル化を使用しない完全なコード生成の場合:

素数Nをうまく利用して、VLレジスタを活用して端数を処理することで、無駄なく処理が実行できる。また、LMULを活用することで、最大限までベクトルレジスタを活用できる。

Loop:
    vsetvli   t0, a3, e32, m8
    vle32.v   v8, (a1)
    vfmul.vf  v16, v8, fa0
    vse32.v   v16, (a0)
    slli      t1, t0, 2
    add       a1, a1, t1
    add       a0, a0, t1
    sub       a3, a3, t0
    bnez      a3, Loop
  • Scalable Vectorizationを使用しない、固定長ベクトル・レジスタを使用した場合のコード生成:

以下のオプションでコンパイルする。

-march=rv64gcv -O3 -Ofast -mllvm -scalable-vectorization=off -mllvm -riscv-v-vector-bits-min=256

見てわかるとおり、scalable-vectorizationを有効にしないと、固定長のベクトル・レジスタ長 (256-bit)を使用すると仮定している。 端数は、スカラ命令列で処理している。

.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        addi    a7, a3, 32
        vle32.v v8, (a3)
        vle32.v v9, (a7)
        // ここの部分。ベクトル・レジスタ長が256-bit(32B)なので、ポインタをそのサイズで進めている。
        // つまり、VLEN=256以外で動かすと、おそらくハードウェアの動きがおかしくなる。
        // 固定値なので、幅が合うようにアラインしたうえで、2回のループアンローリングを行っている
        addi    a7, a5, 32  
        vfmul.vf        v8, v8, fa0
        vfmul.vf        v9, v9, fa0
        vse32.v v8, (a5)
        vse32.v v9, (a7)
        addi    a3, a3, 64
        addi    a4, a4, -16
        addi    a5, a5, 64
        bnez    a4, .LBB0_4
        beq     a6, a2, .LBB0_8
  • Scalable Vectorizationを使用した場合:

以下のオプションでコンパイルする。

-march=rv64gcv -O3 -Ofast -mllvm -scalable-vectorization=on

上記の固定ベクトル・レジスタ長の場合と異なり、ベクトルレジスタ長をVLENBシステムレジスタから取り出し、ポインタのステップ数をここから算出する。

        blez    a2, .LBB0_8
        csrr    t1, vlenb
        srli    a3, t1, 1
        li      a4, 8
        bltu    a4, a3, .LBB0_3
        li      a3, 8
...
/* ... t1にVLENB(ベクトル・レジスタ長)が格納されている ... */
.LBB0_10:                               # =>This Inner Loop Header: Depth=1
        vl1re32.v       v8, (a4)
        // t1=VLENBを使用してベクトル・レジスタ長に合わせながらステップしていく
        // それでも、端数が処理されていないので、スカラ命令列と組み合わせて動かす必要がある
        add     a3, a4, t1
        vl1re32.v       v9, (a3)
        vfmul.vf        v8, v8, fa0
        vfmul.vf        v9, v9, fa0
        vs1r.v  v8, (a5)
        add     a3, a5, t1
        vs1r.v  v9, (a3)
        add     a4, a4, t2
        sub     t3, t3, t0
        add     a5, a5, t2
        bnez    t3, .LBB0_10

まとめとしては、それぞれのコンパイルモードで以下のような違いがあるだろう。

固定ベクトル・レジスタモード Scalable Vectorization有効化 アセンブリ言語による記述
ベクトル・レジスタ 固定長を仮定 VLENBレジスタを使用して取得 VSETVLI命令を使用して取得
端数の処理 スカラ命令により処理 スカラ命令により処理 必要なし
複数ベクトルレジスタLMUL>1の扱い 未サポート 未サポート サポート