SX-Aurora TSUBASAでvector intrinsicを使ってみる４：ループのベクトル化とリマインダー部

※NECさんより正確な情報を頂くことができたので、一部を修正しました。ただし、当時の試行錯誤も自身にとっての記録として重要なので、最低限の修正に留めました。

## はじめに

マスクについて使えるようになったので、いよいよループのベクトル化に挑戦してみます。

ベクトル演算では、256要素の演算を一度に行いますが、実はループ長Nをベクトル長256で割り切れない場合、あまりの部分が発生します。あまりの部分をリマインダーループと呼ぶことにします。

このリマインダループをスカラー処理していては、せっかくのそれ以前の要素をベクトル演算しても割に合いません。SX-Auroraのintrinsic命令では、256未満の要素に対するベクトル演算もできるように命令セットが工夫されています。

ループのベクトル化からリマインダーループの扱いまでを順に試していきましょう。

## ベクトル化する前のコード

まずは、ベクトル化する前のターゲットコードを示します。単純に配列bと配列cの足し算をする処理です。


```c++
int main(){
    constexpr int N = 1000;
    double b[N]={0.0};
    double c[N]={0.0};
    
    //initialize//
    for(int i = 0; i < N; ++i){
        b[i] = (double)i;
        c[i] = 1000.0;
    }

    /*main operation*/
    for(int i = 0; i < N; ++i){
        b[i] = b[i] + c[i];
    }
    
    for(int i = 0; i < N; ++i){        
        printf("%f\n", b[i]);
    }
    
    return 0;
}
```

実行すると、1000.0から1999.0までの数値が出力されます。

このコードの中で、初期化部分は今回は無視して、main operationと記載した足し算のループの部分をベクトル化していきます。

## ループのベクトル化

例として、以下では一貫して配列の長さは`N=1000`とします。

あらためて、ベクトル演算では256要素ずつ一度に処理するので、ループのカウンタも256ずつ一気に進めます。つまり、
```c++
constexpr int VL = 256;
int i;
//(1)main part
for(i = 0; i <= N-VL; i+=VL){
//...ベクトル演算...//
}

//(2)reminder part
if(i < N){
 //この時点でのiは N - Nを256で割った余り値 (i == N - (N % 256))
 //もしくは (i == (N/256)*256 = (N >> 8) << 8 )
//...まだここで 残ったi ~ N-1の要素への処理が必要...//
}
```
となります。ループ後にもカウンタ変数`i`が参照できるように、`i`の定義をループ外で書いています。

メイン部分(1)のループの範囲に注意してください。

ループを抜けた直後の(2)の時点では、`i`の値は`N=1000`を256で割ったあまりを`N`からひいたもの、つまり、`i==(N/256)*256=768`になっているはずです。

## メイン部分(1)のベクトル化

このリマインダー部分の232要素は、まずは置いておいて、メインのループ部分をベクトル化してみます。

結果から書いてしまうと次のようになります。
```c++
    constexpr int VL = 256;
    //(1) main part
    for(int i = 0; i <= N-VL; i+=VL){
        //load from array to vector resister//
        __vr vb;
        __builtin_ve_vld(vb, &amp;b[i], 8);
        __vr vc;
        __builtin_ve_vld(vc, &amp;c[i], 8);
        
        //vector add operation//
        __vr va;
        __builtin_ve_vfadd(va, vb,vc);

        //store to array from vector resister//
        __builtin_ve_vst(va, &amp;b[i], 8);
    }
```

ベクトル型の変数`vb`と`vc`を用意して、`__builtin_ve_vld`命令で配列`b`および`c`から256要素を読み込みます。

続いて、`__builtin_ve_vfadd`命令で`vb`と`vc`の各要素を足し算し、`va`に結果を代入します。

最後に、`__builtin_ve_vst`命令で`va`から配列`b`に書き込みます。

どうでしょう。普通のプログラムと比べてロードとストアが面倒ですが、メモリとレジスタ間のロード・ストアと思えば直感的でしょうか。やってるいることはシンプルだとおもいます。

## リマインダー部分(2)のベクトル化

さて、リマインダー部分ですが、これをスカラー処理のループ(リマインダーループ)として次のように書いてしまうこともできます。
```c++
    //(2') here, i == (N >> 8) << 8  //
    for(   ; i < N; ++i){
        b[i] = b[i] + c[i];
    }
```

ただこれだとAVXのようにベクトル長の短い物なら良いですが、SX-Auroraのようにベクトル長が長い場合には、このリマインダーループでせっかくベクトル化が台無しになりそうです。

そこで、SX-Auroraでは、256未満の要素に対しても一度に処理できるよう、ベクトル命令のオプションがあります。

例えば、足し算をする`__builtin_ve_vfadd`命令では引数は3つから5つまで可変でした。
```c++
void __builtin_ve_vfadd(__vr dest, __vr src1, __vr src2
                              [, __vm mask][, int vl]);
```
ここで、第4引数は前回まで扱ったマスクでした。第5引数が実際に演算するベクトル長で、ここに256未満の数字を入れれば、それ以降の要素は無視するという事です。

これだけみると、今回の場合は`N % 256 = N - i = 232`を第5引数にセットすれば良さそうですが、果たしてうまくいくのか。気になるのはロード・ストアで、Segmentation faultになったりしないのでしょうか？やってみましょう。

