In termini generali, gli algoritmi che funzionano più velocemente sulla GPU sono quelli in cui si sta eseguendo lo stesso tipo di istruzione su molti punti dati diversi.
Un semplice esempio per illustrare questo è con la moltiplicazione della matrice.
Supponiamo di fare il calcolo della matrice
A × B = C
Un semplice algoritmo CPU potrebbe avere un aspetto simile
// a partire da 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];
}
}
}
}
La cosa fondamentale da vedere qui è che ci sono molti cicli nidificati per i cicli e ogni passaggio deve essere eseguito uno dopo l'altro.
Vedi un diagramma di questo
Si noti che il calcolo di ciascun elemento di C non dipende da nessuno degli altri elementi. Quindi non importa in quale ordine vengono eseguiti i calcoli.
Quindi, sulla GPU, queste operazioni possono essere eseguite contemporaneamente.
Un kernel GPU per il calcolo di una moltiplicazione di matrici sarebbe simile
__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];
}
}
}
Questo kernel ha solo i due cicli interni. Un programma che invia questo lavoro alla GPU dirà alla GPU di eseguire questo kernel per ciascun punto dati in C. La GPU eseguirà ciascuna di queste istruzioni contemporaneamente su molti thread. Proprio come il vecchio detto "Più economico della dozzina" GPU sono progettate per essere più veloci facendo la stessa cosa molte volte.
Esistono tuttavia alcuni algoritmi che rallentano la GPU. Alcuni non sono adatti per la GPU.
Se per esempio ci fossero dipendenze dei dati, cioè: immagina che il calcolo di ciascun elemento di C dipendesse dagli elementi precedenti. Il programmatore dovrebbe mettere una barriera nel kernel per attendere il completamento di ogni calcolo precedente. Questo sarebbe un grande rallentamento.
Inoltre, algoritmi che hanno molta logica di ramificazione, ad esempio:
__kernel Foo()
{
if (somecondition)
{
do something
}
else
{
do something completely different
}
}
tendono a funzionare più lentamente sulla GPU perché la GPU non sta più facendo la stessa cosa in ogni thread.
Questa è una spiegazione semplificata perché ci sono molti altri fattori da considerare. Ad esempio, anche l'invio di dati tra CPU e GPU richiede tempo. A volte vale la pena fare un calcolo sulla GPU anche quando è più veloce sulla CPU, solo per evitare il tempo di invio aggiuntivo (e viceversa).
Anche molte CPU moderne supportano la concorrenza ora con processori multicore hyperthreaded.
Anche le GPU sembrano non essere così buone per la ricorsione, vedi qui che probabilmente spiega alcuni dei problemi con l'algoritmo QR. Credo che si abbiano alcune dipendenze ricorsive nei dati.