大まかに言えば、GPU上でより高速に実行されるアルゴリズムは、多くの異なるデータポイントで同じタイプの命令を実行するアルゴリズムです。
これを説明する簡単な例は、行列の乗算です。
行列計算をしていると仮定します
A × B = C
単純なCPUアルゴリズムは次のようになります
// C = 0から開始
for (int i = 0; i < C_Width; i++)
{
for (int j = 0; j < C_Height; j++)
{
for (int k = 0; k < A_Width; k++)
{
for (int l = 0; l < B_Height; l++)
{
C[j, i] += A[j, k] * B[l, i];
}
}
}
}
ここで重要なのは、ネストされたforループが多数あり、各ステップを順番に実行する必要があることです。
この図をご覧ください
Cの各要素の計算は、他の要素に依存しないことに注意してください。したがって、計算の順序は関係ありません。
したがって、GPUでは、これらの操作を同時に実行できます。
行列乗算を計算するためのGPUカーネルは次のようになります
__kernel void Multiply
(
__global float * A,
__global float * B,
__global float * C
)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
for (int k = 0; k < A_Width; k++)
{
for (int l = 0; l < B_Height; l++)
{
C[x, y] += A[x, k] * B[l, y];
}
}
}
このカーネルには、内部ループが2つしかありません。このジョブをGPUに送信するプログラムは、Cの各データポイントに対してこのカーネルを実行するようにGPUに指示します。GPUは、これらの各命令を多数のスレッドで同時に実行します。昔の格言「Cheaper by theダース」GPUは、同じことを何度も高速に実行するように設計されています。
ただし、GPUの速度を低下させるアルゴリズムがいくつかあります。GPUに適さないものもあります。
たとえば、データの依存関係がある場合、つまり、Cの各要素の計算が以前の要素に依存していると想像してください。プログラマは、以前の各計算が終了するのを待つために、カーネルにバリアを配置する必要があります。これは大幅に遅くなります。
また、多くの分岐ロジックを持つアルゴリズム:
__kernel Foo()
{
if (somecondition)
{
do something
}
else
{
do something completely different
}
}
GPUは各スレッドで同じことを行っていないため、GPUで実行が遅くなる傾向があります。
考慮すべき他の多くの要因があるため、これは簡単な説明です。たとえば、CPUとGPU間でデータを送信するのも時間がかかります。余分な送信時間を避けるためだけに、CPUでの処理速度が速い場合でもGPUで計算を行う価値がある場合があります(逆も同様です)。
また、現在の多くのCPUは、ハイパースレッドマルチコアプロセッサとの同時実行をサポートしています。
GPUも再帰にはあまり向いていないようです。QRアルゴリズムの問題のいくつかを説明しているここを参照してください。再帰的なデータ依存性があると思います。