RISC-Vでの自動ベクトル化の活用とその効果

## RISC-Vでの自動ベクトル化の活用とその効果

RISC-Vアーキテクチャを使用して、C言語プログラムでの自動ベクトル化を検証するために、オンラインコンパイラGodboltを利用して以下のようなコードを書きました。

### ソースコード

共有用ショートリンク: [https://godbolt.org/z/jodvoqvTo](https://godbolt.org/z/jodvoqvTo)

```c
#include <stdio.h>
#include <stdlib.h>
#include <riscv_vector.h>

// __v2df
typedef double __v2df __attribute__ ((__vector_size__ (16)));

// __m128d
typedef __v2df __m128d;

// _mm_add_pd
__m128d _mm_add_pd(__m128d A, __m128d B) {
    return (__m128d)((__v2df)A + (__v2df)B);
}

int main() {
    __m128d a = {1.0, 2.0};
    __m128d b = {3.0, 4.0};
    __m128d c = _mm_add_pd(a, b);
    printf("%f %f\n", c[0], c[1]);
    return 0;
}
```

### コンパイラ設定と出力アセンブラ

#### コンパイラ1: RISC-V 64bits gcc(trunk)

- **コンパイラオプション**: `-O3 -ftree-vectorize -march=rv64gcv -mabi=lp64d -fopenmp-simd`

```assembly
_mm_add_pd:
        addi    sp,sp,-32
        sd      a0,0(sp)
        sd      a1,8(sp)
        vsetivli        zero,2,e64,m1,ta,ma
        sd      a2,16(sp)
        sd      a3,24(sp)
        addi    a5,sp,16
        vle64.v v2,0(sp)
        vle64.v v1,0(a5)
        vfadd.vv        v1,v1,v2
        vse64.v v1,0(sp)
        ld      a0,0(sp)
        ld      a1,8(sp)
        addi    sp,sp,32
        jr      ra
.LC2:
        .string "%f %f\n"
main:
        lui     a4,%hi(.LC0)
        lui     a5,%hi(.LC1)
        ld      a2,%lo(.LC0)(a4)
        ld      a1,%lo(.LC1)(a5)
        lui     a0,%hi(.LC2)
        addi    sp,sp,-16
        addi    a0,a0,%lo(.LC2)
        sd      ra,8(sp)
        call    printf
        ld      ra,8(sp)
        li      a0,0
        addi    sp,sp,16
        jr      ra
.LC0:
        .word   0
        .word   1075314688
.LC1:
        .word   0
        .word   1074790400
```

#### コンパイラ2: RISC-V 64bits clang(trunk)

- **コンパイラオプション**: `-march=rv64gcv  -O3 -mllvm --scalable-vectorization=on`

```assembly
_mm_add_pd:                             # @_mm_add_pd
        vsetivli        zero, 2, e64, m1, ta, ma
        vmv.v.x v8, a0
        vslide1down.vx  v8, v8, a1
        vmv.v.x v9, a2
        vslide1down.vx  v9, v9, a3
        vfadd.vv        v8, v8, v9
        vmv.x.s a0, v8
        vslidedown.vi   v8, v8, 1
        vmv.x.s a1, v8
        ret
main:                                   # @main
        addi    sp, sp, -16
        sd      ra, 8(sp)                       # 8-byte Folded Spill
.Lpcrel_hi0:
        auipc   a0, %pcrel_hi(.L.str)
        addi    a0, a0, %pcrel_lo(.Lpcrel_hi0)
        li      a1, 1025
        slli    a1, a1, 52
        lui     a2, 2051
        slli    a2, a2, 39
        call    printf
        li      a0, 0
        ld      ra, 8(sp)                       # 8-byte Folded Reload
        addi    sp, sp, 16
        ret
.L.str:
        .asciz  "%f %f\n"
```

### 詳細な解説と考察

#### 自動ベクトル化とは

自動ベクトル化は、コンパイラがループや特定の計算をベクトル化し、SIMD（Single Instruction, Multiple Data）命令を使用して同時に複数のデータを処理する最適化手法です。これにより、計算速度を大幅に向上させることができます。

#### RISC-Vにおけるベクトル化

RISC-Vはオープンソースの命令セットアーキテクチャで、拡張性が高く、さまざまな最適化機能を持っています。その中でも、RISC-V Vector Extension (V)は、ベクトル化をサポートするために設計されています。RISC-Vでのベクトル化を有効にするためには、`-march=rv64gcv` フラグを使用し、必要なベクトル命令をコンパイラに認識させます。

#### GCCの出力アセンブラの解説

GCCのアセンブラコードを見ると、ベクトルレジスタ `v1` と `v2` を使用してベクトル演算（ここでは浮動小数点の加算）を行っていることがわかります。具体的には、`vfadd.vv` 命令を使用してベクトル同士の加算を行っています。

#### Clangの出力アセンブラの解説

一方、Clangのアセンブラコードでは、ベクトル命令セットを使用してデータの移動や演算を行っている様子がわかります。特に `vslide1down.vx` や `vslidedown.vi` といった命令を使ってベクトルレジスタ間のデータシフティングを行い、最終的に `vfadd.vv` で加算を実行しています。

## RISC-VにおけるGCCとClangのベクトル化最適化の比較

### はじめに

RISC-Vアーキテクチャでのプログラム最適化において、GCCとClangはそれぞれ独自のアプローチと最適化技術を持っています。ここでは、自動ベクトル化を中心に、両者のコンパイル結果と性能を比較し、その特徴を考察します。

### 詳細な解説

#### GCCの特徴

1. **命令セットの利用**:
   - GCCは、`vle64.v` や `vfadd.vv` などのRISC-Vベクトル命令を効果的に使用して、ベクトル化を実現しています。

2. **スタック操作**:
   - 一時的なデータ保存のためにスタックを頻繁に使用しています。これは一方で安全性を確保しつつ、若干のオーバーヘッドを生じる可能性があります。

3. **ベクトルレジスタの管理**:
   - ベクトルレジスタ `v1` と `v2` を使用して、データのロードとストアを行い、ベクトル加算を実行しています。

#### Clangの特徴

1. **命令セットの利用**:
   - ClangもまたRISC-Vベクトル命令を使用していますが、特にデータの移動やシフト操作 (`vslide1down.vx` や `vslidedown.vi`) を多用しています。

2. **ベクトルレジスタの直接操作**:
   - Clangはスタックを使用せず、ベクトルレジスタ内で直接データを操作することで、効率的なベクトル計算を実現しています。

3. **コードの簡潔さ**:
   - Clangの生成するアセンブラコードは、比較的簡潔で、不要な命令が少ない印象です。これは、より効率的なベクトル化が行われていることを示唆します。

### 性能比較

#### 実行速度と効率性

実行速度の比較を行うためには、ベンチマークテストが必要です。しかし、一般的に以下の点を考慮すると、Clangの方がより高効率なコードを生成する可能性があります。

1. **レジスタ操作**:
   - Clangはスタック操作を避け、レジスタ間での直接操作を行うため、命令数が少なく、キャッシュミスやメモリアクセスのオーバーヘッドが少ない。

2. **命令の簡潔さ**:
   - Clangのアセンブラコードは、GCCに比べてシンプルであり、パイプラインの効率が向上する可能性があります。

#### コンパイラの最適化能力

GCCとClangはそれぞれ独自の最適化アルゴリズムを持っています。GCCは多くのアーキテクチャをサポートし、汎用性が高い一方で、ClangはLLVMの柔軟性を活かして、高度な最適化を行います。

RISC-VにおけるGCCとClangのベクトル化の最適化は、それぞれの特徴と利点を持っています。GCCは汎用的な最適化を提供し、スタック操作を多用することで安全性を確保しています。一方で、Clangはレジスタ間での直接操作を行い、効率的なコードを生成する傾向があります。実際の性能評価にはベンチマークテストが不可欠ですが、理論的にはClangの方が高効率なベクトル化を行う可能性が高いと言えます。

RISC-Vのような新しいアーキテクチャにおいては、最適なコンパイラの選択が性能に大きな影響を与えるため、特定の用途やプログラムに応じてGCCとClangを使い分けることが重要です。

### まとめ

自動ベクトル化を活用することで、RISC-Vアーキテクチャ上での計算処理を大幅に高速化できます。GCCとClangの両コンパイラでベクトル化を有効にするためのフラグ設定と、実際のアセンブラ出力を比較することで、どのように最適化が適用されるかを詳細に理解することができます。ベクトル化は、特にデータ並列性が高い計算において効果的であり、今後の高性能計算の基盤となる技術です。