注
C++ AMP ヘッダーは、Visual Studio 2022 バージョン 17.0 以降では非推奨です。
AMP ヘッダーを含めると、ビルド エラーが発生します。 警告をサイレント状態にするには、AMP ヘッダーを含める前に _SILENCE_AMP_DEPRECATION_WARNINGS を定義します。
C++ Accelerated Massive Parallelism (C++ AMP) は、ディスクリート グラフィックス カード上のグラフィックス処理装置 (GPU) などのデータ並列ハードウェアを利用して、C++ コードの実行を高速化します。 C++ AMP を使用すると、異種ハードウェアでの並列処理を使用して実行を高速化できるように、多次元データ アルゴリズムをコーディングできます。 C++ AMP プログラミング モデルには、多次元配列、インデックス作成、メモリ転送、タイリング、数学関数ライブラリが含まれています。 C++ AMP 言語拡張機能を使用して、データを CPU から GPU に戻す方法を制御して、パフォーマンスを向上させることができます。
システム要件
Windows 7 以降
Windows Server 2008 R2 から Visual Studio 2019 まで。
DirectX 11 機能レベル 11.0 以降のハードウェア
ソフトウェア エミュレーターでのデバッグには、Windows 8 または Windows Server 2012 が必要です。 ハードウェアでデバッグするには、グラフィックス カードのドライバーをインストールする必要があります。 詳細については、「デバッグ GPU コード」を参照してください。
注: ARM64 では、AMP は現在サポートされていません。
はじめに
次の 2 つの例は、C++ AMP の主要なコンポーネントを示しています。 2 つの 1 次元配列の対応する要素を追加するとします。 たとえば、 {1, 2, 3, 4, 5} と {6, 7, 8, 9, 10} を追加して {7, 9, 11, 13, 15}を取得できます。 C++ AMP を使用しない場合は、次のコードを記述して数値を追加し、結果を表示できます。
#include <iostream>
void StandardMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5];
for (int idx = 0; idx < 5; idx++)
{
sumCPP[idx] = aCPP[idx] + bCPP[idx];
}
for (int idx = 0; idx < 5; idx++)
{
std::cout << sumCPP[idx] << "\n";
}
}
コードの重要な部分は次のとおりです。
データ: データは 3 つの配列で構成されます。 すべて同じランク (1) と長さ (5) です。
イテレーション: 最初の
forループは、配列内の要素を反復処理するためのメカニズムを提供します。 合計を計算するために実行するコードは、最初のforブロックに含まれています。Index:
idx変数は、配列の個々の要素にアクセスします。
C++ AMP を使用して、代わりに次のコードを記述できます。
#include <amp.h>
#include <iostream>
using namespace concurrency;
const int size = 5;
void CppAmpMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[size];
// Create C++ AMP objects.
array_view<const int, 1> a(size, aCPP);
array_view<const int, 1> b(size, bCPP);
array_view<int, 1> sum(size, sumCPP);
sum.discard_data();
parallel_for_each(
// Define the compute domain, which is the set of threads that are created.
sum.extent,
// Define the code to run on each thread on the accelerator.
[=](index<1> idx) restrict(amp) {
sum[idx] = a[idx] + b[idx];
}
);
// Print the results. The expected output is "7, 9, 11, 13, 15".
for (int i = 0; i < size; i++) {
std::cout << sum[i] << "\n";
}
}
同じ基本要素が存在しますが、C++ AMP コンストラクトが使用されます。
データ: C++ 配列を使用して、3 つの C++ AMP array_view オブジェクトを構築します。
array_viewオブジェクトを作成するには、データ値、ランク、要素型、および各ディメンションのarray_viewオブジェクトの長さの 4 つの値を指定します。 ランクと型は型パラメーターとして渡されます。 データと長さはコンストラクター パラメーターとして渡されます。 この例では、コンストラクターに渡される C++ 配列は 1 次元です。 ランクと長さは、array_viewオブジェクト内のデータの四角形の形状を構築するために使用され、データ値を使用して配列を塗りつぶします。 ランタイム ライブラリには、 クラスに似たインターフェイスを持つarray_viewも含まれており、この記事で後述します。イテレーション: parallel_for_each関数 (C++ AMP) は、データ要素または コンピューティング ドメインを反復処理するためのメカニズムを提供します。 この例では、コンピューティング ドメインは
sum.extentで指定されています。 実行するコードは、ラムダ式または カーネル関数に含まれています。restrict(amp)は、C++ AMP が高速化できる C++ 言語のサブセットのみが使用されることを示します。Index: index クラス 変数 (
idx) は、array_viewオブジェクトのランクと一致する 1 のランクで宣言されます。 インデックスを使用すると、array_viewオブジェクトの個々の要素にアクセスできます。
データの整形とインデックス作成: インデックスとエクステント
カーネル コードを実行する前に、データ値を定義し、データの形状を宣言する必要があります。 すべてのデータは配列 (四角形) として定義され、任意のランク (次元数) を持つ配列を定義できます。 データには、任意のディメンションの任意のサイズを指定できます。
index クラス
index クラスは、各次元の原点からのオフセットを 1 つのオブジェクトにカプセル化することによって、arrayまたはarray_view オブジェクト内の位置を指定します。 配列内の場所にアクセスすると、整数インデックスのリストではなく、 index オブジェクトをインデックス作成演算子 []に渡します。
array::operator() 演算子または array_view::operator() 演算子を使用して、各次元の要素にアクセスできます。
次の例では、1 次元の array_view オブジェクトの 3 番目の要素を指定する 1 次元インデックスを作成します。 インデックスは、 array_view オブジェクトの 3 番目の要素を出力するために使用されます。 出力は 3 です。
int aCPP[] = {1, 2, 3, 4, 5};
array_view<int, 1> a(5, aCPP);
index<1> idx(2);
std::cout << a[idx] << "\n";
// Output: 3
次の例では、2 次元の array_view オブジェクトの行 = 1 と列 = 2 の要素を指定する 2 次元インデックスを作成します。
index コンストラクターの最初のパラメーターは行コンポーネントで、2 番目のパラメーターは列コンポーネントです。 出力は 6 です。
int aCPP[] = {1, 2, 3, 4, 5, 6};
array_view<int, 2> a(2, 3, aCPP);
index<2> idx(1, 2);
std::cout <<a[idx] << "\n";
// Output: 6
次の例では、3 次元の array_view オブジェクトで、深さ = 0、行 = 1、列 = 3 の要素を指定する 3 次元インデックスを作成します。 最初のパラメーターが深度コンポーネント、2 番目のパラメーターが行コンポーネント、3 番目のパラメーターが列コンポーネントであることに注意してください。 出力は 8 です。
int aCPP[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
array_view<int, 3> a(2, 3, 4, aCPP);
// Specifies the element at 3, 1, 0.
index<3> idx(0, 1, 3);
std::cout << a[idx] << "\n";
// Output: 8
エクステントクラス
エクステント クラスは、arrayまたはarray_view オブジェクトの各ディメンションのデータの長さを指定します。 エクステントを作成し、それを使用して array または array_view オブジェクトを作成できます。 既存の array または array_view オブジェクトのエクステントを取得することもできます。 次の使用例は、 array_view オブジェクトの各次元のエクステントの長さを出力します。
int aCPP[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
// There are 3 rows and 4 columns, and the depth is two.
array_view<int, 3> a(2, 3, 4, aCPP);
std::cout << "The number of columns is " << a.extent[2] << "\n";
std::cout << "The number of rows is " << a.extent[1] << "\n";
std::cout << "The depth is " << a.extent[0] << "\n";
std::cout << "Length in most significant dimension is " << a.extent[0] << "\n";
次の例では、前の例のオブジェクトと同じ次元を持つarray_view オブジェクトを作成しますが、この例では、extent コンストラクターで明示的なパラメーターを使用する代わりに、array_view オブジェクトを使用します。
int aCPP[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
extent<3> e(2, 3, 4);
array_view<int, 3> a(e, aCPP);
std::cout << "The number of columns is " << a.extent[2] << "\n";
std::cout << "The number of rows is " << a.extent[1] << "\n";
std::cout << "The depth is " << a.extent[0] << "\n";
アクセラレータへのデータの移動: 配列とarray_view
アクセラレータにデータを移動するために使用される 2 つのデータ コンテナーは、ランタイム ライブラリで定義されます。 これらは 配列クラス と array_view クラスです。
array クラスは、オブジェクトの構築時にデータのディープ コピーを作成するコンテナー クラスです。
array_view クラスは、カーネル関数がデータにアクセスするときにデータをコピーするラッパー クラスです。 ソース デバイスでデータが必要な場合は、データがコピーされます。
array クラス
array オブジェクトが構築されると、データ セットへのポインターを含むコンストラクターを使用すると、アクセラレータにデータのディープ コピーが作成されます。 カーネル関数は、アクセラレータのコピーを変更します。 カーネル関数の実行が完了したら、データをソース データ構造にコピーし直す必要があります。 次の例では、ベクトル内の各要素に 10 を乗算します。 カーネル関数が完了すると、 vector conversion operator を使用してデータがベクター オブジェクトにコピーされます。
std::vector<int> data(5);
for (int count = 0; count <5; count++)
{
data[count] = count;
}
array<int, 1> a(5, data.begin(), data.end());
parallel_for_each(
a.extent,
[=, &a](index<1> idx) restrict(amp) {
a[idx] = a[idx]* 10;
});
data = a;
for (int i = 0; i < 5; i++)
{
std::cout << data[i] << "\n";
}
array_view クラス
array_viewには、array クラスとほぼ同じメンバーがありますが、基になる動作は同じではありません。
array_view コンストラクターに渡されたデータは、array コンストラクターと同じように GPU ではレプリケートされません。 代わりに、カーネル関数の実行時にデータがアクセラレータにコピーされます。 そのため、同じデータを使用する 2 つの array_view オブジェクトを作成した場合、両方の array_view オブジェクトが同じメモリ領域を参照します。 これを行う場合は、マルチスレッド アクセスを同期する必要があります。
array_view クラスを使用する主な利点は、必要な場合にのみデータを移動することです。
配列とarray_viewの比較
次の表は、 array クラスと array_view クラスの類似点と相違点をまとめたものです。
| 説明 | 配列クラス | array_view クラス |
|---|---|---|
| ランクが決定されたとき | コンパイル時。 | コンパイル時。 |
| エクステントが決定されるとき | 実行時。 | 実行時。 |
| 形状 | 長方 形。 | 長方 形。 |
| データストレージ | データ コンテナーです。 | データ ラッパーです。 |
| Copy | 定義時における明示的コピーとディープ コピー。 | カーネル関数によってアクセスされる場合の暗黙的なコピー。 |
| データの取得 | 配列データを CPU スレッド上のオブジェクトにコピーし直します。 |
array_view オブジェクトに直接アクセスするか、array_view::synchronize メソッドを呼び出して、元のコンテナー上のデータに引き続きアクセスします。 |
配列とarray_viewを持つ共有メモリ
共有メモリは、CPU とアクセラレータの両方からアクセスできるメモリです。 共有メモリを使用すると、CPU とアクセラレータの間でデータをコピーするオーバーヘッドが排除または大幅に削減されます。 メモリは共有されますが、CPU とアクセラレータの両方で同時にアクセスすることはできません。これにより、未定義の動作が発生します。
array オブジェクトを使用して、関連付けられているアクセラレータが共有メモリをサポートしている場合に、共有メモリの使用をきめ細かく制御できます。 アクセラレータが共有メモリをサポートするかどうかは、アクセラレータの supports_cpu_shared_memory プロパティによって決まります。これは、共有メモリがサポートされている場合に true を返します。 共有メモリがサポートされている場合、アクセラレータでのメモリ割り当ての既定の access_type列挙 は、 default_cpu_access_type プロパティによって決定されます。 既定では、arrayオブジェクトとarray_view オブジェクトは、プライマリ関連のaccess_typeと同じacceleratorを受け取ります。
の array プロパティを明示的に設定することで、共有メモリの使用方法をきめ細かく制御できるため、計算カーネルのメモリ アクセス パターンに基づいて、ハードウェアのパフォーマンス特性に合わせてアプリを最適化できます。
array_viewは、関連付けられているcpu_access_typeと同じarrayを反映します。または、array_viewがデータ ソースなしで構築された場合、そのaccess_typeには、最初にストレージを割り当てる環境が反映されます。 つまり、ホスト (CPU) によって最初にアクセスされた場合は、CPU データ ソース経由で作成されたかのように動作し、キャプチャによって関連付けられたaccess_typeのaccelerator_viewを共有します。ただし、accelerator_viewによって最初にアクセスされた場合は、そのarrayに作成されたaccelerator_viewを介して作成されたかのように動作し、arrayのaccess_typeを共有します。
次のコード例は、既定のアクセラレータが共有メモリをサポートしているかどうかを判断し、異なるcpu_access_type構成を持つ複数の配列を作成する方法を示しています。
#include <amp.h>
#include <iostream>
using namespace Concurrency;
int main()
{
accelerator acc = accelerator(accelerator::default_accelerator);
// Early out if the default accelerator doesn't support shared memory.
if (!acc.supports_cpu_shared_memory)
{
std::cout << "The default accelerator does not support shared memory" << std::endl;
return 1;
}
// Override the default CPU access type.
acc.default_cpu_access_type = access_type_read_write
// Create an accelerator_view from the default accelerator. The
// accelerator_view inherits its default_cpu_access_type from acc.
accelerator_view acc_v = acc.default_view;
// Create an extent object to size the arrays.
extent<1> ex(10);
// Input array that can be written on the CPU.
array<int, 1> arr_w(ex, acc_v, access_type_write);
// Output array that can be read on the CPU.
array<int, 1> arr_r(ex, acc_v, access_type_read);
// Read-write array that can be both written to and read from on the CPU.
array<int, 1> arr_rw(ex, acc_v, access_type_read_write);
}
データに対するコードの実行: parallel_for_each
parallel_for_each関数は、arrayまたはarray_view オブジェクト内のデータに対してアクセラレータで実行するコードを定義します。 このトピックの概要から次のコードを検討してください。
#include <amp.h>
#include <iostream>
using namespace concurrency;
void AddArrays() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};
array_view<int, 1> a(5, aCPP);
array_view<int, 1> b(5, bCPP);
array_view<int, 1> sum(5, sumCPP);
parallel_for_each(
sum.extent,
[=](index<1> idx) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
);
for (int i = 0; i < 5; i++) {
std::cout << sum[i] << "\n";
}
}
parallel_for_each メソッドは、コンピューティング ドメインとラムダ式の 2 つの引数を受け取ります。
コンピューティング ドメインは、並列実行のために作成するスレッドのセットを定義するextent オブジェクトまたはtiled_extent オブジェクトです。 コンピューティング ドメイン内の要素ごとに 1 つのスレッドが生成されます。 この場合、 extent オブジェクトは 1 次元であり、5 つの要素を持ちます。 そのため、5 つのスレッドが開始されます。
ラムダ式は、各スレッドで実行するコードを定義します。 ラムダ式の本体が値でキャプチャされたすべての変数にアクセスすることを指定するcapture句[=]。この場合、a、b、およびsumが該当します。 この例では、パラメーター リストによって、indexという名前の 1 次元idx変数が作成されます。
idx[0]の値は最初のスレッドでは 0 で、後続の各スレッドで 1 ずつ増加します。
restrict(amp)は、C++ AMP が高速化できる C++ 言語のサブセットのみが使用されることを示します。 restrict 修飾子を持つ関数に関する制限事項については、 restrict (C++ AMP) で説明されています。 詳細については、「 ラムダ式の構文」を参照してください。
ラムダ式には、実行するコードを含めたり、別のカーネル関数を呼び出したりすることができます。 カーネル関数には、 restrict(amp) 修飾子を含める必要があります。 次の例は前の例と同じですが、別のカーネル関数を呼び出します。
#include <amp.h>
#include <iostream>
using namespace concurrency;
void AddElements(
index<1> idx,
array_view<int, 1> sum,
array_view<int, 1> a,
array_view<int, 1> b) restrict(amp) {
sum[idx] = a[idx] + b[idx];
}
void AddArraysWithFunction() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};
array_view<int, 1> a(5, aCPP);
array_view<int, 1> b(5, bCPP);
array_view<int, 1> sum(5, sumCPP);
parallel_for_each(
sum.extent,
[=](index<1> idx) restrict(amp) {
AddElements(idx, sum, a, b);
}
);
for (int i = 0; i < 5; i++) {
std::cout << sum[i] << "\n";
}
}
コードの高速化: タイルとバリア
タイリングを使用すると、追加の高速化を得ることができます。 タイリングは、スレッドを等しい大きさの長方形のサブセット、すなわちタイルに分割します。 データ セットとコーディングするアルゴリズムに基づいて、適切なタイル サイズを決定します。 スレッドごとに、全体またはarrayに対するデータ要素のarray_viewな場所にアクセスし、タイルに対するローカルの場所にアクセスできます。 ローカル インデックス値を使用すると、インデックス値をグローバルからローカルに変換するコードを記述する必要がないため、コードが簡略化されます。 タイリングを使用するには、 メソッドのコンピューティング ドメインで parallel_for_each メソッドを呼び出し、ラムダ式でtiled_index オブジェクトを使用します。
一般的なアプリケーションでは、タイル内の要素は何らかの方法で関連しており、コードはタイル全体の値にアクセスして追跡する必要があります。 これを行うには、 tile_static キーワード と tile_barrier::wait メソッド を使用します。
tile_static キーワードを持つ変数には、タイル全体のスコープがあり、各タイルに対して変数のインスタンスが作成されます。 変数へのタイル スレッド アクセスの同期を処理する必要があります。
tile_barrier::wait メソッドは、タイル内のすべてのスレッドがtile_barrier::waitの呼び出しに達するまで、現在のスレッドの実行を停止します。 そのため、tile_static変数を使用して、タイル全体で値 を 蓄積できます。 その後、すべての値へのアクセスを必要とする計算を完了できます。
次の図は、タイルに配置されたサンプリング データの 2 次元配列を表しています。
次のコード例では、前の図のサンプリング データを使用します。 このコードは、タイル内の各値をタイル内の値の平均に置き換えます。
// Sample data:
int sampledata[] = {
2, 2, 9, 7, 1, 4,
4, 4, 8, 8, 3, 4,
1, 5, 1, 2, 5, 2,
6, 8, 3, 2, 7, 2};
// The tiles:
// 2 2 9 7 1 4
// 4 4 8 8 3 4
//
// 1 5 1 2 5 2
// 6 8 3 2 7 2
// Averages:
int averagedata[] = {
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
};
array_view<int, 2> sample(4, 6, sampledata);
array_view<int, 2> average(4, 6, averagedata);
parallel_for_each(
// Create threads for sample.extent and divide the extent into 2 x 2 tiles.
sample.extent.tile<2,2>(),
[=](tiled_index<2,2> idx) restrict(amp) {
// Create a 2 x 2 array to hold the values in this tile.
tile_static int nums[2][2];
// Copy the values for the tile into the 2 x 2 array.
nums[idx.local[1]][idx.local[0]] = sample[idx.global];
// When all the threads have executed and the 2 x 2 array is complete, find the average.
idx.barrier.wait();
int sum = nums[0][0] + nums[0][1] + nums[1][0] + nums[1][1];
// Copy the average into the array_view.
average[idx.global] = sum / 4;
});
for (int i = 0; i <4; i++) {
for (int j = 0; j <6; j++) {
std::cout << average(i,j) << " ";
}
std::cout << "\n";
}
// Output:
// 3 3 8 8 3 3
// 3 3 8 8 3 3
// 5 5 2 2 4 4
// 5 5 2 2 4 4
数学ライブラリ
C++ AMP には、2 つの数学ライブラリが含まれています。
Concurrency::p recise_math 名前空間の倍精度ライブラリは、倍精度関数をサポートします。 また、単精度関数のサポートも提供しますが、ハードウェアでの倍精度サポートは引き続き必要です。
C99 仕様 (ISO/IEC 9899) に準拠しています。 アクセラレータは、完全な倍精度をサポートする必要があります。
アクセラレータ::supports_double_precision データ メンバーの値を確認することで、ダブル精度をサポートするかどうかを判断できます。
Concurrency::fast_math 名前空間の高速数学ライブラリには、別の数学関数のセットが含まれています。 これらの関数は、 float オペランドのみをサポートしますが、より迅速に実行できますが、倍精度演算ライブラリの関数ほど正確ではありません。 関数は <amp_math.h> ヘッダー ファイルに含まれており、すべて restrict(amp) で宣言されます。
<cmath> ヘッダー ファイル内の関数は、fast_math名前空間とprecise_math名前空間の両方にインポートされます。
restrict キーワードは、<cmath> バージョンと C++ AMP バージョンを区別するために使用されます。 次のコードでは、計算ドメイン内の各値の fast メソッドを使用して、10 を底とする対数を計算します。
#include <amp.h>
#include <amp_math.h>
#include <iostream>
using namespace concurrency;
void MathExample() {
double numbers[] = { 1.0, 10.0, 60.0, 100.0, 600.0, 1000.0 };
array_view<double, 1> logs(6, numbers);
parallel_for_each(
logs.extent,
[=] (index<1> idx) restrict(amp) {
logs[idx] = concurrency::fast_math::log10(numbers[idx]);
}
);
for (int i = 0; i < 6; i++) {
std::cout << logs[i] << "\n";
}
}
グラフィックス ライブラリ
C++ AMP には、高速グラフィックス プログラミング用に設計されたグラフィックス ライブラリが含まれています。 このライブラリは、ネイティブ グラフィックス機能をサポートするデバイスでのみ使用されます。 メソッドは Concurrency::graphics 名前空間 にあり、 <amp_graphics.h> ヘッダー ファイルに含まれています。 グラフィックス ライブラリの主なコンポーネントは次のとおりです。
texture クラス: テクスチャ クラスを使用して、メモリまたはファイルからテクスチャを作成できます。 テクスチャはデータを含み、割り当てとコピーの構築に関して C++ 標準ライブラリのコンテナーに似ているため、配列に似ています。 詳細については、「 C++ 標準ライブラリ コンテナー」を参照してください。
textureクラスのテンプレート パラメーターは、要素の型とランクです。 ランクには、1、2、または 3 を指定できます。 要素型には、この記事で後述する短いベクター型のいずれかを指定できます。writeonly_texture_view クラス: 任意のテクスチャへの書き込み専用アクセスを提供します。
Short Vector Library:
int、uint、float、double、 norm、または unorm に基づく長さ 2、3、4 の短いベクター型のセットを定義します。
ユニバーサル Windows プラットフォーム (UWP) アプリ
他の C++ ライブラリと同様に、UWP アプリで C++ AMP を使用できます。 これらの記事では、C++、C#、Visual Basic、または JavaScript を使用して作成されたアプリに C++ AMP コードを含める方法について説明します。
C++ AMP とコンカレンシー ビジュアライザー
コンカレンシー ビジュアライザーには、C++ AMP コードのパフォーマンス分析のサポートが含まれています。 これらの記事では、次の機能について説明します。
パフォーマンスに関する推奨事項
符号なし整数の剰余と除算は、符号付き整数の剰余と除算よりもパフォーマンスが大幅に向上します。 可能な場合は、符号なし整数を使用することをお勧めします。
こちらも参照ください
C++ AMP (C++ Accelerated Massive Parallelism)
ラムダ式の構文
リファレンス (C++ AMP)
Parallel Programming in Native Code blog (ネイティブ コードでの並行プログラミング ブログ)