とにかく、第5引数を使うとリマインダー部分のコードはループではなく1度の処理として次のように書けそうです。
```c++
//(2)reminder part
    if(i < N){
        const int vl = N - i;

        //all bit stands//
        __vm mask;
        __builtin_ve_eqvm(mask, mask, mask);
        
        //load from array to vector resister//
        __vr vb;
        __builtin_ve_vld(vb, &b[i], 8, mask, vl);
        __vr vc;
        __builtin_ve_vld(vc, &c[i], 8, mask, vl);
        
        //vector add operation//
        __vr va;
        __builtin_ve_vfadd(va, vb,vc, mask, vl);

        //store to array from vector resister//
        __builtin_ve_vst(va, &b[i], 8, mask, vl);
    }
```

余り部分の要素の長さを`vl = N-i`で定義しています。

足し算だけでなく、ベクトリ型へのロード・ストア命令でも、第5引数で要素数`vl`を渡しています。

~~ところで、問題は、第5引数を渡すためには第4引数も渡さなければならないということです。一方で今回はマスクによる演算の無効化は時に必要ないので、マスク変数として全てのbitを立てたものを用意しています。必要ないのに渡すというのもなんとも面倒な気もしますが、仕方ないですね（実はこのために、先にマスク編の記事を書きました。）~~

**追記：** マスクの指定を省略して、第4引数に要素数`vl`を入力するという記述も可能だそうです。上記の場合では例えば次のように書けます。
```
__builtin_ve_vld(vb, &b[i], 8, mask, vl);
↓
__builtin_ve_vld(vb, &b[i], 8, vl);
```


## 結果

最終的なコードは次のようになります。試したところ、一応これで上手く動きました。
```c++
#include <cstdio>

//the definition of vector type. 2048 means 8 byte x 256 //
typedef double __vr __attribute__((__vector_size__(2048)));  

int main(){
    constexpr int N = 1000;
    double b[N]={0.0};
    double c[N]={0.0};

    //initialize//
    for(int i = 0; i < N; ++i){
        b[i] = (double)i;
        c[i] = 1000.0;
    }

    /*original loop
    for(int i = 0; i < N; ++i){
        b[i] = b[i] + c[i];
    }
    */

    constexpr int VL = 256;
    //(1) main part
    int i;
    for(i = 0; i <= N - VL; i+=VL){
        //load from array to vector resister//
        __vr vb;
        __builtin_ve_vld(vb, &b[i], 8);
        __vr vc;
        __builtin_ve_vld(vc, &c[i], 8);
        
        //vector add operation//
        __vr va;
        __builtin_ve_vfadd(va, vb, vc);

        //store to array from vector resister//
        __builtin_ve_vst(va, &b[i], 8);
    }
    
    //(2)reminder part
    if(i < N){
        const int vl = N - i;
        __vm mask;
        __builtin_ve_eqvm(mask, mask, mask);//all bit stands//
        //load from array to vector resister//
        __vr vb;
        __builtin_ve_vld(vb, &b[i], 8, mask, vl);
        __vr vc;
        __builtin_ve_vld(vc, &c[i], 8, mask, vl);
        
        //vector add operation//
        __vr va;
        __builtin_ve_vfadd(va, vb,vc, mask, vl);

        //store to array from vector resister//
        __builtin_ve_vst(va, &b[i], 8, mask, vl);
    }

    for(int i = 0; i < N; ++i){        
        printf("%f\n", b[i]);
    }
    
    return 0;
}
```

## その他の書き方

今回はリマインダー部分を別途書きました。一方で、webのintrinsic チュートリアル等を参考にすると、次のようにリマインダー部分を分けずに1つのループで書いてしまう方法もあります。


```c++
#include <cstdio>
#include <algorithm>

//the definition of vector type. 2048 means 8 byte x 256 //
typedef double __vr __attribute__((__vector_size__(2048)));  

int main(){
    constexpr int N = 1000;
    double b[N]={0.0};
    double c[N]={0.0};

    //initialize//
    for(int i = 0; i < N; ++i){
        b[i] = (double)i;
        c[i] = 1000.0;
    }

    /*original loop
    for(int i = 0; i < N; ++i){
        b[i] = b[i] + c[i];
    }
    */

    constexpr int VL = 256;
    //(1) main part
    int i;
    for(i = 0; i < N; i+=VL){
        const int vl = std::min(VL, N-i);
        __vm mask;
        __builtin_ve_eqvm(mask, mask, mask);//all bit stands//
        //load from array to vector resister//
        __vr vb;
        __builtin_ve_vld(vb, &b[i], 8, mask, vl);
        __vr vc;
        __builtin_ve_vld(vc, &c[i], 8, mask, vl);
        
        //vector add operation//
        __vr va;
        __builtin_ve_vfadd(va, vb,vc, mask, vl);

        //store to array from vector resister//
        __builtin_ve_vst(va, &b[i], 8, mask, vl);
    }

    for(int i = 0; i < N; ++i){        
        printf("%f\n", b[i]);
    }
    
    return 0;
}
```

この方法だと、リマインダー部分を別建てで書かなくて見やすいです。またループの範囲もoriginalと変わらないので間違えにくいですね。

一方で、この方法で心配なのは、毎回の処理にマスク処理が適用されていることです。これによるスループットの速度の低下があるのかないのか良く分からないので、個人的には前者の方法でリマインダー部を分けるのが良いかなと思います。

## おわりに

いよいよベクトル化できるようになりました。

ここまで何度か行き詰りましたが、なんとか辿り着けてよかったです。

メリークリスマス！