C++

サンプル コードに基づく C++ AMP の紹介

Daniel Moth

コード サンプルのダウンロード

この記事では、Visual Studio 11 に付属予定の C++ AMP というプレリリース版テクノロジを取り上げており、記載しているすべての情報は変更されることがあります。

Visual Studio 11 は、C++ Accelerated Massive Parallelism (C++ AMP) というテクノロジによって、異種コンピューティングのサポートをメインストリームへと押し上げます。これにより、GPU などのアクセラレータを利用して、データ並列アルゴリズムを高速化できます。

C++ AMP は、各種ハードウェアへの移植が可能で、現在の C++ および Visual Studio パッケージに期待されるようになった、生産性を犠牲にしないパフォーマンスの向上を実現します。C++ AMP を利用すると、CPU のみを使用する場合と比べて、桁違いの高速化を実現できる可能性があります。私は、カンファレンスで、通常、NVIDIA と AMD の両方の GPU を同時に利用しながら、CPU のフォールバック ソリューションも利用できる単一プロセスのデモを使用しています。

コードを基に C++ AMP の使い方を紹介するこの記事では、記事内のコードのすべての行に目を通すことを前提にしています。インライン コードがこの記事の中心です。ただし、C++ コード内の記述は、必ずしも記事の文章中に引用していません。

セットアップとサンプル アルゴリズム

まず、準備として、必要なセットアップ コードと、ここで使用するシンプルなアルゴリズムについて説明します。このアルゴリズムを、後で C++ AMP を使用するように変更します。

空の C++ プロジェクトを作成し、新しい空の C++ ファイル (Source.cpp) を追加して、次のコードを入力します。コードの内容は見ての通りです (本文で説明しやすいように、行番号は不連続の番号にしています。また、この記事に付属のダウンロード可能なプロジェクトでも同じ行番号を使用しています)。

1 #include <amp.h>                // C++ AMP header file
3 #include <iostream>             // For std::cout etc
4 using namespace concurrency;    // Save some typing :)
5 using std::vector;     // Ditto. Comes from <vector> brought in by amp.h
6
79 int main()
80 {
81   do_it();
82
83   std::cout << "Hit any key to exit..." << std::endl;
84   std::cin.get();
85 }

C++ AMP では、複数のヘッダー ファイルで使用される型がいくつか導入されています。上記のコード スニペットの 1 行目と 4 行目からわかるように、メイン ヘッダー ファイルは amp.h で、主要な型は既存の concurrency 名前空間に追加されています。C++ AMP を使用するには、これ以外のセットアップやコンパイル オプションは必要ありません。今度は、main の上に do_it 関数を追加します (図 1 参照)。

図 1 main から呼び出される do_it 関数

52 void do_it()
53 {
54   // Rows and columns for matrix
55   const int M = 1024;
56   const int N = 1024;
57
58   // Create storage for a matrix of above size
59   vector<int> vA(M * N);
60   vector<int> vB(M * N);
61
62   // Populate matrix objects
63   int i = 0;
64   std::generate(vA.begin(), vA.end(), [&i](){return i++;});
65   std::generate(vB.begin(), vB.end(), [&i](){return i--;});
66
67   // Output storage for matrix calculation
68   vector<int> vC(M * N);
69
70   perform_calculation(vA, vB, vC, M, N);
76 }

本当に扱いたいのは 2 次元の型ですが、このコードでは、59、60、および 68 行目で、std::vector オブジェクトを各行列のフラット コンテナーとして使用しています。

2 つのベクター オブジェクトに値を設定するために、std::generate に渡す 64 行目と 65 行目のラムダ式の使い方を理解することが重要です。ここでは、読者が C++ でラムダを使いこなせることを前提としています。たとえば、変数 i は ([i] や [=] のようなキャプチャ リストを変更し、mutable キーワードを使用して) 値を基にキャプチャされ、ベクターのすべての数値が 0 に初期化されることが、すぐに理解できる必要があります。ラムダ (C++ 11 の標準に加わった便利な機能) を抵抗なく使用できない場合は、MSDN ライブラリの記事「C++ のラムダ式」(msdn.microsoft.com/library/dd293608、英語) を読んでから、この記事をお読みください。

do_it 関数は、perform_calculation を呼び出します。perform_calculation のコードは次のとおりです。

