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 j
arg->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!
Stai guardando il codice generato? – unwind
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
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