【日本語訳】C++AMPの概要

2012年3月3日 11:57 pm | カテゴリー: PCSoftware | コメントを残す
タグ: ,

この記事については,CC-BYライセンスとしますので,(マイクロソフトに怒られない範囲で)好きにいじってもらって構いません.

—–

VisualStudio11では,C++AMPという新しいGPGPUのための開発環境が作られました.C++AMPはDirectComputeを利用しますが,DirectComputeの面倒な手続きが大幅に少なくなっています.またCUDAのように,CPUのコードとGPUのコードが混合して書けて,デバッグもできます.一応,仕様書が一般公開されているので,将来GCCやLLVMが対応…してくれたらいいなぁ.

MSDNにC++AMPのOverviewが公開されていますOverview of C++ Accelerated Massive Parallelism (C++ AMP)).せっかくなので,日本語訳してみました(厳密な日本語役ではなく,どうでもよさそうな部分は飛ばしつつ,意訳したり書き換えたりしています).中の人の英語能力がカスなので(超重要),元の文と照らし合わせることをお勧めします.

なお,C++AMPの正式な仕様はまだ決まっていません.まだ仕様変更が行われているので,今後この情報は参考にならない可能性があります.

—–

C++ Accelerated Massive Parallelism (C++ AMP)は,外付けGPUのようなデータを並列に処理するハードウェアを使って,C++のコードの実行を高速化します.C++AMPにより,並列処理によるヘテロジニアスなハードウェアで高速実行できる,多次元データアルゴリズムが書けるようになります.C++AMPのプログラミングモデルは,多次元配列,インデックス,メモリ転送,タイル,数学関数ライブラリを含みます.C++AMP言語拡張とコンパイラ制約により,どのデータがCPUからGPU,GPUからCPUへ移動 するか制御できるので,パフォーマンスを向上させることができます.

—–

【システム要件】

Windows7以降かWindows Server 2008 R2以降

最大限利用するにはDirectX 11が必要

—–

【紹介】

配列{1, 2, 3, 4, 5}と配列{6, 7, 8, 9, 10}を加算して{7, 9, 11, 13, 15}を出力するプログラム

#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";
    }
}
  • データ:aCPP,bCPP,sumCPPの3つの配列があり,どれもランク1(注:1次元配列のことを指すと思われる),長さ5
  • 反復:最初のforループは,配列内の要素を通して反復するメカニズムを提供する.最初のforブロック内に合計を計算するコードがある.
  • インデックス:変数idxは配列の個々の要素にアクセスする.

C++AMPで書くと…

#include <amp.h>
#include <iostream>
using namespace concurrency;

void CampMethod() {
    int aCPP[] = {1, 2, 3, 4, 5};
    int bCPP[] = {6, 7, 8, 9, 10};
    int sumCPP[5] = {0, 0, 0, 0, 0};

    // C++ AMP のオブジェクトを作る
    array_view<int, 1> a(5, aCPP);
    array_view<int, 1> b(5, bCPP);
    array_view<int, 1> sum(5, sumCPP);

    parallel_for_each(
        // 作成されたスレッド集合の演算領域を定義する(注:CUDAのdim3に近く,OpenCLのglobal_sizeに相当)
        sum.extent,
        // アクセラレータが各スレッドで実行するコードを定義する(注:CUDAやOpenCLのカーネル関数に相当)
        [=](index<1> idx) restrict(amp)
        {
            sum[idx] = a[idx] + b[idx];
        }
    );

    for (int i = 0; i < 5; i++) {
        std::cout << sum[i] << "\n";
    }
}

基本は同じですが,C++AMPの構造が使われています.

  •  データ:C++の配列からC++AMPのarray_viewオブジェクトを構築する.構築には,データ,ランク,型,各次元の長さ,の4つのパラメータを指定する.データと長さはコンストラクタに渡す.この例では,コンストラクタに渡すC++配列は1次元である.ランクと長さは,array_viewオブジェクト内の角胴形(原文:rectangular shape)を構築するために使われ  ,値は配列を埋めるために使われる.ランタイムライブラリには,array_viewに似たインターフェイスのarrayクラスがあり,これについては後で議論する.
  • 反復:parallel_for_each関数はデータ要素,すなわち演算領域(compute domain)を通して反復するためのメカニズムを提供する.この例では,演算領域がsum.extentにより決められる.実行したいコードはラムダ式,すなわちカーネル関数(kernel function)に含まれている.”restrict(amp)”は,C++のサブセットでしかないC++AMPが高速化されることを示す.
  • インデックス:変数idxはindexクラスであり,array_viewオブジェクトのランクに合致したあるランクで宣言される.インデックスを使い,array_viewオブジェクトの個々の要素にアクセスできる.

