2012-07-04 13 views
10

Sto cercando di migliorare questo codice con il prodotto punto SSE4 ma sto riscontrando difficoltà nel trovare una soluzione. Questa funzione ottiene i parametri qi e tj che contengono array float con 80 celle ciascuno e quindi calcolano il prodotto punto. Il valore di ritorno è un vettore con quattro punti. Quindi, quello che sto cercando di fare è calcolare quattro prodotti punto di venti valori in parallelo.Vectorizing Calcolo del prodotto del punto utilizzando SSE4

Hai qualche idea su come migliorare questo codice?

inline __m128 ScalarProd20Vec(__m128* qi, __m128* tj) 
{ 
    __m128 res=_mm_add_ps(_mm_mul_ps(tj[0],qi[0]),_mm_mul_ps(tj[1],qi[1])); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[2],qi[2]),_mm_mul_ps(tj[3],qi[3]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[4],qi[4]),_mm_mul_ps(tj[5],qi[5]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[6],qi[6]),_mm_mul_ps(tj[7],qi[7]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[8],qi[8]),_mm_mul_ps(tj[9],qi[9]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17]))); 
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19]))); 
    return res; 
} 

risposta

9

Delle centinaia di esempi SSE che ho visto su SO, il tuo codice è uno dei pochi che è già in buona forma fin dall'inizio. Non hai bisogno dell'istruzione del prodotto a punti SSE4. (Si può fare di meglio!)

Tuttavia, c'è una cosa che si può provare: (. Dico provare perché non l'ho ancora cronometrato)

Attualmente hai una catena di dati-dipendenza res. L'aggiunta del vettore è 3-4 cicli sulla maggior parte delle macchine oggi.Così il vostro codice avrà un minimo di 30 cicli per eseguire dal momento che hai:

(10 additions on critical path) * (3 cycles addps latency) = 30 cycles 

Che cosa si può fare è al nodo-split la variabile res come segue:

__m128 res0 = _mm_add_ps(_mm_mul_ps(tj[ 0],qi[ 0]),_mm_mul_ps(tj[ 1],qi[ 1])); 
__m128 res1 = _mm_add_ps(_mm_mul_ps(tj[ 2],qi[ 2]),_mm_mul_ps(tj[ 3],qi[ 3])); 

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 4],qi[ 4]),_mm_mul_ps(tj[ 5],qi[ 5]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[ 6],qi[ 6]),_mm_mul_ps(tj[ 7],qi[ 7]))); 

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 8],qi[ 8]),_mm_mul_ps(tj[ 9],qi[ 9]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11]))); 

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15]))); 

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19]))); 

return _mm_add_ps(res0,res1); 

Questo riduce quasi tua critica percorso a metà. Si noti che a causa della non associatività in virgola mobile, questa ottimizzazione è illegale per i compilatori.


Ecco una versione alternativa che utilizza istruzioni di suddivisione del nodo a 4 vie e FMA4 AMD. Se non puoi usare gli additivi moltiplicati con fusibili, sentiti libero di dividerli. Potrebbe essere ancora migliore della prima versione di cui sopra.

__m128 res0 = _mm_mul_ps(tj[ 0],qi[ 0]); 
__m128 res1 = _mm_mul_ps(tj[ 1],qi[ 1]); 
__m128 res2 = _mm_mul_ps(tj[ 2],qi[ 2]); 
__m128 res3 = _mm_mul_ps(tj[ 3],qi[ 3]); 

res0 = _mm_macc_ps(tj[ 4],qi[ 4],res0); 
res1 = _mm_macc_ps(tj[ 5],qi[ 5],res1); 
res2 = _mm_macc_ps(tj[ 6],qi[ 6],res2); 
res3 = _mm_macc_ps(tj[ 7],qi[ 7],res3); 

res0 = _mm_macc_ps(tj[ 8],qi[ 8],res0); 
res1 = _mm_macc_ps(tj[ 9],qi[ 9],res1); 
res2 = _mm_macc_ps(tj[10],qi[10],res2); 
res3 = _mm_macc_ps(tj[11],qi[11],res3); 

