2015-06-12 24 views
8

Ho una domanda sull'ottimizzazione del compilatore C e quando/come i loop nelle funzioni inline sono srotolati.Svolgimento del loop in funzioni integrate in C

Sto sviluppando un codice numerico che fa qualcosa come nell'esempio qui sotto. Fondamentalmente, my_for() calcola una specie di stencil e chiama op() per fare qualcosa con i dati in my_type *arg per ogni i. Qui, my_func() esegue il wrapping my_for(), creando l'argomento e inviando il puntatore a my_op() ... che è compito modificare il doppio i per ciascuno dei due (arg->n) array arg->dest[j].

typedef struct my_type { 
    int const n; 
    double *dest[16]; 
    double const *src[16]; 
} my_type; 

static inline void my_for(void (*op)(my_type *,int), my_type *arg, int N) { 
    int i; 

    for(i=0; i<N; ++i) 
    op(arg, i); 
} 

static inline void my_op(my_type *arg, int i) { 
    int j; 
    int const n = arg->n; 

    for(j=0; j<n; ++j) 
    arg->dest[j][i] += arg->src[j][i]; 
} 

void my_func(double *dest0, double *dest1, double const *src0, double const *src1, int N) { 
    my_type Arg = { 
    .n = 2, 
    .dest = { dest0, dest1 }, 
    .src = { src0, src1 } 
    }; 

    my_for(&my_op, &Arg, N); 
} 

Questo funziona correttamente. Le funzioni si stanno adattando come dovrebbero e il codice è (quasi) efficiente quanto aver scritto tutto in linea in un'unica funzione e srotolato il ciclo j, senza alcun tipo di my_type Arg.

Ecco la confusione: se imposto int const n = 2; anziché in my_op(), il codice diventa veloce quanto la versione a singola funzione srotolata. Quindi, la domanda è: perché? Se tutto è inserito in my_func(), perché il compilatore non vede che sto letteralmente definendo Arg.n = 2? Inoltre, non c'è alcun miglioramento quando faccio esplicitamente il bound sul ciclo jarg->n, che dovrebbe apparire come il più veloce int const n = 2; dopo l'inlining. Ho anche provato a usare my_type const ovunque per segnalare veramente questa costante al compilatore, ma semplicemente non vuole srotolare il ciclo.

Nel mio codice numerico, ciò equivale a un calo delle prestazioni di circa il 15%. Se è importante, lì, n=4 e questi loop j vengono visualizzati in un paio di rami condizionali in un op().

Sto compilando con icc (ICC) 12.1.5 20120612. Ho provato #pragma unroll. Qui sono le mie opzioni di compilazione (mi sono perso qualche quelli buoni?):

-O3 -ipo -static -unroll-aggressive -fp-model precise -fp-model source -openmp -std=gnu99 -Wall -Wextra -Wno-unused -Winline -pedantic

Grazie!

+0

Stai guardando il codice generato? – unwind

+0

In che modo "lontano" cercare i valori noti in fase di compilazione quando l'inlining è una decisione difficile. Sembra che tu abbia incontrato il limite del compilatore. Passare 'n' come parametro di funzione esplicita potrebbe migliorare le probabilità. – molbdnilo

+1

Mi chiedo se non otterrebbe molta più velocità se si scambiano le dimensioni. Come dato ora, è possibile che si ottenga un piccolo vantaggio sulle linee della cache e sui riempimenti a raffica (e si può usare memcpy, che è già altamente ottimizzato). Inoltre, riempire la struct con un intializer è un'estensione gcc (spero tu ne sia a conoscenza - non è un problema per me). – Olaf

risposta

3

Bene, ovviamente il compilatore non è abbastanza "intelligente" da propagare la costante n e srotolare il ciclo for. In realtà è sicuro perché arg->n può cambiare tra l'istanziazione e l'utilizzo.

Per avere prestazioni coerenti tra generazioni del compilatore e spremere il massimo dal codice, eseguire lo srotolamento a mano.

Quello che le persone come me fanno in queste situazioni (la performance è il re) si basa su macro.

Le macro saranno "in linea" nelle compilazioni di debug (utili) e possono essere inserite in un modello (ad un punto) utilizzando i parametri macro. I parametri macro che sono costanti di tempo di compilazione sono garantiti per rimanere in questo modo.

+0

C Le macro preprocessore sono un dolore al collo quando si utilizza un debugger. Le macro sono macro e non modelli anche se esistono alcuni tipi di comportamenti che sembrano condividere. Non si macina modelli, si forniscono solo argomenti anche se si utilizzano macro all'interno di macro. Le macro vengono espanse dal preprocessore C per generare il testo che viene quindi inserito nel compilatore C. È la tecnologia degli anni '70 presa in prestito dalla tecnologia dei macro assembler ancora più antica. I modelli sono incorporati nel compilatore C++ che consente l'accesso alle funzionalità del compilatore e ai dati non disponibili per il preprocessore C. –

+0

@RichardChambers: Sono d'accordo per il confronto con i modelli. Tuttavia, le macro OTOH consentono cose che non puoi fare con i modelli. Come la creazione di identificatori, dichiarazioni, ecc. Penso che entrambi abbiano il loro uso, si dovrebbe fare attenzione quando li si usa. E poiché C non ha template, devi usare macro per quello che forniscono. – Olaf

+0

@egur: Hmm, per incoraggiare icc a non giocare sul sicuro, ho dichiarato 'my_type''s' n' come 'const', così come l'istanza della struct stessa. Una volta in linea, immagineresti che sia immutabile quanto il 'n' in' my_op() '.Ma, come ho capito da quello che Adrian ha detto, il fatto che la memoria sia stata allocata per 'Arg', quindi necessariamente anche per' Arg.n', che impedisce di ottimizzarlo. Ad ogni modo, vinci per l'idea macro. Mi sono dilettato solo in precedenza, ma nelle ultime ore sono diventato macro-pazzo e li ho usati per generare più versioni di queste funzioni ... e ho recuperato il 15%. – FiniteElement

2

È più veloce, perché il programma non assegna memoria alla variabile.

Se non si devono eseguire operazioni su valori sconosciuti, vengono considerati come se fossero #define constant 2 con controllo tipo. Sono appena aggiunti durante la compilazione.

Puoi scegliere uno dei due tag (intendo C o C++), è confuso, perché le lingue trattano i valori const in modo diverso - C li considera come variabili normali il cui valore non può essere modificato e in C++ hanno o non hanno memoria assegnata a seconda del contesto (se hai bisogno del loro indirizzo o se devi calcolarli quando il programma è in esecuzione, allora la memoria è assegnata).

Fonte: "Pensare in C++". Nessuna citazione esatta.

+0

Cosa intendi con "che avrebbe dovuto essere un commento"? –

+1

Ho capito. Ma non posso pubblicare commenti poiché non ho ancora 50 punti. –

+0

Beh, avrebbe dovuto essere un commento invece di una risposta. Almeno parti di esso (è davvero poco strutturato). – Olaf