RISC-V Vector拡張のアセンブリを読み解く

RISC-V Vector拡張(RVV)のアセンブリを読むのにまだ慣れないので、実際に出力されたコードを読んで読み解いてみようと思う。

C言語ソースコード

void loops(int n, int A[], int B[]) {
    for (int i = 0; i < n; i++) {
        A[i] *= B[i];
    }
}

int main() {
    int n = 1000;
    int A[n], B[n];
    for(int i = 0; i < n; i++) {
        A[i] = i;
        B[i] = i;
    }
    loops(n, A, B);
    return 0;
}

出力されたRVV アセンブリ

    .text
    .attribute    4, 16
    .attribute    5, "rv64i2p0_m2p0_a2p0_f2p0_d2p0_c2p0_v1p0_zvl128b1p0_zvl32b1p0_zvl64b1p0" 
    .file    "test0.c" 
    .globl    loops
    .p2align    1
    .type    loops,@function
loops:
    blez    a0, .LBB0_9
    slli    a3, a0, 32
    li    a4, 16
    srli    a6, a3, 32
    bgeu    a0, a4, .LBB0_3
    li    a7, 0
    j    .LBB0_7
.LBB0_3:
    slli    a0, a6, 2
    add    a3, a1, a0
    add    a0, a0, a2
    sltu    a0, a1, a0
    sltu    a3, a2, a3
    and    a0, a0, a3
    li    a7, 0
    bnez    a0, .LBB0_7
    andi    a7, a6, -16
    addi    a4, a2, 32
    addi    a5, a1, 32
    mv    a0, a7
.LBB0_5:
    addi    a3, a4, -32
    vsetivli    zero, 8, e32, m1, ta, mu
    vle32.v    v8, (a3)
    vle32.v    v9, (a4)
    addi    a3, a5, -32
    vle32.v    v10, (a3)
    vle32.v    v11, (a5)
    vmul.vv    v8, v10, v8
    vmul.vv    v9, v11, v9
    vse32.v    v8, (a3)
    vse32.v    v9, (a5)
    addi    a4, a4, 64
    addi    a0, a0, -16
    addi    a5, a5, 64
    bnez    a0, .LBB0_5
    beq    a7, a6, .LBB0_9
.LBB0_7:
    slli    a3, a7, 2
    add    a0, a1, a3
    add    a1, a2, a3
    sub    a2, a6, a7
.LBB0_8:
    lw    a3, 0(a1)
    lw    a4, 0(a0)
    mulw    a3, a4, a3
    sw    a3, 0(a0)
    addi    a0, a0, 4
    addi    a2, a2, -1
    addi    a1, a1, 4
    bnez    a2, .LBB0_8
.LBB0_9:
    ret
.Lfunc_end0:
    .size    loops, .Lfunc_end0-loops

    .globl    main
    .p2align    1
    .type    main,@function
main:
// 2を12bit左シフト=4096 x 2
// 4バイト要素を1000個持つ配列が2つなのでちょうどこれくらい
    lui    a0, 2
    addiw    a0, a0, -176
// 配列の分だけスタック領域を確保
    sub    sp, sp, a0
    lui    a0, 1
    addiw    a1, a0, -128
    lui    a0, 1
    addiw    a0, a0, -80
// 以下二つの命令でa0にスタック内の配列の開始位置を割り当てる
    add    a0, a0, sp
    add    a0, a0, a1

    addi    a2, sp, 16
    add    a6, a2, a1
    lui    a1, 1
    addiw    a1, a1, -48
// もう一つの配列の開始位置をスタック内に割り当てる
    add    a2, sp, a1
    addi    a3, sp, 48
// 最初のオペランドはzeroだが、本来ここは一回のループで処理される要素数を格納する。
// つまりvlを格納し、いつもはAVLだが最後は余りの数が格納される。
// 二つ目のオペランドは8となっており処理する全ての演算対象要素数を表す。
    vsetivli    zero, 8, e32, m1, ta, mu
// 各要素のインデックスを0, 1, 2, ..., vl-1とv8に書き込む。
    vid.v    v8
    li    a4, 992
    li    a5, 16
.LBB1_1:
// 0, 1, 2, ... vl-1(7)となっているインデックス全てに8を加算
    vadd.vi    v9, v8, 8
    addi    a1, a2, -32