7  void perform_calculation(
8    vector<int>& vA, vector<int>& vB, vector<int>& vC, int M, int N)
9  {
15   for (int i = 0; i < M; i++)
16   {
17     for (int j = 0; j < N; j++)
18     {
19       vC[i * N + j] = vA[i * N + j] + vB[i * N + j];
20     }
22   }
24 }

この単純な行列加算のサンプルで特徴的な点は、行列を線形化してベクター オブジェクトに格納しているために、行列の多次元性が失われていることです (ベクター オブジェクトと併せて行列の次元数を渡す必要があるのはこのためです)。また、19 行目でインデックスを使用して風変わりな演算を実行することも必要です。この点は、これらの行列の部分行列も併せて加算する場合はさらに顕著になります。

ここまでは、C++ AMP コードを使用していません。今度は、perform_calculation 関数を変更しながら、どのようにして C++ AMP の型を導入するかを見ていきます。その後は、C++ AMP をフル活用して、データ並列アルゴリズムを高速化する方法について説明します。

array_view<T, N>、extent<N>、および index<N>

C++ AMP は、データをラップするコンテナー用に concurrency::array_view 型を導入しています。これは、スマート ポインターのようなものと考えることができます。多次元のデータを長方形方式で表し、最下位次元に連続するように 1 列に並べます。やがてこのようにデータを並べている理由がわかりますが、今度はこのデータをどのように使用するかを見ていきます。perform_calculation 関数本体を次のように変更します。

11     array_view<int> a(M*N, vA), b(M*N, vB);
12     array_view<int> c(M*N, vC);
14
15     for (int i = 0; i < M; i++)
16     {
17       for (int j = 0; j < N; j++)
18       {
19         c(i * N + j) = a(i * N + j) + b(i * N + j);
20       }
22     }

この関数は、CPU 上でコンパイルおよび実行され、前と同じ出力を返します。唯一の違いは、11 行目と 12 行目で array_view オブジェクトを使用している点です。19 行目にはやはり (今のところ) 風変わりなインデックスの処理がありますが、ベクター オブジェクト (vA、vB、および vC) ではなく、array_view オブジェクト (a、b、および c) を使用していて、array_view 関数演算子を使用して要素にアクセスしています (前回は、ベクター添え字演算子を使用しています。これについては、後で詳しく説明します)。

テンプレート引数 (この例では int) を使用して、array_view にラップ対象のコンテナーの要素の型を指定する必要があります。それには、コンテナーをコンストラクターの最後の引数として渡します (たとえば、12 行目のベクター型の vC 変数)。コンストラクターの最初の引数は要素の数です。

また、con­currency::extent オブジェクトを使用して要素の数を指定することもできるため、11 行目と 12 行目は次のように変更できます。

10     extent<1> e(M*N);
11     array_view<int, 1> a(e, vA), b(e, vB);
12     array_view<int, 1> c(e, vC);

extent<N> オブジェクトは多次元空間を表し、テンプレート引数として階数を渡します。この例ではこのテンプレート引数は 1 ですが、0 よりも大きい値であれば任意の値を階数に指定できます。extent コンストラクターは、extent オブジェクトが表す各次元のサイズを受け取ります (10 行目)。その後、extent オブジェクトを array_view オブジェクト コンストラクターに渡して、行列の形状を定義できます (11 および 12 行目)。これらの行では、array_view に渡す 2 つ目のテンプレート引数も追加して、これが 1 次元空間であることを示しています。ただし、1 は既定の階数のため、前のコード サンプルと同様この引数は省略できます。

これらの型の概要がわかったので、関数にさらに手を加えて、行列をより忠実に表現する、より自然な 2 次元の形状でデータにアクセスできるようにします。

10     extent<2> e(M, N);
11     array_view<int, 2> a(e, vA), b(e, vB);
12     array_view<int, 2> c(e, vC);
14
15     for (int i = 0; i < e[0]; i++)
16     {
17       for (int j = 0; j < e[1]; j++)
18       {
19         c(i, j) = a(i, j) + b(i, j);
20       }
22     }

10 ~ 12 行目の変更によって、array_view オブジェクトが 2 次元になるため、要素へのアクセスには 2 つのインデックスが必要になります。15 行目と 17 行目は、変数 M および N を直接使用するのではなく、添え字演算子を使用して、エクステントの範囲にアクセスします。エクステントに行列の形状をカプセル化すると、そのオブジェクトをコード全体で使用できるようになります。

重要な変更が 19 行目です。風変わりな演算が必要なくなりました。インデックス処理は、はるかに自然になり、アルゴリズム全体は格段に読みやすく、メンテナンスしやすくなっています。

