从广义上讲,在 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];
}
}
}
这个内核只有两个内部 for 循环。将此作业发送到 GPU 的程序将告诉 GPU 为 C 中的每个数据点执行此内核。GPU 将在许多线程上同时执行这些指令中的每一个。就像那句老话“便宜一打”GPU 被设计成可以更快地多次执行相同的操作。
然而,有一些算法会减慢 GPU 的速度。有些不太适合 GPU。
例如,如果存在数据依赖关系,即:想象 C 的每个元素的计算都依赖于先前的元素。程序员必须在内核中设置一个屏障来等待每个先前的计算完成。这将是一个重大的放缓。
此外,具有大量分支逻辑的算法即:
__kernel Foo()
{
if (somecondition)
{
do something
}
else
{
do something completely different
}
}
倾向于在 GPU 上运行较慢,因为 GPU 不再在每个线程中执行相同的操作。
这是一个简化的解释,因为还有许多其他因素需要考虑。例如,在 CPU 和 GPU 之间发送数据也很耗时。有时在 GPU 上进行计算是值得的,即使它在 CPU 上速度更快,只是为了避免额外的发送时间(反之亦然)。
现在,许多现代 CPU 也支持超线程多核处理器的并发性。
GPU 似乎也不太适合递归,请参阅此处,这可能解释了 QR 算法的一些问题。我相信一个人有一些递归数据依赖性。