res0 = _mm_macc_ps(tj[12],qi[12],res0); 
res1 = _mm_macc_ps(tj[13],qi[13],res1); 
res2 = _mm_macc_ps(tj[14],qi[14],res2); 
res3 = _mm_macc_ps(tj[15],qi[15],res3); 

res0 = _mm_macc_ps(tj[16],qi[16],res0); 
res1 = _mm_macc_ps(tj[17],qi[17],res1); 
res2 = _mm_macc_ps(tj[18],qi[18],res2); 
res3 = _mm_macc_ps(tj[19],qi[19],res3); 

res0 = _mm_add_ps(res0,res1); 
res2 = _mm_add_ps(res2,res3); 

return _mm_add_ps(res0,res2); 
+3

Vieni a pensarci. Ci sono 40 carichi di memoria. A meno che non si stia utilizzando un processore Sandy Bridge, il collo di bottiglia si riduce a 40 cicli. Quindi il codice dell'OP potrebbe essere già ottimale. – Mysticial

+2

Informazioni sull'associatività in virgola mobile: Spesso le sottostanti bandiere del compilatore '-ffast-math 'spesso sottovalutate e incomprese fanno miracoli. E gli AMD possono fare due carichi di memoria L1 per ciclo fin dagli albori dell'umanità, ma sfortunatamente sono cani lenti ovunque. – hirschhornsalz

+0

Grazie mille per il vostro aiuto. Il risultato del mio test afferma che il mio codice funziona alla velocità della tua idea (come hai detto nel commento). L'AMD FMA4 sembra interessante ma questa istruzione non è disponibile sulla mia macchina e il codice deve essere compatibile con SSE2. Lo proverò con -ffast-math. –

3

In primo luogo, l'ottimizzazione più importante che si può fare è assicurarsi che il proprio compilatore abbia tutte le sue impostazioni di ottimizzazione attivate.


compilatori sono abbastanza intelligente, quindi se scriverlo come un ciclo, è probabile che srotolarlo:

__128 res = _mm_setzero(); 
for (int i = 0; i < 10; i++) { 
    res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1]))); 
} 
return res; 

(con GCC è necessario passare -funroll-loops, e poi ti srotolano lo fare 5 iterazioni alla volta)

si potrebbe anche definire una macro e srotolare a mano, se la versione del ciclo è più lento, ad esempio:.

__128 res = _mm_setzero(); 

#define STEP(i) res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1]))) 

STEP(0); STEP(1); STEP(2); STEP(3); STEP(4); 
STEP(5); STEP(6); STEP(7); STEP(8); STEP(9); 

#undef STEP 

return res; 

Si potrebbe anche eseguire il ciclo da 0 a 20 (o fare lo stesso con la versione macro), vale a dire:

__128 res = _mm_setzero(); 
for (int i = 0; i < 20; i++) { 
    res = _mm_add_ps(res, _mm_mul_ps(tj[i], qi[i])); 
} 
return res; 

(con GCC e -funroll-loops questo è srotolato per fare 10 iterazioni alla volta , vale a dire lo stesso del ciclo two-at-a-time sopra.)

2

I dati non sono disposti in memoria in un formato adatto per le istruzioni del prodotto SSE4 specializzato (dpps). Tali istruzioni si aspettano che le dimensioni di un singolo vettore di essere adiacente ,, come questo:

| dim0 | dim1 | dim2 | ... | dim19 | 

mentre i dati sembra avere i vettori interleaved con l'altro:

| v0-dim0 | v1-dim0 | v2-dim0 | v3-dim0 | v0-dim1 | ... 

Il tuo attuale approccio generale sembra opportuno - potresti essere in grado di migliorare le cose riordinando le istruzioni in modo tale che i risultati delle moltiplicazioni non vengano utilizzati immediatamente dopo la loro generazione, ma in realtà il compilatore dovrebbe essere in grado di capirlo da solo.