Tengo una pregunta sobre la optimización del compilador de C y cuándo/cómo se desarrollan los bucles en las funciones en línea.
Estoy desarrollando un código numérico que hace algo como el siguiente ejemplo. Básicamente, my_for() calcularía algún tipo de plantilla y llamaría a op() para hacer algo con los datos en my_type *arg para cada i . Aquí, my_func() envuelve my_for() , creando el argumento y enviando el puntero de función a my_op() ... cuyo trabajo es modificar el i -ésimo doble para cada uno de los arreglos dobles ( arg->n ) 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 ); } Esto funciona bien. Las funciones están en línea como deberían y el código es (casi) tan eficiente como haber escrito todo en línea en una sola función y desenrollar el bucle j , sin ningún tipo de my_type Arg .
Aquí está la confusión: si configuro int const n = 2; en lugar de int const n = arg->n; en my_op() , entonces el código se vuelve tan rápido como la versión de función única desenrollada. Entonces, la pregunta es: ¿por qué? Si todo se integra en my_func() , ¿por qué el compilador no ve que estoy definiendo literalmente Arg.n = 2 ? Además, no hay ninguna mejora cuando realizo explícitamente el límite en el j loop arg->n , que debería verse como el int const n = 2; después de incrustar. También intenté usar my_type const en todas partes para señalar realmente esta constante al compilador, pero simplemente no quiere desenrollar el bucle.
En mi código numérico, esto equivale a alrededor de un 15% de impacto en el rendimiento. Si importa, ahí, n=4 y estos bucles j aparecen en un par de ramas condicionales en un op() .
Estoy compilando con icc (ICC) 12.1.5 20120612. Probé #pragma unroll . Aquí están mis opciones de compilador (¿me perdí alguna buena?):
-O3 -ipo -static -unroll-aggressive -fp-model precise -fp-model source -openmp -std=gnu99 -Wall -Wextra -Wno-unused -Winline -pedantic
¡Gracias!
Bueno, obviamente el compilador no es lo suficientemente 'inteligente' para propagar la constante n y desenrollar el ciclo for . En realidad, es seguro ya que arg->n puede cambiar entre instanciación y uso.
Para tener un rendimiento constante en todas las generaciones de compiladores y exprimir al máximo su código, realice el desenrollado a mano.
Lo que la gente como yo hace en estas situaciones (el rendimiento es el rey) es confiar en las macros.
Las macros se 'en línea' en las compilaciones de depuración (útil) y se pueden crear plantillas (hasta cierto punto) utilizando parámetros de macro. Se garantiza que los parámetros de macro que son constantes de tiempo de compilación permanecerán así.
Es más rápido, porque su programa no asigna memoria a la variable.
Si no tiene que realizar ninguna operación en valores desconocidos, se tratan como si fueran #define constant 2 con verificación de tipo. Simplemente se agregan durante la compilación.
¿Podría elegir una de las dos etiquetas (me refiero a C o C ++), es confuso, porque los idiomas tratan los valores const de manera diferente: C los trata como variables normales cuyo valor simplemente no se puede cambiar, y en C ++ lo hacen o no No tiene memoria asignada según el contexto (si necesita su dirección o si necesita calcularlos cuando el programa se está ejecutando, entonces se asigna memoria).
Fuente: "Pensando en C++". Sin cita exacta.