この記事は C++ (fork) Advent Calendar 2013 24 日目の記事です。
こんにちは。今日は、C++ AMP についてお話していきたいと思います。
概要
1. C++ AMP とは何なのか
C++ AMP の “AMP” とは、Accelerated Massive Parallelism ということです。C++ を使ってヘテロ*1な開発環境を準備し、それにより生産性や移植性を上げるということです。AMD さんの協力により開発していったというのもあると思います。Channel 9 のほうにありますしね。現状、Visual Studio の一部なので、逆にいえば Linux などでは使えません。Visual Studio では GPU を使った強力なデバッグ環境もサポートされており、デバッグするのも容易かと思います。
C++ AMP の一番の売りは、C++er にとって STL-like な記述方法で CPU または GPU による並列化アルゴリズムを記述できる点です。従来であれば、SIMD (Single-Instruction, Multiple-Data) と CUDA や HLSL、OpenGL など CPU と GPU 両方対応するには同じ意味のコードを 2 つ用意しなければなりません。ところが、C++ AMP は GPU が使えない場合、自動で SIMD レベルでの実行に落としてくれるというメリットもあります。これは一見デメリットに見えて、メリットもあります。
まず、今は SSE から AVX への移行の過渡期です。SSE と AVX は混ぜて使わないように、と言われています。それは両命令の違いから、コンテクスト切り替えの必要としたハードウエア実装をしているため、オーバーヘッドが生じるためです。しかし、AVX を利用するプログラミングを、1 C++ 開発者は作るでしょうか? いいえ、作りません。ましてや、SSE ですら考慮したコードを書かない人も多いでしょう。ところが、C++ AMP を利用するだけで、GPU が使える、あるいは GPU が無理ならこのような CPU 命令レベルに落としてくれるというのは、非常に重たい処理であれば十分有効な方法でしょう。
私も初めてなので、間違っていることもあるかもしれませんが、その場合は指摘していただければ幸いです。
2. C++ AMP を始めるにあたって
まずはデータ構造のイメージをつかみましょう。
たとえば、std::vector<T> で 2 次元配列 (i, j) を実現するとき (using V = std::vector)、
- V<T>::size を i * j
- V<V<T>> で V<T>::size を j、V<V<T>>::size を i
の 2 種類が思いつくと思います。どちらにせよ、V<T> というデータは以下のようにつながっているイメージが重要です。
今回、これらと相互運用する中に重要なものがあります。C++14 の string_view をイメージしてもらえばいいと思いますが、concurrency::array_view<T, R> というクラスがあります。これを用いると、たとえば、上のような 2 次元配列の状態を保持しデータを加工することができます (これは C++ AMP との相互運用上で重要なクラスです)。
つまり、通常 V<T> は「2 次元配列 (rank = 2)」という意味を持っていませんでしたが、この array_view<T, R> は「2 次元配列」という意味を保持し、体系的に処理を行うことができるようになっています。以下のようなグループ分けが行われた、というイメージです。
これらをコードで表してみると以下のような感じになります。
const int i = 4, j = 2;
std::vector<double> v( i * j );
const concurrency::array_view<const double, 2> a( i, j, v );
簡単ですね! これで i, j マトリックス (2 次元配列)という意味になります。もちろん、3 次元も同様にできます。
「何? こんなの何の意味があるんだって? 簡単すぎやしないか?」
その通り。これは前段階の説明です。ほかに若干説明しておきます。
concurrency::index<R>
ランク R での index を表す。array_view[index<R>] で簡単にアクセスできるようになっています。なお、今回は明示的に使いません。array_view::extent (property: get=get_extent) にて取得できます。
3. C++ AMP でマトリックスの加算を書いてみよう!
行列の加算なんて簡単ですよね? こんなところで書きませんよ…
定義 Z = X + Y なマトリックス演算を行う。V<T> の T は double を用いる。
3.1. CPU コード
そのままの CPU コードです。これならだれもが書いたことあるはず…
std::vector<double> calc_cpu( const std::vector<double>& v1, const std::vector<double>& v2 )
{
std::vector<double> ret( matrix );
auto i = v1.cbegin(), j = v2.cbegin();
auto k = ret.begin();
while( k != ret.end() )
{
*k = *i + *j;
++i; ++j; ++k;
}
return ret;
}
行列演算が何行何列になろうとこれで問題なく行えます。次は C++ AMP 版です。
3.2. AMP コード
std::vector<double> calc_amp( const std::vector<double>& v1, const std::vector<double>& v2 )
{
using namespace concurrency;
std::vector<double> ret( matrix );
const array_view<const double, 2> i( matrix_w, matrix_h, v1 ), j( matrix_w, matrix_h, v2 );
array_view<double, 2> k( matrix_w, matrix_h, ret );
k.discard_data();
parallel_for_each( k.extent, [=]( const index<2>& idx ) restrict( amp )
{
k[idx] = i[idx] + j[idx];
} );
k.synchronize();
return ret;
}
こんな感じです。魔法の呪文の「restrict( amp )」もお忘れなく。
const が付いていないものはデータの変更があるため、データの同期を行ったりということを考えなければなりません。そこで一度、discard_data() でデータの同期を止め、そして synchronize() でデータを同期するという指示を与えています。
4. まとめ
今日では、Windows ストア アプリでは ARM を使ったデバイスが登場しています。イントリンシック命令やアセンブリ、あるいは HLSL などを使って実装することもできますが、C++ 開発者にとってそれは敷居の高いところになるでしょう。そんなとき、NEON*2 や SSE/AVX、あるいは GPU などを自動的に駆使してくれる C++ AMP はとても頼りになるのではないのでしょうか? まだまだ使われている印象はありませんが、もし興味を持ったならいじってみるのもいいかと思います。
次回は、もう少し高度なネタでも書こうかな? とか思ってたりしますが、どうなるかはわかりません。 おしまい