C++ AMP
C++ AMP でのタイリングの概要
この記事では、Visual Studio 11 に付属予定の C++ AMP というプレリリース版テクノロジを取り上げており、記載している情報は変更されることがあります。
Visual Studio 11 は、C++ Accelerated Massive Parallelism (C++ AMP) によって、異種コンピューティングのサポートをメインストリームへと押し上げます。今月号の別の記事でも C++ AMP を取り上げているので、この記事を読む前にそちらを一読されることをお勧めします。まだお読みでなければ、まずはそちらの記事を読んでから先に進んでください。
これから、「タイリング」という GPU プログラミングのパフォーマンス最適化手法を紹介しますが、内容に入る前に、お読みいただいた別の記事で index<N>、extent<N>、array_view<T,N>、restrict(amp) および parallel_for_each について学んだことを思い出してください。C++ AMP API を使用すれば、独自のデータ並列処理アルゴリズムを実装できます。データ並列処理アルゴリズムの例としては、前述の記事でも示しているように、行列乗算などが挙げられます (図 1 参照)。
図 1 行列乗算 (簡易モデル)
1 void MatMul(vector<int>& vC, const vector<int>& vA,
const vector<int>& vB, int M, int N, int W )
2 {
3 array_view<const int, 2> a(M, W, vA), b(W, N, vB);
4 array_view<int, 2> c(M, N, vC);
5 c.discard_data();
6 parallel_for_each(c.extent, [=](index<2> idx) restrict(amp)
7 {
8 int row = idx[0]; int col = idx[1];
9 int sum = 0;
10 for(int i = 0; i < b.extent[0]; i++)
11 sum += a(row, i) * b(i, col);
12 c[idx] = sum;
13 });
14 c.synchronize();
15 }
行列乗算について詳しくない方は、bit.ly/HiuP (英語) を参照してください。
今回は、GPU のプログラミングを行う際に最も一般的な最適化手法である「タイリング」の概要を説明します。アルゴリズムにタイリングを導入する理由はただ 1 つ、データの再利用や適切なメモリ アクセス パターンを使って、パフォーマンスを向上することです。別の記事で説明したように、タイリングによって、上記の簡易モデルよりも低いレベルで GPU メモリ階層を活用できるようになります。
タイリングは、2 つの手順で実行します。まず、計算を明示的にタイルに分割します (上記の簡易モデルでは、この最初の手順が内部で行われています)。次に、tile_static メモリを利用する必要があります (この 2 つ目の手順は自動的には行われないため、自身で明示的に行う必要があります)。
tiled_extent クラス
parallel_for_each は、最初の引数としてエクステント オブジェクトを受け取ります。エクステントは計算の定義域を表します。つまり、いくつのスレッド (サイズ) を利用し、どのような形状 (次元) で計算を実行するかを表します。次に 2 つのエクステントの例を考えてみます。
extent<1> e(12); // 12 threads in a single dimension
extent<2> ee(2,6); // 12 threads in two-dimensional space
parallel_for_each には、tiled_extent 引数を受け取り、タイルを使用するオーバーロードがあります。tiled_extent は、元のエクステントを均等なサイズのタイルに分割する方法を表します。tiled_extent の行列階級 (次元数) は最大 3 です。それよりも多くの次元がある場合は、簡易モデルを使用するか、コードをリファクタリングする必要があります。タイル内の総スレッド数は 1,024 以下にしなければなりません。
tiled_extent オブジェクトを取得する最も簡単な方法は、パラメーターを受け取らないテンプレート化されたタイル メソッドをエクステントで呼び出すことです。これによって、そのエクステント オブジェクトの tiled_extent が返されます。上記 2 つのエクステントの例の場合、対応する tiled_extent オブジェクトを取得するには以下のように記述します。
tiled_extent<6> t_e = e.tile<6>(); // e.rank==t_e.rank
tiled_extent<2,2> t_ee = ee.tile<2, 2>(); // ee.rank==t_ee.rank
これを視覚的に表現したのが図 2 です。エクステントをタイリングすることによって、データが小さいサブセットにパーティション分割されるしくみを示しています。このサブセットを C++ AMP では「タイル」と呼びます。
図 2 tiled_extent とは、タイルに分割されたエクステントのこと
テンプレート引数に渡す数値は、コンパイル時にはわかっていなければなりません。次のように、この数値は、parallel_for_each に渡すグローバル エクステントの次元を剰余なく割り切れる必要があります。
- e[0]=12 は t_e.tile_extent[0]=6 によって割り切れる
- ee[0]=2 は t_ee.tile_extent[0]=2 によって割り切れる
- ee[1]=6 は t_ee.tile_extent[1]=2 によって割り切れる
パフォーマンス上の理由から、最下位次元の最小タイル サイズは 16 にします。タイル サイズを調整してパフォーマンスが向上するかどうかは、使用するハードウェアによって異なります。個人的には、タイル サイズの調整はお勧めしません。代わりに、さまざまなハードウェア全体で等しく適切に機能する、16 やその倍数から始まる数値を選んでください。
tiled_index クラス
parallel_for_each 呼び出しでは、2 つ目の引数としてコードを含むラムダを渡します。指定するコードはインデックス オブジェクトを受け取ります。インデックス オブジェクトは、スレッド ID と考えてかまいません。次に例を示します。
parallel_for_each(my_extent,[=](index<2> idx) restrict(amp){
my_array_view[idx] = ...
});
parallel_for_each に渡すエクステントをタイルに分割するときに、引数として渡すラムダのシグネチャが tiled_index を受け取ります。tiled_index は、4 つのインデックス オブジェクトから構成されます。たとえば、次のように、tiled_index オブジェクトのプロパティを使用することで、想定しているインデックス オブジェクトにアクセスすることができます。
parallel_for_each(my_extent.tile<2,2>(),[=](tiled_index<2,2> t_idx) restrict(amp){
my_array_view[t_idx.global] = ...
});
タイルを使用するアルゴリズムを記述しているときに、おそらく、(計算定義域全体のグローバル インデックスだけでなく) タイルのローカル インデックスについても知りたくなります。このインデックス オブジェクトは、tiled_index の local プロパティから取得できます。計算上、別のタイルとの相対インデックスと、そのタイルの原点のグローバル インデックスを把握しておくと役に立つアルゴリズムもあります。これらのインデックス オブジェクトには、tiled_index の tile プロパティと tile_origin プロパティからアクセスできます。
先ほどの例の 2 次元エクステント (extent<2>(2,6).tile<2,2>()) を使用すると、網掛けした四角形部分の tiled_index の前述の各プロパティ値は図 3 に示すようになります。
図 3 インデックス オブジェクトを返す tiled_index の各プロパティ
タイルを使用する parallel_for_each による行列乗算
図 1 は、C++ AMP の簡易モデルを使用した行列乗算の実装を示していました。今度は、これまで学習したことから、(tiled_extent と tiled_index を使用して) アルゴリズムで明示的にタイルを使用するよう変更してみましょう。
この方法を、図 4 に示します。
図 4 タイルを使用する行列乗算 (tile_static を使用しない手順 1)
3 array_view<const int, 2> a(M, W, vA), b(W, N, vB);
4 array_view<int, 2> c(M, N, vC);
5 c.discard_data();
6 parallel_for_each(c.extent.tile<16,16>(),
[=](tiled_index<16,16> t_idx) restrict(amp)
7 {
8 int row = t_idx.global[0]; int col = t_idx.global[1];
9 int sum = 0;
10 for(int i = 0; i < b.extent[0]; i++)
11 sum += a(row, i) * b(i, col);
12 c[t_idx.global] = sum;
13 });
14 c.synchronize();
6 行目で、エクステントのタイル メソッドを呼び出しています。ここでは、タイル サイズ (この例では 16 x 16) を指定し、対応するタイル サイズのテンプレート引数を指定する tiled_index を受け取るようラムダを変更しています。ラムダ本体では、出現するすべての idx を t_idx.global に置き換えます (8 行目と 12 行目)。このようなメカニズムの変換は、タイルを使用することに決める際にすべての C++ AMP アルゴリズムで最初に行う必要があることです。これは、簡易モデルからタイルを使用するモデルに変換する、最初の (しかし、唯一ではない) 手順です。
前述のように、この変更では、グローバル エクステントの次元数を剰余なく割り切れるようにタイル サイズを選択する必要があります。今回の例では、各次元が 16 で割り切れる正方行列を想定しています。また、タイル サイズを、静的な const int 変数か、テンプレート引数に取り出すのが一般的な手法です。
図 1 の簡易行列演算のサンプルでは、システムが自動的に計算をタイル化します。したがって、明示的ではなく暗黙のうちにタイル化されるため、次元数を割り切れるかどうかといった要件に配慮する必要はありません。しかし、タイリングにとって重要な 2 番目の手順の、tile_static メモリを使用するようアルゴリズムを変更することと、別のインデックス オブジェクトを 1 つ以上使用することは、簡易モデルでは実現できないため、開発者が自分で行う必要があります。この点について詳しく説明する前に、ちょっと回り道して、ハードウェアの基礎について理解します。
GPU のメモリ階層についての簡単な背景
GPU ハードウェアの特性について記述しているオンライン コンテンツはたくさんありますが、どのハードウェア ベンダーを扱っているか、また同じベンダーでもどのハードウェア製品ファミリーを扱っているかによって内容はさまざまです。ここでは、C++ AMP 開発者の観点から、特定のハードウェアに依存しない大まかなイメージを紹介します (ハードウェア ベンダーによって、用語や細部はそれぞれ異なります)。
GPU には、array データおよび array_view データが常駐するグローバル メモリがあります。また、スレッドごとにレジスタもあり、一般的にはローカル変数が格納されます (ハードウェア ドライバーによっては違う使い方もあります。たとえば、コードを実行するハードウェアのレジスタを使いすぎると、コンテンツがグローバル メモリに移動することになります)。グローバル メモリへのアクセスは、レジスタへのアクセスよりも非常に遅く、たとえば、レジスタへのアクセスは 1 GPU サイクルで済みますが、グローバル メモリへのアクセスには 1,000 GPU サイクル必要です。
さらに、GPU は、各処理要素の近くにスクラッチ パッド メモリ領域を所持しています (ハードウェアによって 16 ~ 48 KB になります)。これは、グローバル メモリよりも非常に速くアクセスできます。おそらく、1 回のアクセスにかかるのは 10 GPU サイクル程度です。このメモリ領域 (ローカル データ ストアとも呼びます) は、プログラミング可能なキャッシュです。CPU キャッシュは、自動的かつ透過的に管理されるため、あらゆるパフォーマンス上のメリットを自動的に手にすることができます。対照的に、GPU キャッシュは、データを内外にコピーしながら自分で管理しなければならないため、自動的にはパフォーマンス上のメリットが得られません。プログラミング モデルの中には、プログラミング可能なキャッシュを "共有メモリ"、"ローカル メモリ"、または "グループ共有メモリ" と呼ぶものもあります。C++ AMP では、これを「tile_static メモリ」と呼びます (後ほど詳しく扱います)。
今度は、メモリの有効期間とスコープの観点から、論理モデルと GPU ハードウェア モデルを照らし合わせます。
グローバル メモリにはすべてのスレッドからアクセスでき、その有効期間は parallel_for_each 計算の有効期間よりも長いため、後続の parallel_for_each 呼び出しでは、同じデータで演算できます。レジスタ値にはスレッドからのみアクセスでき、有効期間はスレッドと同じです。tile_static は、すべてのスレッドのサブセットによって共有されます。C++ AMP では、これをスレッドのタイルと呼び、有効期間はタイルと同じです。計算をタイルに分割する必要がある理由が、明らかになり始めたのではないでしょうか。つまり、スレッドが属しているタイルがわからなければ、高速な tile_static メモリを使うことができません。
tile_static メモリは、あるタイルが実行を完了して別のタイルがそれを引き継ぐまで、スレッドのタイルに "リース" されると考えることができます。したがって、論理的には、異なるタイルのスレッドの tile_static メモリは、スレッドごとに相互に独立しており、各スレッドに tile_static メモリの所有権があるように見えます。
新しい tile_static ストレージ クラスを使用する
tile_static メモリにアクセスするには、tile_static ストレージ クラスを使用します。これは、C++ AMP が C++ 言語に行う 2 つ目の機能強化です (もう 1 つは制約で、今月号の別の記事で扱っています)。
この新しいストレージ クラスは、restrict(amp) 関数でしか使用できません。また、使用できるのは parallel_for_each がタイル化されている場合に限るうえ、関数ブロック内で宣言された変数にしか使うことができません。ポインターと参照は、tile_static としてマークできず、tile_static 変数の暗黙のコンストラクターとデストラクターを呼び出しません。通常 tile_static 変数は配列 (ただし、必ずしもそうとは限りません) なので、その配列は一般的にタイル サイズと釣り合います。たとえば、次のようになります。
tile_static float t[16][16]; // Access t to access tile static memory
tile_static メモリを活用する最も一般的な方法は、アルゴリズムで複数回アクセスするグローバル メモリ領域を特定することです。特定したら、その領域を tile_static メモリにコピーして (このために、グローバル メモリに 1 回だけアクセスします)、グローバル メモリのデータに複数回アクセスする代わりに、データの tile_static のコピーを使用するようアルゴリズムを変更します (繰り返し、高速にアクセスできるようになります)。プログラミング可能なキャッシュは小さいため、array データと array_view データのすべてを tile_static 変数にコピーすることはできません。タイルを使用するアルゴリズムは、グローバル メモリから、タイル サイズに相当するデータのみをコピーして対処しなければならないため、一般により複雑になります。
上記の手法を示す実世界の例を見てみる前に、tile_static だけを使用している、単純で不自然なバグのある例を示します。この例では、行列のすべての要素を合計しようとしています。
1 static const int TS = 2;
2 array_view<int, 2> av(2, 6, my_vector);
3 parallel_for_each(av.extent.tile<TS,TS>(),
[=](tiled_index<TS,TS> t_idx) restrict(amp)
4 {
5 tile_static int t[TS][TS];
6 t[t_idx.local[0]][t_idx.local[1]] = av[t_idx.global];
7
8 if (t_idx.local == index<2>(0,0)) {
9 t[0][0] = t[0][0] + t[0][1] + t[1][0] + t[1][1];
10 av[t_idx.tile_origin] = t[0][0];
11 }
12 });
13 int sum = av(0,0) + av(0,2) + av(0,4); // The three tile_origins
このコードは、技術的にはとにかくひどいものです。9 行目と 13 行目は、タイル サイズが 2 x 2 = 4 で、サイズ全体が 2 x 6= 12 (したがってタイル 3 個) であるという事実を活用していますが、実際の実装をそのように記述できるのは、タイル サイズやエクステント全体が変わってもコードが変更する必要がない場合です。パフォーマンスも最悪です。と言うのも、アルゴリズムが単純で、その分岐はデータの並列処理に適していません。アルゴリズムでデータが再利用されないため、tile_static メモリを使用すること自体意味がありません。また、後で指摘しますが、正確さに関するエラーもあります。ただ、確かにひどい内容ですが、tile_static ストレージの初心者がコードについて理解できる程度にはシンプルです。重要なのはそれだけです。
6 行目では、各タイルの各スレッドが、グローバル メモリから tile_static メモリにデータをコピーしています。9 行目では、各タイルの最初のスレッドのみが、そのタイルの結果を、タイルの tile_static メモリの最初の位置に集計します。その後、10 行目で、(各タイルの) 同じスレッドが、結果をグローバル メモリに戻します。その後、アクセラレータの計算が完了し、13 行目のホスト側で、CPU スレッドが 3 つのタイルからの 3 つの合計を sum 変数に集計します。
正確性に関するエラー (具体的には競合状態) を見つけることができましたか。この後、タイル API の最後の部分を学習しながら、このバグの対処方法について説明します。
tile_barrier クラス
前の例の競合状態は、6 行目と 9 行目の間で発生します。9 行目では、ローカル インデックス (0,0) のスレッドが、既に tile_static 変数の t にすべての値が格納されていると仮定していますが、これは、6 行目でタイルの他のすべてのスレッドの実行が完了している場合のみ満たされる条件です。通常スレッドは互いに独立して実行されるため、この仮定は成り立ちません。
ここで必要なのは、タイルのすべてのスレッドが 6 行目の実行を完了するまで 9 行目が実行されないように条件を体系化する方法です。この制限を表現するには、tile_barrier を使用して、その wait メソッドのいずれかを呼び出します。自分で tile_barrier オブジェクトを構築することはできませんが、ラムダに渡す tiled_index の barrier プロパティから取得できます。したがって、6 行目の後に以下の行を挿入すれば、競合状態を解決することができます。
7 t_idx.barrier.wait();
この行は、条件ブロック内の 9 行目の直前には挿入できません。tile_barrier::wait への呼び出しは、タイルのすべてのスレッドが通る場所か、どのスレッドも通らない場所に置く必要があります。境界 (barrier) をそれ以外の場所に配置できるようにすると、タイルのスレッドが 1 つでも wait を実行しなかった場合、プログラムがそのスレッドを待機して停止してしまいます。
コンパイラは、こうした多くの競合状態にフラグを設定します。設定できないものについては、デバッガーが見つけます (このためには、Visual Studio 11 で、[Debug] (デバッグ) メニューの [Exceptions] (例外) をクリックし、[Exceptions] (例外) ダイアログ ボックスで [GPU Memory Access Exceptions] (GPU メモリ アクセス例外) チェック ボックスをオンにします)。
タイルを使った行列乗算の例
図 4 の行列乗算の例を思い出してください。ここからは、タイリングについての演習の第 2 部です。tile_static メモリも活用するので、必然的に、ローカル インデックスと tile_barrier オブジェクトも使用します。
ソリューションについて説明する前に、個人的な例を紹介します。私のノートブック コンピューターでは、C++ AMP の簡易行列乗算が、M = N = W = 1024 の行列の場合、シリアル処理を行う CPU コードの 40 倍以上ものパフォーマンスを発揮しました。TS = 16 (つまり、1 タイルあたり 16 x 16 = 256 スレッド) のこれから紹介するタイル ソリューションは、この簡易モデルの 2 倍の速さを実現します。これはデータ転送も含めた計測なので、既にデータを転送済みで、データの計算に集中できる場合のカーネル実行時間は tile_static メモリを使用しない場合に比べて 2 倍以上速くなります。では、このようにパフォーマンスを向上するには、どのような複雑さに対処しなければならないのでしょう。
完全にタイルを使用する行列演算コードを図 5 に示します。
図 5 タイルと tile_static メモリを使用する行列演算
1 void MatMul(vector<int>& vC, const vector<int>& vA,
const vector<int>& vB, int M, int N, int W )
2 {
3 array_view<const int,2> a(M, W, vA), b(W, N, vB);
4 array_view<int,2> c(M, N, vC);
5 c.discard_data();
6
7 parallel_for_each(c.extent.tile<TS,TS>(),
[=](tiled_index<TS,TS> t_idx) restrict(amp)
8 {
9 int row = t_idx.local[0]; int col = t_idx.local[1];
10 tile_static int locA[TS][TS], locB[TS][TS];
11 int sum = 0;
12 for (int i = 0; i < a.extent[1]; i += TS) {
13 locA[row][col] = a(t_idx.global[0], col + i);
14 locB[row][col] = b(row + i, t_idx.global[1]);
15 t_idx.barrier.wait();
16
17 for (int k = 0; k < TS; k++)
18 sum += locA[row][k] * locB[k][col];
19 t_idx.barrier.wait();
20 }
21 c[t_idx.global] = sum;
22 });
23 c.synchronize();
24 }
図 5 のコードは、(先ほどの規則に従いながら) タイル サイズやエクステントの総サイズがどのようなものであっても適切に機能します。ただ、より小さい数字を使えば、何を実行しているかを理解しやすくなります。小さい数字は、実行時には適していませんが、何が起こっているかを把握するには便利です。したがって、タイルが 2 x 2 (TS = 2) で、M = 2、N = 6、W = 4 であると仮定します。この構成を図 6 に示します。
図 6 2 x 2 のタイルに 12 個のスレッドがある行列乗算の例
コードを理解するために、タイル 1 つ (3 つのうち 2 つ目のタイル) と、160 という結果を計算するスレッド 1 つに注目します (この部分は、図 6 で網掛けにしています。セル C に結果を計算するためにこのスレッドがアクセスする必要がある行列 A の行と行列 B の列も網掛けにしています)。
図 6 の表示結果を見ながら、図 5 のコードについて考えてみます。
i = 0 のとき、図 7 の並列ウォッチ ウィンドウには、16 行目の内側のループまでの関連変数の値が示されます。
図 7 i = 0 のときの変数値を各列に、スレッドを各行に表示するウォッチ ウィンドウ
ご覧のように、タイルの 4 つのスレッドが同時に行列 A、B から locA、locB という 2 つの tile_static 配列にデータをコピーしています。15 行目の並列処理の境界ですべての結果が出揃ったら、17 行目の内部のループに入ります。k = 0 および k = 1 の関連変数値については図 8 を参照してください。
図 8 引き続き i = 0 で、k = 0 および k = 1 の場合の変数値
この時点では、注目しているスレッドは、1 x 4 の部分合計を計算して、変数 k の 2 回目の反復処理でその結果を 2 x 10 に加算し、24 という総計を算出しています (図 8 参照)。次に、19 行目の並列処理の境界で別のスレッドの完了を待機します。それらのスレッドは、外側のループの 2 回目の反復処理に入る準備ができています。変数 i が 2 になっていることに注目してください。図 9 は、境界までの関連変数の新しい値を示しています。
図 9 i = 2 のときの変数値
再び、タイルの 4 つのスレッドが同時に行列 A、B から locA、locB という 2 つの tile_static 配列にデータをコピーして、行列 A を右へ、行列 B を下へ移動します。15 行目の並列処理の境界ですべての結果が出揃ったら、もう一度 17 行目の内部のループに入ります。k = 0 と k = 1 の関連変数値については図 10 を参照してください。
図 10 によると、この時点で、注目しているスレッドは、3 x 16 の積に 24 を加算し、2 回目の k 反復処理でさらに 4 x 22 を加算して、160 という総計を算出しています。次に、19 行目の並列処理の境界で別のスレッドの完了を待機します。スレッドは、外側のループの 3 回目の反復処理に入る準備ができていますが、変数 i のループ条件が満たされなくなったため、21 行目に移動します。注目しているスレッドは、グローバル インデックスを使用して、C のグローバル メモリを更新します。
図 10 引き続き i = 2 で、k = 0 および k = 1 の場合の変数値
各スレッドがグローバル メモリから行列要素を 1 回コピーし、タイルの別のスレッドがこのコピーを再利用するしくみについておわかりいただけたと思います。前述のように、パフォーマンスを向上するには、グローバル メモリからデータを 1 回 tile_static メモリにコピーし、そのデータをコードで複数回再利用します。
コードを理解しやすいように、小さなエクステントとサイズ 4 のタイル (TS = 2) を選びましたが、実際のコードではどちらも大きくなります。前述のパフォーマンスの数値は、1 タイルあたり 16 x 16 = 256 のスレッドで求めています。グローバル メモリのデータに毎回アクセスする代わりに、高速メモリのデータを 256 回再利用することによりこの結果が得られています。タイルを使用した行列乗算の実装は、行列 A の適切なメモリ アクセス パターン (同時に、行列 B および C が、すべての実装において効率よくアクセスされる) によってさらなるメリットが得られます。ただし、この記事ではメモリの結合について取り上げていません。
tile_static メモリを活用するタイル型行列乗算については、これですべてです。タイルを使ったアルゴリズムについてさらに演習を行う場合は、"ネイティブ コードにおける並列プログラミング" チームのブログ (bit.ly/xZt05R、英語) で、サンプルについて確認してください。
トレードオフ
今回は、C++ AMP コードを最適化する最も一般的な手法である「タイリング」について説明しました。
新しい 3 つの C++ AMP クラス (tiled_extent、tiled_index、および tile_barrier) の使い方と、tile_static ストレージ クラスについて学習しました。(明示的にタイル化していない) 簡易実装から着手できます。その後、(タイル サイズを選択するために) エクステント オブジェクトのタイル関数を呼び出し、tiled_index を受け取るために (同じタイル サイズのテンプレート引数によって) ラムダのシグネチャを変更して、実装を変更します。
これで、メカニズムに関する手順 1 は完了です。2 つ目と最後の手順では、tile_barrier によって適切に同期を取りながら tile_static メモリを活用するようアルゴリズムを作り直します。これは、各アルゴリズムに新しいソリューションを考えなければならない場合のクリエイティブな手順です。行列乗算の簡易実装とタイルを使った実装は、それぞれタイリングの複雑さと、それでもタイリングを使用した場合のパフォーマンス向上を示しています。複雑さを回避するか、パフォーマンスの向上を目指すかのトレードオフは、自動的には行われず、自ら実行しなければなりません。
この種のキャッシュに対応するプログラミングは、自動透過キャッシュ管理システムに効果があるため、CPU コードも高速化します。また、タイルを使ったコードは、スマートなコンパイラによってさらにベクトル化に適するようになります。
今月号の 2 つの記事以外にも、並列プログラミング チームのブログ (bit.ly/bWfC5Y、英語) に、C++ AMP についての記事が数多く投稿されています。ぜひ、お読みください。
Daniel Moth は、マイクロソフトの開発部門に所属するプリンシパル プログラム マネージャーです。彼のブログは、danielmoth.com/Blog (英語) からご覧いただけます。
この記事のレビューに協力してくれた技術スタッフの Steve Deitz、Yossi Levanoni、Robin Reynolds-Haertle、Stephen Toub、および Weirong Zhu に心より感謝いたします。