В общих чертах, алгоритмы, которые работают быстрее на графическом процессоре, - это те, в которых вы выполняете инструкции одного и того же типа для множества различных точек данных.
Простой пример для иллюстрации этого - умножение матриц.
Предположим, что мы делаем матричные вычисления
A × B = C
Простой алгоритм процессора может выглядеть примерно так
// начиная с 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];
}
}
}
}
Главное, что здесь нужно увидеть, это то, что существует множество вложенных циклов, и каждый шаг должен выполняться один за другим.
Смотрите схему этого
Обратите внимание, что расчет каждого элемента 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 будет выполнять каждую из этих инструкций одновременно во многих потоках. Точно так же, как старая поговорка «Дешевле на дюжину», графические процессоры предназначены для того, чтобы быстрее делать одно и то же много раз.
Однако есть некоторые алгоритмы, которые замедляют работу графического процессора. Некоторые не очень подходят для графического процессора.
Если, например, были зависимости данных, то есть: представьте, что вычисление каждого элемента C зависело от предыдущих элементов. Программист должен был бы поставить барьер в ядре, чтобы дождаться завершения каждого предыдущего вычисления. Это было бы серьезным замедлением.
Кроме того, алгоритмы, которые имеют много логики ветвления, то есть:
__kernel Foo()
{
if (somecondition)
{
do something
}
else
{
do something completely different
}
}
на GPU работают медленнее, потому что GPU больше не делает одно и то же в каждом потоке.
Это упрощенное объяснение, потому что есть много других факторов, которые необходимо учитывать. Например, передача данных между процессором и графическим процессором также занимает много времени. Иногда стоит выполнить вычисления на графическом процессоре, даже если он быстрее на процессоре, просто чтобы избежать дополнительного времени отправки (и наоборот).
Кроме того, многие современные процессоры теперь поддерживают параллелизм и с многопоточными многоядерными процессорами.
Также кажется, что графические процессоры не очень хороши для рекурсии, см. Здесь, что, вероятно, объясняет некоторые проблемы с алгоритмом QR. Я считаю, что у кого-то есть рекурсивные зависимости данных.