RISC-V Vector拡張のアセンブリを読み解く 異なる型同士の演算

以前と同様に、RVVのアセンブリを読み解いてみようと思う。今回は型の大きさが異なる同士の演算を見ていこうと思う。

C言語ソースコード

// Vectorization of Mixed Types

#include <stdio.h>

int foo(int *A, char *B, int n) {
    for (int i = 0; i < n; i++) {
        A[i] += 4 * B[i];
    }
    return 0;
}

int main() {
    int n = 2001;
    int A[n]; char B[n];
    for(int i = 0; i < n; i++) {
        A[i] = i;
        B[i] = i % 256;
    }
    foo(A, B, n);
}

RVVアセンブリ

今回も関数のインライン化が行われているので、main関数の中だけを見れば良い。

 .text
    .attribute    4, 16
    .attribute    5, "rv64i2p0_m2p0_a2p0_f2p0_d2p0_c2p0_v1p0_zvl128b1p0_zvl32b1p0_zvl64b1p0"
    .file "test0.c"
    .globl    foo
    .p2align  1
    .type foo,@function
foo:
    blez a2, .LBB0_9
    slli a3, a2, 32
    li   a4, 8
    srli a6, a3, 32
    bgeu a2, a4, .LBB0_3
    li   a2, 0
    j    .LBB0_7
.LBB0_3:
    slli a2, a6, 2
    add  a2, a2, a0
    add  a3, a1, a6
    sltu a3, a0, a3
    sltu a2, a1, a2
    and  a3, a3, a2
    li   a2, 0
    bnez a3, .LBB0_7
    andi a2, a6, -8
    mv   a4, a2
    mv   a5, a0
    mv   a3, a1
.LBB0_5:
    vsetivli zero, 8, e8, mf4, ta, mu
    vle8.v v8, (a3)
    vsetvli  zero, zero, e32, m1, ta, mu
    vle32.v    v9, (a5)
    vzext.vf4  v10, v8
    vsll.vi    v8, v10, 2
    vadd.vv    v8, v8, v9
    vse32.v    v8, (a5)
    addi a3, a3, 8
    addi a4, a4, -8
    addi a5, a5, 32
    bnez a4, .LBB0_5
    beq  a2, a6, .LBB0_9
.LBB0_7:
    slli a3, a2, 2
    add  a0, a0, a3
    add  a1, a1, a2
    sub  a2, a6, a2
.LBB0_8:
    lbu  a3, 0(a1)
    lw   a4, 0(a0)
    slliw    a3, a3, 2
    addw a3, a3, a4
    sw   a3, 0(a0)
    addi a0, a0, 4
    addi a2, a2, -1
    addi a1, a1, 1
    bnez a2, .LBB0_8
.LBB0_9:
    li   a0, 0
    ret
.Lfunc_end0:
    .size foo, .Lfunc_end0-foo

    .globl    main
    .p2align  1
    .type main,@function
main:
    lui  a0, 2
    addiw    a0, a0, 1824
    sub  sp, sp, a0
    li   a0, 0
    addi a1, sp, 2044
    vsetivli zero, 8, e8, mf4, ta, mu
    vid.v  v8
    vsetvli  zero, zero, e32, m1, ta, mu
    vid.v  v9
    addi a2, sp, 11
    li   a3, 16
    li   a4, 2000
.LBB1_1:
    vsetvli  zero, zero, e32, m1, ta, mu
    vadd.vi    v10, v9, 8
    // mf4=64bitにすることでintと同様に
    // 8つのデータを扱えるようになる。
    // 8bitをオーバーした分は自動的に切り捨てられる
    vsetvli  zero, zero, e8, mf4, ta, mu
    vadd.vi    v11, v8, 8
    addi a5, a1, -32
    vse32.v    v9, (a5)
    vse32.v    v10, (a1)
    add  a5, a2, a0
    vse8.v v8, (a5)
    addi a5, a5, 8
    vse8.v v11, (a5)
    addi a0, a0, 16
    vsetvli  zero, zero, e32, m1, ta, mu
    vadd.vx    v9, v9, a3 // a3=16
    vsetvli  zero, zero, e8, mf4, ta, mu
    vadd.vx    v8, v8, a3 // a3=16
    addi a1, a1, 64
    bne  a0, a4, .LBB1_1
    li   a0, 0
    lui  a1, 2
    addiw    a2, a1, -192
    addi a1, sp, 2012
    add  a3, a1, a2
    li   a2, 2000
    sw   a2, 0(a3)
    li   a3, 208
    sb   a3, 2011(sp)
    addi a3, sp, 11
.LBB1_3:
    add  a4, a3, a0
    vsetvli  zero, zero, e8, mf4, ta, mu
    vle8.v v8, (a4)
    vsetvli  zero, zero, e32, m1, ta, mu
    vle32.v    v9, (a1)
    // SEW/4のソースオペランドをゼロ拡張してSEW幅化し書き込む
    vzext.vf4  v10, v8
    vsll.vi    v8, v10, 2
    vadd.vv    v8, v8, v9
    vse32.v    v8, (a1)
    addi a0, a0, 8
    addi a1, a1, 32
    bne  a0, a2, .LBB1_3
    lbu  a0, 2011(sp)
    lui  a1, 2
    addiw    a1, a1, -192
    addi a2, sp, 2012
    add  a1, a1, a2
    lw   a2, 0(a1)
    slliw    a0, a0, 2
    addw a0, a0, a2
    sw   a0, 0(a1)
    li   a0, 0
    lui  a1, 2
    addiw    a1, a1, 1824
    add  sp, sp, a1
    ret
.Lfunc_end1:
    .size main, .Lfunc_end1-main

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

気づいた点

  • 異なる型同士では一度に処理できるデータ数を揃えるために、レジスタ幅を調整している。具体的に、int型は256bitベクトルレジスタで8つのデータを処理することができるので、それに合わせてchar型も8つ処理するように256bitを4つに分けて64bitベクトルレジスタとして使っていた。
  • 8bit扱っていたため、最大値を超えた(オーバーフロー)した時には、データ幅の制約ゆえに自動的に切り捨てられていたから、i % 256のような計算は特に明示的に行われていなかった。