array_view を 3 次元のエクステントで作成すると、関数演算子には、要素にアクセスするために 3 つの整数を渡す必要があります。この場合も最上位次元から最下位次元の順に指定します。多次元 API の機能として期待されているかもしれませんが、添え字演算子に単一のオブジェクトを渡すことによって、array_view をインデックス処理する手段もあります。このオブジェクトは concurrency::index<N> 型にする必要があります。この N は、array_view の作成時に指定しているエクステントの階数と同じです。インデックス オブジェクトをコードに渡す方法については後述しますが、ここでは、関数の本体を次のように変更して、手動で 1 つインデックスを作成して、感じをつかみ、実際にどのように使うかを見てみます。

10     extent<2> e(M, N);
11     array_view<int, 2> a(e, vA), b(e, vB);
12     array_view<int, 2> c(e, vC);
13
14     index<2> idx(0, 0);
15     for (idx[0] = 0; idx[0] < e[0]; idx[0]++)
16     {
17       for (idx[1] = 0; idx[1] < e[1]; idx[1]++)
18       {
19         c[idx] = a[idx] + b[idx];
//19         //c(idx[0], idx[1]) = a(idx[0], idx[1]) + b(idx[0], idx[1]);
20       }
22     }

14、15、17、19 行目からわかるように、concurrency::index<N> 型のインターフェイスは、extent 型と非常によく似ていますが、インデックスが N 次元空間ではなく、N 次元の点を表しているところが異なります。extent 型も index 型も、演算子をオーバーロードすることによって、さまざまな算術演算 (上記の例にあるインクリメント操作など) をサポートします。

以前は、ループ変数 (i と j) を使用して array_view にインデックス処理を行っていましたが、それらを 19 行目のインデックス オブジェクト 1 つに置き換えられるようになりました。これは、array_view の添え字演算子を使用して、1 つの変数 (この例では、index<2> 型の idx) だけで、array_view のインデックス処理を行う方法を示しています。

この時点で、array_view<T,N>、extent<N>、および index<N> という、C++ AMP で導入された 3 つの新しい型の基礎を理解できたと思います。これらの型には、図 2 のクラス ダイアグラムからわかるように、ほかにも機能があります。

array_view, extent and index Classes
図 2 array_view、extent、および index クラス

この多次元 API の真の威力と、これを使用する本当の目的は、GPU などのデータ並列アクセラレータでアルゴリズムを実行することです。そのためには、アクセラレータでコードを実行するための API にエントリ ポイントが必要です。また、そのようなアクセラレータで効率よく実行できる C++ 言語のサブセットを使用していることを、コンパイル時にチェックする手段も必要です。

parallel_for_each と restrict(amp)

C++ AMP ランタイムに、関数を取得してアクセラレータで実行するように指示する API は、concurrency::parallel_for_each の新しいオーバーロードの 1 つです。この API は、extent オブジェクトとラムダの 2 つの引数を受け取ります。

前に説明した extent<N> オブジェクトは、アクセラレータでラムダが呼び出される回数の定義に使用します。毎回のコード呼び出しには順序の保証がなく、個別のスレッドが使用され、場合によって複数のスレッドが同時に実行されると考えてください。たとえば、extent<1>(5) の場合、parallel_for_each に渡すラムダが 5 回呼び出され、extent<2>(3,4) の場合は 12 回ラムダが呼び出されます。実際のアルゴリズムでは、通常、数千回のラムダ呼び出しのスケジュールを設定します。

ラムダは、前に説明した index<N> オブジェクトを受け取る必要があります。index オブジェクトの階数は、parallel_for_each に渡す extent オブジェクトと同じでなければなりません。インデックス値は、当然、ラムダを呼び出すたびに異なります。これにより、ラムダの各呼び出しを区別できます。インデックス値はスレッド ID のようなものと考えることができます。

以下は、parallel_for_each についてこれまで説明した内容を表すコードです。

89     extent<2> e(3, 2);
90     parallel_for_each(e,
91       [=](index<2> idx)
92       {
93         // Code that executes on the accelerator.
94         // It gets invoked in parallel by multiple threads
95         // once for each index "contained" in extent e
96         // and the index is passed in via idx.
97         // The following always hold true
98         //      e.rank == idx.rank
99         //      e.contains(idx) == true
100        //      the function gets called e.size() times
101        // For this two-dimensional case (.rank == 2)
102        //      e.size() == 3*2 = 6 threads calling this lambda
103        // The 6 values of idx passed to the lambda are:
104        //      { 0,0 } { 0,1 } { 1,0 } { 1,1 } { 2,0 } { 2,1 }
105      }
106    );
107    // Code that executes on the host CPU (like line 91 and earlier)

