C'è un modo per istruire GCC (versione I utilizzata 4.8.4) per srotolare il ciclo while nella funzione di fondo completamente, ad esempio sbucciare questo ciclo? Il numero di iterazioni del ciclo è noto al momento della compilazione: 58.Come chiedere a GCC di srotolare completamente questo ciclo (ad esempio, sbucciare questo ciclo)?
Lasciatemi prima spiegare cosa ho provato.
Controllando ouput GAS:
gcc -fpic -O2 -S GEPDOT.c
12 registri XMM0 - XMM11 vengono utilizzati. Se mi passa la bandiera -funroll-loops a gcc:
gcc -fpic -O2 -funroll-loops -S GEPDOT.c
il ciclo è srotolato solo due volte. Ho controllato le opzioni di ottimizzazione GCC. GCC dice che -funroll-loop si accende -frame-registers, quindi, quando GCC srotola un ciclo, la sua scelta prioritaria per l'allocazione del registro è quella di utilizzare i registri "rimanenti". Ma ci sono solo 4 rimasti su XMM12 - XMM15, quindi GCC può solo srotolare 2 volte al suo meglio. Se fossero disponibili 48 anziché 16 registri XMM, GCC srotolerà il ciclo while 4 volte senza problemi.
Eppure ho fatto un altro esperimento. Per prima cosa ho srotolato manualmente il ciclo while due volte, ottenendo una funzione GEPDOT_2. Allora non c'è alcuna differenza affatto tra
gcc -fpic -O2 -S GEPDOT_2.c
e
gcc -fpic -O2 -funroll-loops -S GEPDOT_2.c
Da GEPDOT_2 già utilizzato tutti i registri, senza srotolamento è eseguita.
GCC si registra la ridenominazione per evitare potenziale false dipendenze introdotte. Ma so per certo che non ci sarà un tale potenziale nel mio GEPDOT; anche se esiste, non è importante. Ho provato a srotolare il loop da solo, e srotolare 4 volte è più veloce dello srotolamento 2 volte, più veloce di quanto non si possa srotolare. Naturalmente posso srotolare manualmente più volte, ma è noioso. GCC può farlo per me? Grazie.
// C file "GEPDOT.c"
#include <emmintrin.h>
void GEPDOT (double *A, double *B, double *C) {
__m128d A1_vec = _mm_load_pd(A); A += 2;
__m128d B_vec = _mm_load1_pd(B); B++;
__m128d C1_vec = A1_vec * B_vec;
__m128d A2_vec = _mm_load_pd(A); A += 2;
__m128d C2_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C3_vec = A1_vec * B_vec;
__m128d C4_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C5_vec = A1_vec * B_vec;
__m128d C6_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C7_vec = A1_vec * B_vec;
A1_vec = _mm_load_pd(A); A += 2;
__m128d C8_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
int k = 58;
/* can compiler unroll the loop completely (i.e., peel this loop)? */
while (k--) {
C1_vec += A1_vec * B_vec;
A2_vec = _mm_load_pd(A); A += 2;
C2_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C3_vec += A1_vec * B_vec;
C4_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C5_vec += A1_vec * B_vec;
C6_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C7_vec += A1_vec * B_vec;
A1_vec = _mm_load_pd(A); A += 2;
C8_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
}
C1_vec += A1_vec * B_vec;
A2_vec = _mm_load_pd(A);
C2_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C3_vec += A1_vec * B_vec;
C4_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C5_vec += A1_vec * B_vec;
C6_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B);
C7_vec += A1_vec * B_vec;
C8_vec += A2_vec * B_vec;
/* [write-back] */
A1_vec = _mm_load_pd(C); C1_vec = A1_vec - C1_vec;
A2_vec = _mm_load_pd(C + 2); C2_vec = A2_vec - C2_vec;
A1_vec = _mm_load_pd(C + 4); C3_vec = A1_vec - C3_vec;
A2_vec = _mm_load_pd(C + 6); C4_vec = A2_vec - C4_vec;
A1_vec = _mm_load_pd(C + 8); C5_vec = A1_vec - C5_vec;
A2_vec = _mm_load_pd(C + 10); C6_vec = A2_vec - C6_vec;
A1_vec = _mm_load_pd(C + 12); C7_vec = A1_vec - C7_vec;
A2_vec = _mm_load_pd(C + 14); C8_vec = A2_vec - C8_vec;
_mm_store_pd(C,C1_vec); _mm_store_pd(C + 2,C2_vec);
_mm_store_pd(C + 4,C3_vec); _mm_store_pd(C + 6,C4_vec);
_mm_store_pd(C + 8,C5_vec); _mm_store_pd(C + 10,C6_vec);
_mm_store_pd(C + 12,C7_vec); _mm_store_pd(C + 14,C8_vec);
}
aggiornamento 1
Grazie al commento di @ user3386109, vorrei estendere questa domanda un po '. @ user3386109 solleva una domanda molto buona. In realtà ho qualche dubbio sulla capacità del compilatore di allocare il registro in modo ottimale, quando ci sono così tante istruzioni parallele da programmare.
Personalmente ritengo che un modo affidabile sia quello di codificare prima il corpo del loop (che è la chiave per HPC) nell'assemblaggio in linea asm, quindi duplicarlo tutte le volte che voglio. Ho avuto un post impopolare all'inizio di quest'anno: inline assembly. Il codice era leggermente diverso perché il numero di iterazioni di loop, j, è un argomento di funzione quindi sconosciuto al momento della compilazione. In quel caso non riesco a srotolarlo completamente, quindi ho solo duplicato il codice assembly due volte e ho convertito il loop in un'etichetta e saltato.Si è scoperto che le prestazioni risultanti del mio assembly scritto sono circa il 5% più alte dell'assemblaggio generato dal compilatore, il che potrebbe suggerire che il compilatore non riesce ad allocare i registri nel modo previsto e ottimale.
Ero (e sono ancora) un bambino nella codifica di assiemi, quindi mi serve un buon caso di studio per imparare un po 'sull'assemblaggio di x86. Ma a lungo termine non sono incline a codificare GEPDOT con una grande proporzione per il montaggio. Ci sono principalmente tre ragioni:
- asm assembly inline è stato critisized per non essere portabile. Anche se non capisco perché. Forse perché diverse macchine hanno diversi registri danneggiati?
- Anche il compilatore sta migliorando. Quindi preferirei sempre l'ottimizzazione algoritmica e l'abitudine al codice C per aiutare il compilatore a generare buoni risultati;
- L'ultimo motivo è più importante. Il numero di iterazioni potrebbe non essere sempre 58. Sto sviluppando una subroutine di fattorizzazione della matrice ad alte prestazioni. Per un fattore di blocco della cache nb, il numero di iterazioni sarà (nb-2). Non ho intenzione di inserire nb come argomento di funzione, come ho fatto nel post precedente. Questo è un parametro specifico della macchina che verrà definito come una macro. Quindi il numero di iterazioni è noto al momento della compilazione, ma può variare da macchina a macchina. Indovina quanto lavoro tedioso devo svolgere nello srotolamento del ciclo manuale per una varietà di nb. Quindi, se c'è un modo per istruire semplicemente il compilatore a sbucciare un ciclo, è grandioso.
Sarei molto apprezzato se si può anche condividere qualche esperienza nella produzione di librerie portatili ad alte prestazioni.
Hai provato '-funroll-all-loops'? –
Quindi, se si duplica manualmente il corpo di quel ciclo 58 volte, GCC esegue un lavoro decente gestendo l'utilizzo del registro? Chiedo perché sembra abbastanza semplice scrivere un preprocessore che srotolerà il ciclo. Ad esempio, sostituire 'while' con' preproc__repeat (58) '. Quindi scrivi un preprocessore che cerca 'preproc__repeat', estrae il numero e duplica il corpo il numero di volte indicato. – user3386109
1) I processori diversi non limitano solo i diversi registri. Non hanno nemmeno * gli stessi registri. E non hanno le stesse istruzioni (sebbene _mm_load1_pd sia già un po 'specifico per il processore). Inoltre, diversi compilatori trattano diversamente le istruzioni asm in linea. In linea asm che funziona su un compilatore può essere compilato, ma non riesce a produrre i risultati corretti su un altro. –