// 配列Aに0, 1, 2, ... 15を書き込んでSTORE
    vse32.v    v8, (a1)
    vse32.v    v9, (a2)
// 配列Bも同じだけSTORE
    addi    a1, a3, -32
    vse32.v    v8, (a1)
    vse32.v    v9, (a3)
// インデックスが入っているv8に16を足して更新
    vadd.vx    v8, v8, a5
// 配列Aのスタック領域を更新
    addi    a2, a2, 64
// 処理すべき配列の要素数の更新。bnezで使う。
    addi    a4, a4, -16
// 配列Bのスタック領域を更新
    addi    a3, a3, 64
    bnez    a4, .LBB1_1
// 以下あまり部分。1000%16 = 8回だけ行う。
    li    a2, 992
    sw    a2, 0(a0)
    sw    a2, 0(a6)
    li    a1, 993
    sw    a1, 4(a0)
    sw    a1, 4(a6)
    li    a1, 994
    sw    a1, 8(a0)
    sw    a1, 8(a6)
    li    a1, 995
    sw    a1, 12(a0)
    sw    a1, 12(a6)
    li    a1, 996
    sw    a1, 16(a0)
    sw    a1, 16(a6)
    li    a1, 997
    sw    a1, 20(a0)
    sw    a1, 20(a6)
    li    a1, 998
    sw    a1, 24(a0)
    sw    a1, 24(a6)
    li    a1, 999
    sw    a1, 28(a0)
    sw    a1, 28(a6)
// 関数のインライン化が行われていてここからloop関数。
    addi    a3, sp, 48
    lui    a1, 1
    addiw    a1, a1, -48
    add    a4, sp, a1
.LBB1_3:
// 配列AのLOAD
    addi    a1, a3, -32
    vle32.v    v8, (a1)
    vle32.v    v9, (a3)
// 配列BのLOAD
    addi    a1, a4, -32
    vle32.v    v10, (a1)
    vle32.v    v11, (a4)
    vmul.vv    v8, v10, v8
    vmul.vv    v9, v11, v9
    vse32.v    v8, (a1)
    vse32.v    v9, (a4)
    addi    a3, a3, 64
    addi    a2, a2, -16 // 最初はa2 = 992
    addi    a4, a4, 64
    bnez    a2, .LBB1_3
// あまりの乗算
    lw    a1, 0(a6)
    lw    a2, 0(a0)
    lw    a3, 4(a6)
    lw    a4, 4(a0)
    mulw    a1, a2, a1
    sw    a1, 0(a0)
    mulw    a1, a4, a3
    lw    a2, 8(a6)
    lw    a3, 8(a0)
    lw    a4, 12(a6)
    lw    a5, 12(a0)
    sw    a1, 4(a0)
    mulw    a1, a3, a2
    sw    a1, 8(a0)
    mulw    a1, a5, a4
    lw    a2, 16(a6)
    lw    a3, 16(a0)
    lw    a4, 20(a6)
    lw    a5, 20(a0)
    sw    a1, 12(a0)
    mulw    a1, a3, a2
    sw    a1, 16(a0)
    mulw    a1, a5, a4
    lw    a2, 24(a6)
    lw    a3, 24(a0)
    lw    a4, 28(a6)
    lw    a5, 28(a0)
    sw    a1, 20(a0)
    mulw    a1, a3, a2
    sw    a1, 24(a0)
    mulw    a1, a5, a4
    sw    a1, 28(a0)
    li    a0, 0
    lui    a1, 2
    addiw    a1, a1, -176
    add    sp, sp, a1
    ret
.Lfunc_end1:
    .size    main, .Lfunc_end1-main

    .ident    "clang version 14.0.0" 
    .section    ".note.GNU-stack","",@progbits
    .addrsig

浮かんできた疑問

  • 今回行っているvsetvli命令は必要?
  • 複数のベクトルレジスタを用いたいけどどうすればいいのか?コンパイルオプションで指定するの?
  • ストリップマイニングとは →大きなループをより小さなセグメントやストリップに分割すること。つまり大きなループ内に並列性を見つけたらそこをひとまとまりにしてベクトル命令で並列計算するというのも含まれる。参照