この簡潔なコードは、91 行目の重要な追加がなければ、コンパイルされず、次のようなエラーが返されます。

error C3577: Concurrency::details::_Parallel_for_each argument #3 is illegal: missing public member: 'void operator()(Concurrency::index<_Rank>) restrict(amp)'

コードを記述できたので、(Visual C++ コンパイラーがサポートする) 完全な C++ 言語で使用できる任意の要素を、ラムダの本体 (92 ~ 105 行目) で使用できます。ただし、現在の GPU アーキテクチャでは、C++ 言語の特定の要素の使用は制限されるため、コードのどの部分がこの制限に従うかを指定する必要があります (これにより、コンパイル時にルールに違反していないかどうかを確認できます)。この指定は、ラムダや、ラムダから呼び出される、その他任意の関数シグネチャで行います。したがって、91 行目を次のように変更します。

91         [=](index<2> idx) restrict(amp)

これは、Visual C++ コンパイラに追加された、C++ AMP 仕様の新しい重要な言語機能の 1 つです。関数 (ラムダを含む) には、指定がない場合に既定値として使用される restrict(cpu)、または上記のコード サンプルのように restrict(amp)、または restrict(cpu, amp) のようにこの 2 つを組み合わせて、注釈を付けることができます。その他のオプションはありません。この注釈は、関数シグネチャの一部になるため、オーバーロード時に考慮されます。これが、この注釈を設計したときの、主な目的の 1 つでした。関数に restrict(amp) という注釈が付けられると、制約のセットを基にチェックされ、制約に違反しているものがあった場合はコンパイル エラーが返されます。完全な制約のセットについては、ブログ記事 bit.ly/vowVlV (英語) を参照してください。

restrict(amp) のラムダに対する制約の 1 つは、参照によって変数をキャプチャできず (この記事の終わり近くの注意事項を参照)、ポインターもキャプチャできないことです。この制限に留意して、parallel_for_each の最後のコードを見てみると、次のように自問されるのではないでしょうか。「参照によるキャプチャもできず、ポインターもキャプチャできないのなら、一体どうやってラムダの結果、つまり望ましい副作用を確認するのだろう。ラムダが完了したら、値を基にキャプチャする変数への変更は、外側のコードに提供できないだろう。」

この疑問の答えは、既におなじみの array_view 型を使用することです。array_view オブジェクトは、ラムダで値を基にキャプチャできます。これが、データを受け渡しするメカニズムです。array_view オブジェクトを使用して、実際のコンテナーをラップしたうえで、アクセスや値の設定のためにラムダで array_view オブジェクトをキャプチャし、parallel_for_each を呼び出した後で、適切な array_view オブジェクトにアクセスします。

まとめ

新しい知識を得たところで、前の CPU を使ってシリアルに行う行列加算のコード (array_view、extent、および index を使用したコード) に戻り、15 ~ 22 行目を次のコードに置き換えます。

15     parallel_for_each(e, [=](index<2> idx) restrict(amp)
16     {
19       c[idx] = a[idx] + b[idx];
22     });

19 行目は変わりませんが、エクステント範囲内の、手動のインデックス オブジェクト作成がある二重の入れ子になったループを、parallel_for_each 関数の呼び出しに置き換えています。

独自のメモリを持つ個別のアクセラレータを操作する場合、parallel_for_each に渡す array_view オブジェクトがラムダでキャプチャされると、基盤のデータがアクセラレータのグローバル メモリにコピーされます。同様に、parallel_for_each の呼び出し後に、array_view オブジェクト (この例では c) からデータにアクセスすると、データはアクセラレータからホスト メモリにコピーされます。

(array_view ではなく) 元のコンテナー vC から array_view c の結果にアクセスする場合は、array_view オブジェクトの synchronize メソッドを呼び出すと便利です。array_view デストラクターによって自動的に synchronize が呼び出されるため、コードは現状のままでも実行されますが、この方法では例外が発生しても失われるため、明示的に synchronize を呼び出すことをお勧めします。そこで、次のように、parallel_for_each 呼び出しの後の任意の場所に、ステートメントを追加します。

