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
浮かんできた疑問