この章では実際に計算させるために必要なことを考えて行きます。
- arrayオブジェクト
- array_viewオブジェクト
- extentオブジェクト
- indexオブジェクト
- parallel_for_each関数
等の内容を扱っていきます。先にぐぐっちゃってもかまいませんが。
まずはGPU上に利用可能なメモリを確保しましょう。以下がコードです。
accelerator Acs;
array<int , 1> vAGpu(100 , Acs.get_default_view() );
これでAcsが指し示すGPUのメモリ上に、int型100個分のメモリが確保されます。
arrayはテンプレートクラスとなっていて、上記のように<>の間に型、配列の次元を指定できます。そしてコンストラクタとして次元に対応する数だけ各方向の配列数を決めることができ、最後の引数にどのGPUメモリに確保するかをご覧のように指定します。
それから、2次元配列としてアクセスしたいなら以下のように初期化します。。
accelerator Acs;
array<int , 2> vAGpu(100 , 100 , Acs.get_default_view() );
テンプレートに渡す次元数とコンストラクタの引数の数があっていないと謎のコンパイルエラーを出しますのでお気をつけください。
コンストラクタに渡す引数が次元数に従い増えます。2次元配列のようにアクセスできますが、内部では1次元配列としてアクセスされます。
そして以下に示すのが、前の章、"GPUを選ぼう" で選んだacceleratorオブジェクトを用いて、指定のGPUメモリ上にデータ領域を確保し、適当な計算をさせてその結果をCPU側に返すコードです。
accelerator Acs;
array<int , 2> *pvAGpu;
array_view<int , 2> vAGpuView();
extent<2> AEx(10 , 10);
vector<int> vACpu();
pvAGpu = new array<int , 2>(10 , 10 , Acs.get_default_view());
vAGpuView = pvAGpu;
parallel_for_each(
Acs.get_default_view() ,
AEx ,
[=](index<2> indC) restrict(amp)
{
vAGpuView[indC] = indC[0];
}
);
vACpu = pvAGpu;
for(int ic=0;ic<AEx[0];ic++)
{
for(int jc=0;jc<AEx[1];jc++)
{
cout << vACpu[ic*AEx[0] + jc] << " ";
}
cout << "\n";
}
array
このクラスはGPU上にメモリを確保し、管理してくれます。変数宣言の時点で必ず初期化関数(コンストラクタ)を呼び出す必要があります。よってプログラム開始後に動的に確保するメモリ量を決めたい時には上記のようにnew演算子を用います。new演算子は上記のように、メモリ確保時にコンストラクタを呼び出すこともできます。
array_view
このクラスは、実際にデータを持っているわけではなく、array や vector といったほかのオブジェクトのメモリを統一的にアクセスできるように設計されています。難しいですが、こういった機能をインターフェースとかビュウとか言ったりします。大体GPUからはarray_viewを通してアクセスするんだと思って大丈夫です。
ビュウの作成ですが、arrayオブジェクトへのビュウの作成には代入演算子のオーバーロードで対応してくれていますので、代入するだけです。メモリのコピーなどは行われません。消費するメモリはarrayが持っていた分だけです。今後このビュウへアクセスすれば、arrayの中のデータが改変されます。Cのポインタの機能を、CPU、GPUで統一して扱うクラスだと思えます。
extent
GPUに発行させるスレッド数を指定するためのクラスです。テンプレートにスレッドの次元、コンストラクタに各次元の要素数を指定します。内部構造は単純で次元数分の配列となっているだけのようです。
なお、GPUに処理させる場合、extentオブジェクトの次元、要素数がarrayオブジェクトの次元、要素数と一致している必要はありません。これにより、arrayオブジェクトのデータを半分だけ改変するような処理ができます。
index
スレッド番号を受け取るためと、arrayおよびarray_viewオブジェクトのデータへアクセスするためのものです。GPUに処理させる関数は必ずindexオブジェクトを引数としていなければならず、その次元はスレッド発行時に指定したextentオブジェクトと一致している必要があります。
index<2>と記述したのはそのためです。2次元のスレッドを発行したので、スレッド番号も2次元で受け取ります。
このスレッド番号を用いれば処理するべきデータがそのままわかります。
vector = array
arrayが持つデータ(GPU上のデータ)を参照するには、vectorで受け取ると良いでしょう。代入演算子で受け取れ、代入しようとするとその時点でGPUの処理が完全に終わるまで待機し、その後CPU側へデータを転送。そのデータへアクセスするためのvectorを返してくれます。
ちなみに上で説明したように、array_viewを用いてもCPU側からデータを参照できます。それでも良いかも知れませんね。この場合は、CPU側で参照しようとした瞬間にGPUとの同期が始まります。GPU側でデータが再度必要になった場合、CPU側から転送されるでしょう。ん?array_viewを使ったほうが良いのではないでしょうか?。
以下、array_view , arrayのみの例です。
accelerator Acs;
array<int , 2> *pvAGpu;
array_view<int , 2> vAGpuView();
index<2> indCaa;
pvAGpu = new array<int , 2>(10 , 10 , Acs.get_default_view());
vAGpuView = pvAGpu;
parallel_for_each(
Acs.get_default_view() ,
vAGpuView.extent ,
[=](index<2> indC) restrict(amp)
{
vAGpuView[indC] = indC[0];
}
);
for(int ic=0;ic<AEx[0];ic++)
{
for(int jc=0;jc<AEx[1];jc++)
{
indCaa[0] = ic;
indCaa[1] = jc;
cout << vAGpuView[indCaa] << " ";
}
cout << "\n";
}
なぜ、vectorで受け取る方法なんて示したのかといいますと、array_viewオブジェクトはGPUとの同期をとるからです。あとで詳しく話しますが非同期処理をしたい場合、vectorで受けとりましょう。microsoftは最適なパフォーマンスを提供するとだけ言っていましたが、vectorで受け取った方が高速で、非同期処理に向いていると思います。
非同期処理をしない場合、素直にarray_viewを通してCPU、GPU間の統一的なデータアクセスを行うことを推奨します。
CPU,GPU間のデータ転送はこのarray_viewクラスが鍵になりそうです。