23          c.synchronize();

この逆の処理 (元のコンテナーのデータが変更されている場合に、最新のデータが array_view に格納されているようにする) には、refresh メソッドを利用します。

さらに重要なことは、データを (通常) PCIe バス全体にコピーするのは非常に負荷が高いため、必要な方向にのみデータをコピーすることをお勧めします。前のコードの 11 ~ 13 行目を変更することで、array_view オブジェクト a および b の基盤のデータは、アクセラレータにコピーされる必要があること (ただし、書き戻しはされない)、および array_view c の基盤のデータはアクセラレータにコピーされる必要がないことを指定できます。次のスニペットのように変更します。

11          array_view<const int, 2> a(e, vA), b(e, vB);
12          array_view<int, 2> c(e, vC);
13          c.discard_data();

ただし、これらの変更を行っても、この行列加算アルゴリズムは、データ コピーのオーバーヘッドを相殺するほど計算処理を集中的には行わないため、C++ AMP による並列化の対象には適しはいません。ここでは、基本を説明するために使用しています。

それでも、このシンプルなサンプルを一貫して使用することで、実際にメリットが得られるほど計算負荷の高い、他のアルゴリズムを並列化するスキルを習得していただけたと思います。そのような負荷の高いアルゴリズムとしては行列乗算があります。何のコメントがなくても、次に示す、行列乗算アルゴリズムの簡潔なシリアル実装を理解できることを確認してください。

void MatMul(vector<int>& vC, const vector<int>& vA,
  const vector<int>& vB, int M, int N, int W)
{
  for (int row = 0; row < M; row++)
  {
    for (int col = 0; col < N; col++)
    {
      int sum = 0;
      for(int i = 0; i < W; i++)
        sum += vA[row * W + i] * vB[i * N + col];
      vC[row * N + col] = sum;
    }
  }
}

これに対応する以下の C++ AMP 実装も同様です。

array_view<const int, 2> a(M, W, vA), b(W, N, vB);
array_view<int, 2> c(M, N, vC);
c.discard_data();
parallel_for_each(c.extent, [=](index<2> idx) restrict(amp)
{
  int row = idx[0]; int col = idx[1];
  int sum = 0;
  for(int i = 0; i < b.extent[0]; i++)
    sum += a(row, i) * b(i, col);
  c[idx] = sum;
});
c.synchronize();

筆者のノート PC では、C++ AMP の行列乗算では、M=N=W=1024 の場合、シリアルに実行する CPU コードに比べて、パフォーマンスが 40 倍以上向上します。

これで基本はすべて理解できたので、今度は、C++ AMP を使用してアルゴリズムを実装したら、そのアルゴリズムを実行するアクセラレータがどのように選択されるのかを見ていきましょう。

accelerator と accelerator_view

concurrency 名前空間には、新しい accelerator 型が含まれています。これは、システム上で C++ AMP ランタイムが使用できるデバイスを表します。初期リリースでは、このデバイスは、DirectX 11 ドライバー (または DirectX エミュレーター) がインストールされているハードウェアです。

C++ AMP ランタイムの起動時に、すべてのアクセラレータが列挙され、内部ヒューリスティックに基づき、既定として 1 つが選ばれます。これまでのすべてのコード サンプルで、アクセラレータを直接処理する必要がなかったのは、このためです。既定のアクセラレータが自動で選択されていました。アクセラレータを列挙して、自分で既定のアクセラレータを決めるとしても、図 3 のコードのように、非常に簡単です (このコードは説明がなくてもおわかりになると思います)。

図 3 アクセラレータの選択

26 accelerator pick_accelerator()
27 {
28   // Get all accelerators known to the C++ AMP runtime
29   vector<accelerator> accs = accelerator::get_all();
30
31   // Empty ctor returns the one picked by the runtime by default
32   accelerator chosen_one;
33
34   // Choose one; one that isn't emulated, for example
35   auto result =
36     std::find_if(accs.begin(), accs.end(), [] (accelerator acc)
37   {
38     return !acc.is_emulated; //.supports_double_precision
39   });
40   if (result != accs.end())
41     chosen_one = *(result); // else not shown
42
43   // Output its description (tip: explore the other properties)
44   std::wcout << chosen_one.description << std::endl;
45
46   // Set it as default ... can only call this once per process
47   accelerator::set_default(chosen_one.device_path);
48
49   // ... or just return it
50   return chosen_one;
51 }