—–

【Shaping and Indexing Data : indexとextent】

カーネルコードを実行する前に,データ値の定義と,データの形を宣言しなければなりません.全てのデータは(四角形の)配列で定義され,そしてあらゆるランク(次元数)を持つ配列で定義されます.データはあらゆる次元内で任意のサイズをとることができます.使いやすいように,ランタイムライブラリには,3次元データ集合で使うあらかじめ決められた型と関数が用意されています.

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列目(注:先頭は0行0列目)の要素を示す2次元インデックスを作成します.indexクラスのコンストラクタの1つ目のパラメータが行,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次元インデックスを作成します.indexクラスのコンストラクタの1つ目のパラメータが深さ,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};
// x次元の長さは4,y次元の長さは3,z次元の長さは2
array_view<int, 3> a(2, 3, 4, aCPP);
// x = 3, y = 1, z = 0 の要素を指定
index<3> idx(0, 1, 3);
std::cout << a[idx] << "\n";

// Output: 8

extentクラス

extentクラスは,arrayまたはarray_viewオブジェクトの各次元内のデータの長さを規定します.あるextentを作り,arrayまたはarray_viewオブジェクトを作成するときにそのextentを使うことができます.同様に,arrayまたはarray_viewオブジェクト内に存在するextentを取り出すこともできます.下の例は,array_viewオブジェクトの各次元内の,extentの長さを出力します.

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};
// 3行4列,深さが2
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オブジェクトを作りますが,この例ではarray_viewコンストラクタ内の厳密なパラメータを使うのではなく,extemtオブジェクトを使います.

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};
concurrency::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とarray_view】

(注:ここでいうarrayはC++AMPのconcurrency::arrayであり,C++11のstd::arrayではないので注意)

2つのデータコンテナは,アクセラレータへデータを転送するためにランタイムライブラリで定義されます.これらはarrayクラスとarray_viewクラスです.arrayクラスは,オブジェクトがコンストラクトされたときにデータをディープコピーするコンテナクラスです.array_viewクラスは,カーネル関数がデータにアクセスするときにデータをコピーするラッパークラスです.

arrayクラス

arrayクラスがコンストラクトされたとき,もしデータ集合へのポインタを含んだコンストラクタを使うならば,ディープコピーされたデータをアクセラレータに作成します. カーネル関数はアクセラレータにあるデータコピーを更新します.カーネル関数の実行が完了したら,ホストにデータをコピーする必要があります.下の例は, 各要素を10倍します.カーネル関数が完了した後,データをベクタオブジェクトにコピーし戻すため,vector conversion operatorが使われる.

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とarray_viewの比較

下の表にarrayとarray_viewクラスの似ている点と異なる点を要約します.

arrayとarray_viewの比較

—–

【データを越えてコードを実行する: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スレッドは演算領域内の各要素のために生成されます.この場合,gridオブジェクトは1次元で5つの要素を持ちます.よって,5スレッドが開始します.各スレッドは演算領域内の全要素にアクセスしています.

ラムダ式は各スレッドで実行するコードを定義します.キャプチャ節([=]を指す)は,ラムダ式の本体が,全ての変数を値渡してアクセスすることを示します.この例では,パラメータリストが変数名idxの1次元indexを生成します.1スレッド目のidx[0]は0で,あとに続く各スレッドでは1ずつ増えます. “restrict(amp)”は,C++のサブセットでしかないC++AMPが高速化できることを示します .restrict修飾子のついた関数の制限については,Restriction Clause (C++ AMP)で説明しています.より詳細な情報は,Lambda Expression Syntaxを見てください.

ラムダ式は,実行するコードを含められるか,分割されたカーネル関数を呼び出すことができます.カーネル関数は必ず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";
    }
}

—–

【コードの単純化と高速化:タイルとバリア】

タイリングは,等しい四角のサブセットであるタイルにスレッドを分割します.データ集合やコードのアルゴリズムにより適切なタイルサイズを決めます.For each thread, you have access to the global location of a data element relative to the whole array or array_viewand access to the local location relative to the tile.(訳せないorz) ローカルなindex値を使うことは,コードを単純化します.なぜなら,インデックス値をグローバルからローカルへ変換するコードを書く必要がないからである.タイルを使うには,演算領域のparallel_for_eachメソッド内でextent::tileメソッドを呼び,ラムダ式内にtiled_indexオブジェクトを使います.

