※NECさんより正確な情報を頂くことができたので、一部を修正しました。ただし、当時の試行錯誤も自身にとっての記録として重要なので、最低限の修正に留めました。
はじめに
今回はメモリの間接参照によるメモリ~ベクトルレジスタ間の読み書きを扱います。間接参照とは次のように、別の配列indexes
の指定する場所の配列要素を読むことです。
for(int i=0; i<N; ++i){
const int idx = indexes[i];
c[i] = b[idx];
}
このような処理は、SX-AuroraではGather(間接参照の読み込み)やScatter(間接参照の書き込み)と呼ぶようです。今回の目的はベクトル命令としてのGatherとScatterの使い方を試すことです。
また、上記の例で、配列b
やc
は浮動小数点だとしても、インデックスを格納したindexes
は整数型です。ですので、整数ベクトル型の扱い方を試すのももう一つの目的です。
SX-Aurora TSUBASAにおける整数ベクトル型
SX-Aurora TSUBASAのアーキテクチャガイドによると、ベクトルレジスタとして扱える整数ベクトル型には、次の四種があるようです:
- 32bit 符号無し整数型 × 256要素
- 32bit 符号付き整数型 × 256要素
- 64bit 符号無し整数型 × 256要素
- 64bit 符号付き整数型 × 256要素
ここで、32bitの整数型の場合は、レジスタ内の0-31bitを無視して、32-63bitを使う様ですが、この辺りはあまり意識しなくても良いでしょう。
C++のコード上で変数を使うには、これらのベクトル型の定義が必要です。浮動小数点の場合を参考に、次のように定義しました。
//the definition of vector type. 2048 means 8 byte x 256 //
typedef int __vi32 __attribute__((__vector_size__(1024)));
typedef unsigned int __vui32 __attribute__((__vector_size__(1024)));
typedef int64_t __vi64 __attribute__((__vector_size__(2048)));
typedef uint64_t __vui64 __attribute__((__vector_size__(2048)));
上から順に、
- 32bit符号付き整数ベクトル型
__vi32
- 32bit 符号無し整数ベクトル型
__vui32
- 64bit符号付き整数ベクトル型
__vi64
- 64bit符号無し整数ベクトル型
__vui64
としました。この定義で無事に動きました。
間接参照
間接参照としては次の2つのベクトル命令が準備されています。
- Gather:間接読み込み(メモリ→レジスタ)
__builtin_ve_vgt
- Scatter:間接書き込み(レジスタ→メモリ)
__builtin_ve_vsc
このとき、参照先のインデックスは先の整数ベクトル型変数を指定する必要があります。
ここでは、間接読み込みを使ったサンプルコードを示します。
#include <cstdio>
//the definition of vector type. 2048 means 8 byte x 256 //
typedef double __vr __attribute__((__vector_size__(2048)));
typedef int __vi32 __attribute__((__vector_size__(1024)));
typedef unsigned int __vui32 __attribute__((__vector_size__(1024)));
typedef int64_t __vi64 __attribute__((__vector_size__(2048)));
typedef uint64_t __vui64 __attribute__((__vector_size__(2048)));
int main(){
constexpr int VL = 256;
constexpr int N = 1000;
double* b = new double[N];
double* c = new double[N];
double* d = new double[N];
int* indexes = new int[N];
int64_t* indexes64 = new int64_t[N];
//initialize//
for(int i = 0; i < N; ++i){
b[i] = (double)i;
indexes[i] = N - 1 - i;
indexes64[i] = N - 1 - i;
}
//(1) main part
int i;
for(i = 0; i <= N - VL; i+=VL){
__vi32 vi;
__builtin_ve_vld(vi, &indexes[i], 4);//note: 3rd argument is 4//
__vr vb;
__builtin_ve_vgt(vb, b, vi);
__builtin_ve_vst(vb, &c[i], 8);
__vi64 vi64;
__builtin_ve_vld(vi64, &indexes64[i], 8);
__builtin_ve_vgt(vb, b, vi64);
__builtin_ve_vst(vb, &d[i], 8);
}
//(2) reminder part
if(i < N){
const int vl = N - i;
__vm mask;
__builtin_ve_eqvm(mask, mask, mask);
__vi32 vi;
__builtin_ve_vld(vi, &indexes[i], 4, mask, vl);
__vr vb;
__builtin_ve_vgt(vb, b, vi, mask, vl);
__builtin_ve_vst(vb, &c[i], 8, mask, vl);
__vi64 vi64;
__builtin_ve_vld(vi64, &indexes64[i], 8, mask, vl);
__builtin_ve_vgt(vb, b, vi64, mask, vl);
__builtin_ve_vst(vb, &d[i], 8, mask, vl);
}
//if the vector opeartion is correctly used, the numbers from 999.0 to 0.0 are printed.//
for (int i = 0; i < N; ++i) {
printf("c[%d] = %f\td[%d] = %f\t\n", i, c[i],i,d[i]);
}
delete[] b;
delete[] c;
delete[] d;
delete[] indexes;
delete[] indexes64;
return 0;
}
まず初期化の部分にて、読込ソースとなる配列b
には0から昇順に値をセットしています。一方で、間接参照のインデックスとなるindexes
およびindexes64
には、N-1
から降順にインデックスがセットしてあります。つまり、間接参照によって配列b
の中身を逆の順番に読み込んでいこうというわけです。
多少冗長ですが、前回までに触れたマスクや256未満のベクトル長を扱う例として、ループをメインpartとreminder partに分けています。詳しくは「その5」をお読みください。
ループ内では最初に整数ベクトル型の変数vi
やvi64
にindexes
やindexes64
を__builtin_ve_vld
命令で読み込んでます。これは間接参照ではなく通常の連続アクセスによる読み込みですね。
注意点は、32bit整数ベクトル型の場合には、__builtin_ve_vld(vi, &indexes[i], 4)
というように、第3引数であるstrideには4を指定することです。これは32bit=4byteの意味ですね(完全に勘でやってますが)。もちろん64bit整数ベクトル型の場合には8です。
追記: strideはbyte単位の指定で正しいそうです。
今度は__builtin_ve_vgt(vb, b, vi)
で間接参照による読み込みを行います。第3引数には、今準備した整数ベクトル型変数を指定します。第2引数は決して&b[i]
とはしてはいけません。あくまで配列の先頭ポインタであるb
もしくは&b[0]
を渡します。
あとは配列c
に結果を順に書き込んで完了です。
上手くいけば、999.0から0.0まで降順に出力されるはずです。
おわりに
これで間接参照と、それに必要な整数ベクトル型の扱いが出来ました。間接書き込みは今回は記載をしませんが、上記の応用で出来ると思います。単純に__builtin_ve_vsc(vb, b, vi)
とすればOKです。
メリークリスマス!