38 行目で、多数ある accelerator プロパティの 1 つを照会しています。他のプロパティについては、図 4 を参照してください。

accelerator and accelerator_view Classes
図 4 accelerator クラスと accelerator_view クラス

各種アクセラレータ用に複数の parallel_for_each 呼び出しが必要になるか、なんらかの理由で、プロセス全体で使用される既定のアクセラレータを設定するよりもさらに明示的に指定する場合は、accelerator_view オブジェクトを parallel_for_each に渡す必要があります。parallel_for_each には、最初のパラメーターとして accelerator_view を受け取るオーバーロードがあるために、このような操作を実現できます。accelerator_view オブジェクトは、次の例のように、accelerator で default_view を呼び出すだけで取得できます。

accelerator_view acc_vw = pick_accelerator().default_view;

DirectX 11 ハードウェア以外に、C++ AMP によって利用できる特別なアクセラレータが 3 種類あります。

  • direct3d_ref: 正確性のデバッグには有用ですが、実際のハードウェアよりもかなり低速になるため、運用環境では役立ちません。
  • direct3d_warp: 現在のマルチコアおよびストリーム SIMD 拡張機能を使用する CPU で、C++ AMP コードを実行するためのフォールバック ソリューションです。
  • cpu_accelerator: このリリースでは、C++ AMP コードはまったく実行できません。ステージング配列 (高度な最適化手法) のセットアップにのみ有用です。ステージング配列については、この記事の範囲外になりますが、ブログ記事 bit.ly/vRksnn (英語) で説明されています。

タイリングと参考資料

ここで説明していない最も重要なトピックは、タイリングです。

シナリオの観点では、タイリングの場合、これまで説明したコーディング手法とは桁違いのパフォーマンス向上が得られ、(可能性としては) さらにパフォーマンスを改善できます。API の観点では、タイリングは、tiled_index 型と tiled_extent 型、および tile_barrier 型と tile_static ストレージ クラスで構成されます。tiled_extent オブジェクトを受け取る parallel_for_each のオーバーロードもあります。tiled_extent オブジェクトのラムダは、tiled_index オブジェクトを受け取ります。そのラムダ内では、tile_barrier オブジェクトと tile_static 変数を使用できます。タイリングついては、C++ AMP についての 2 つ目の記事で説明します。

その他に、ブログ記事やオンラインの MSDN ドキュメントに自習できるトピックがあります。

  • <amp_math.h> は、高精度の数学関数用と、高速ですが精度が落ちる数学関数用の 2 つの名前空間が含まれる数学ライブラリです。ハードウェアの機能とシナリオの要件を基に選択します。
  • <amp_graphics.h> と <amp_short_vectors.h>、およびいくつかの DirectX 相互運用関数をグラフィック プログラミングの作業に利用できます。
  • concurrency::array は、アクセラレータにバインドされるコンテナー データ型で、ほぼ array_view と同じインターフェイスがあります。この型は、parallel_for_each に渡されるラムダで、参照によりキャプチャする必要がある 2 つの型の 1 つです (もう 1 つの型は graphics 名前空間の texture)。これが、先ほど触れた注意事項です。
  • スレッド間同期のアトミックなど、DirectX 組み込みのサポート。
  • Visual Studio 11 での GPU デバッグおよびプロファイル。

将来も使い続けることができる取り組みを

今回は、GPU を使用してアプリケーションのパフォーマンスを向上できるようなアルゴリズムの表現を可能にする、最新の C++ データ並列 API を紹介しました。C++ AMP は、まだ目にしたことのないハードウェアでも、開発者の取り組みを使い続けることができるように設計しています。

ここでは、アクセラレータ (accelerator および accelerator_view オブジェクトにより指定可能) で ("restrict(amp)" のラムダ以降の) コードを実行できるようにするグローバル関数と合わせて、いくつかの型 (array_view、extent、および index) を、多次元データの操作に利用できることを学習しました。

Microsoft Visual C++ 実装だけでなく、C++ AMP は、だれでも、どのプラットフォームにでも実装できるオープンな仕様としてコミュニティに提供しています。

Daniel Moth は、マイクロソフトの開発部門に所属するプリンシパル プログラム マネージャーです。彼のブログは、danielmoth.com/Blog からご覧になれます。

この記事のレビューに協力してくれた技術スタッフの Steve DeitzYossi LevanoniRobin Reynolds-HaertleStephen Toub、および Weirong Zhu に心より感謝いたします。