Em termos gerais, os algoritmos executados mais rapidamente na GPU são aqueles em que você está executando o mesmo tipo de instrução em muitos pontos de dados diferentes.
Um exemplo fácil para ilustrar isso é com a multiplicação de matrizes.
Suponha que estejamos fazendo o cálculo matricial
A × B = C
Um algoritmo simples da CPU pode parecer algo como
// começando com 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];
}
}
}
}
O principal a ver aqui é que há muitos aninhados para loops e cada etapa deve ser executada uma após a outra.
Veja um diagrama deste
Observe que o cálculo de cada elemento de C não depende de nenhum dos outros elementos. Portanto, não importa em que ordem os cálculos são feitos.
Portanto, na GPU, essas operações podem ser realizadas simultaneamente.
Um núcleo de GPU para calcular uma multiplicação de matrizes seria algo como
__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];
}
}
}
Este kernel possui apenas os dois loops internos. Um programa que envia esse trabalho para a GPU instruirá a GPU a executar esse kernel para cada ponto de dados em C. A GPU executará cada uma dessas instruções simultaneamente em vários threads. Assim como o antigo ditado "GPU mais barato", as GPUs são projetadas para serem mais rápidas, fazendo a mesma coisa várias vezes.
No entanto, existem alguns algoritmos que desacelerarão a GPU. Alguns não são adequados para a GPU.
Se, por exemplo, houvesse dependências de dados, ou seja: imagine que o cálculo de cada elemento de C dependesse dos elementos anteriores. O programador teria que colocar uma barreira no kernel para aguardar a conclusão de cada cálculo anterior. Isso seria uma grande desaceleração.
Além disso, algoritmos que possuem muita lógica de ramificação, ou seja:
__kernel Foo()
{
if (somecondition)
{
do something
}
else
{
do something completely different
}
}
tendem a rodar mais devagar na GPU porque a GPU não está mais fazendo a mesma coisa em cada thread.
Esta é uma explicação simplificada, porque há muitos outros fatores a serem considerados. Por exemplo, o envio de dados entre a CPU e a GPU também consome tempo. Às vezes, vale a pena fazer um cálculo na GPU mesmo quando mais rápido na CPU, apenas para evitar o tempo extra de envio (e vice-versa).
Agora, muitas CPUs modernas também suportam simultaneidade com processadores multicore com hyperthread.
As GPUs também parecem não ser tão boas para recursão; veja aqui o que provavelmente explica alguns dos problemas com o algoritmo QR. Acredito que se tenha algumas dependências recursivas de dados.