典型的ななアプリケーションでは, タイル内の要素はいくつかの方法で関連され,コードはタイルを跨ぐ値を絶えず注意してアクセスすべきです.これを成し遂げるために,tile_staticキーワードとtile_barrier::waitメソッドを使います(注:CUDAの__shared__と__synchronize()に相当すると思われる).tile_staticを持つ変数は,タイル全体にわたるスコープを持ち,変数の実体がタイルごとに生成される.tile_barrier::waitメソッドは,タイル内の全てのスレッドの実行が完了するまで,コードの実行を停止する.よって,tile_static変数により,タイルを跨いで値を蓄積できる.タイル内のすべてのスレッドの実行が完了したら,すべてのその値へのあらゆるアクセス要求の演算を完了できる.

下の図は,タイルに再配置されたサンプリングデータの2次元配列を意味します.

tile

下のサンプルコードは,上図のサンプリングデータを使う.コードは,タイル内の平均値によってタイル内の各値に置き換わります.

// サンプルデータ:
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};

// タイル:
// 2 2    9 7    1 4
// 4 4    8 8    3 4
//
// 1 5    1 2    5 2
// 6 8    3 2    7 2

// 平均:
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(
    // sample.extent分のスレッドを作成し,グリッドを2×2のタイルに分ける
    sample.extent.tile<2,2>(),
    [=](tiled_index<2,2> idx) restrict(amp)
    {
        // このタイル内の値を保持するための2×2の配列を作成する
        tile_static int nums[2][2];
        // 2×2の配列へタイルの値をコピーする
        nums[idx.local[1]][idx.local[0]] = sample[idx.global];
        // 全てのスレッドが実行され,2×2の配列が完了したら,平均を見つける
        idx.barrier.wait();
        int sum = nums[0][0] + nums[0][1] + nums[1][0] + nums[1][1];
        // 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::precise_math名前空間にある倍精度ライブラリは,倍精度の関数のサポートを提供します.これはC99 Specification (ISO/IEC 9899)でコンパイルします.最大限に高速化するため,アクセラレータが倍精度をサポートする必要があります.accelerator::supports_double_precision Dataメンバの値でチェックすることにより判定できます.Concurrency::fast_math名前空間にある高速数学関数には,別の数学関数の集合を含みます.floatオペランドのみをサポートするこれらの関数は,高速に実行できますが,倍精度数学ライブラリほどの精度はありません.この関数は<amp_math.h>ヘッダファイルに含まれており,全てrestrict(amp)で宣言されています.下のコードは,演算領域内の各値の常用対数を,高速な方法を使って計算します.

#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(logs[idx]);
        }
    );

    for (int i = 0; i < 6; i++) {
        std::cout << logs[i] << "\n";
    }
}

—–

【グラフィックライブラリ】

C++AMPはグラフィックスプログラミングを高速化するために設計されたライブラリを含みます.このライブラリは,ネイティブなグラフィック機能をサポートするデバイスでのみ使用できます.メソッドは,Concurrency::graphics名前空間にあり,<amp_graphics.h>ヘッダファイル内にあります.グラフィックスライブラリの主要なコンポーネントは次のようになっています.

  1. textureクラス:メモリやファイルからテクスチャを作成します.Textures are similar to arrays in that they are containers of data and they are similar to containers in the Standard Template Library with respect to assignment and copy construction(訳せないorz).詳細は,STL Containersを参照してください.textureクラスのテンプレートパラメータは要素の型とランクです.ランクは1,2,3のいずれかです.要素の型は1つの短いベクトル型で,後述します.
  2. writeonly_texture_viewクラス:このクラスは,あらゆるテクスチャへ書き込みのみのアクセスを提供します.
  3. Short Vector Libraryクラス:このライブラリは,int,uint,float,double,normunormを基本とする,長さ2,3,4の短いベクトル型の集合を定義します.

—–

【パフォーマンス推奨事項】

符号なし整数の剰余・除算は,符号つき整数の 剰余・除算よりも,明確に良いパフォーマンスが出ます.可能ならば符号なし整数を使うことを推奨します.

precise_math名前空間内の数学関数は,アクセラレータが倍精度演算をサポートしているならば,より良いパフォーマンスが出ます.

—–

【これも見てね】

参照

コンセプト

その他のリソース

Parallel Programming in Native Code Blog

—–

以上です.

英訳下手過ぎワロエナイ(´・ω・`)

コメントする »

RSS feed for comments on this post. TrackBack URI

コメントを残す

WordPress.com Blog.
Entriesコメント feeds.