2011-07-29 11 views
6

He estado utilizando los atributos gcc const y pure para funciones que devuelven un puntero a datos "constantes" que se asignan e inicializan en el primer uso, es decir, donde la función devuelve el mismo valor cada vez que se lo llama. Como ejemplo (no es mi caso de uso, sino un ejemplo bien conocido) pienso en una función que asigna y calcula tablas de búsqueda trigonométricas en la primera llamada y simplemente devuelve un puntero a las tablas existentes después de la primera llamada.atributos de gcc para funciones de inicialización en primer uso

El problema: me han dicho que este uso es incorrecto porque estos atributos prohíben los efectos secundarios, y que el compilador podría incluso optimizar la llamada completamente en algunos casos si el valor de retorno no se utiliza. ¿Es seguro el uso de los atributos const/pure, o hay otra forma de decirle al compilador que las llamadas N>1 a la función son equivalentes a 1 llamada a la función, pero que 1 llamada a la función no es equivalente a 0 llamadas al ¿función? ¿O en otras palabras, que la función solo tiene efectos secundarios la primera vez que se llama?

+1

¿Estás seguro de que es un problema?Si la llamada se optimiza, los datos se crearán la próxima vez. Siempre y cuando solo se pueda acceder a los datos a través del valor de retorno, no debería haber nada de malo en esto. – ughoavgfhw

+0

'pure' es específico de GCC, pero' const' no lo es. –

+0

'__attribute __ ((const))' también es un gcc-ism, pero es más ampliamente soportado por compiladores no gcc ... –

Respuesta

6

me dicen que esto es correcto basado en mi entendimiento de pura y const, pero si alguien tiene una definición precisa de los dos, por favor hablar. Esto resulta complicado porque la documentación de GCC no establece exactamente lo que significa para una función tener "ningún efecto excepto el valor de retorno" (para pure) o "no examinar ningún valor excepto sus argumentos" (para const). Obviamente todas las funciones tienen algunos efectos (usan ciclos de procesador, modifican la memoria) y examinan algunos valores (el código de función, constantes).

Los "efectos secundarios" tendrían que definirse en términos de la semántica del lenguaje de programación C, pero podemos adivinar lo que significa la gente del CCG basándose en el propósito de estos atributos, que es habilitar optimizaciones adicionales (al menos , eso es para lo que supongo que son).

Perdóname si alguna de las siguientes es demasiado básico ...

funciones puras pueden participar en la eliminación de subexpresiones comunes. Su característica es que no modifican el entorno, por lo que el compilador puede llamarlo menos veces sin cambiar la semántica del programa.

z = f(x); 
y = f(x); 

se convierte en:

z = y = f(x); 

O sea eliminado por completo si z y y no han sido utilizados.

Así que mi mejor estimación es que una definición de trabajo de "puro" es "cualquier función que se puede llamar menos veces sin cambiar la semántica del programa". Sin embargo, las llamadas a funciones que no se pueden mover, por ejemplo,

size_t l = strlen(str); // strlen is pure 
*some_ptr = '\0'; 
// Obviously, strlen can't be moved here... 

funciones Const pueden ser reordenadas, ya que no dependen del entorno dinámico.

// Assuming x and y not aliased, sin can be moved anywhere 
*some_ptr = '\0'; 
double y = sin(x); 
*other_ptr = '\0'; 

Así que mi mejor conjetura es que una definición de trabajo de "const" es "cualquier función que se puede llamar en cualquier momento sin cambiar la semántica del programa".Sin embargo, hay un peligro:

__attribute__((const)) 
double big_math_func(double x, double theta, double iota) 
{ 
    static double table[512]; 
    static bool initted = false; 
    if (!initted) { 
     ... 
     initted = true; 
    } 
    ... 
    return result; 
} 

Puesto que es constante, el compilador podría reordenar ...

pthread_mutex_lock(&mutex); 
... 
z = big_math_func(x, theta, iota); 
... 
pthread_mutex_unlock(&mutex); 
// big_math_func might go here, if the compiler wants to 

En este caso, se podría llamar simultáneamente desde dos procesadores a pesar de que sólo aparece dentro de una sección crítica en tu código. Entonces, el procesador podría decidir posponer los cambios en table después de que un cambio a initted ya se haya realizado, lo cual es una mala noticia. Puede resolver esto con barreras de memoria o pthread_once.

No creo que este error aparezca nunca en x86, y no creo que se muestre en muchos sistemas que no tienen procesadores físicos múltiples (no núcleos). Por lo tanto, funcionará bien durante años y luego fallará de repente en una computadora POWER de doble socket.

Conclusión: La ventaja de estas definiciones es que dejan claro qué tipo de cambios se permite que el compilador para realizar en presencia de estos atributos, que (creo que es) un tanto vaga en la documentación del CCG. La desventaja es que no está claro que estas sean las definiciones utilizadas por el equipo de GCC.

Si mira la especificación del lenguaje Haskell, por ejemplo, encontrará una definición mucho más precisa de pureza, ya que la pureza es tan importante para el lenguaje Haskell.

Editar: no he sido capaz de obligar a GCC o Clang en mover una solitaria llamada __attribute__((const)) función a través de otra llamada a la función, pero parece muy posible que en el futuro, algo así sucedería. ¿Recuerda cuando el -fstrict-aliasing se convirtió en el predeterminado, y todo el mundo de repente tenía muchos más errores en sus programas? Son cosas así que me vuelven cauteloso.

Me parece que cuando se marca una función __attribute__((const)), te aseguras el compilador que el resultado de la llamada a la función es la misma sin importar cuando se le llama durante la ejecución de su programa, siempre y cuando los parámetros son los mismo.

Sin embargo, se me ocurrió una forma de mover una función const fuera de una sección crítica, aunque la forma en que lo hice podría llamarse "hacer trampa" de algún tipo.

__attribute__((const)) 
extern int const_func(int x); 

int func(int x) 
{ 
    int y1, y2; 
    y1 = const_func(x); 
    pthread_mutex_lock(&mutex); 
    y2 = const_func(x); 
    pthread_mutex_unlock(&mutex); 
    return y1 + y2; 
} 

El compilador traduce esto en el siguiente código (del conjunto):

int func(int x) 
{ 
    int y; 
    y = const_func(x); 
    pthread_mutex_lock(&mutex); 
    pthread_mutex_unlock(&mutex); 
    return y * 2; 
} 

Tenga en cuenta que esto no sucederá sólo con __attribute__((pure)), el atributo const y sólo el atributo const desencadena este comportamiento .

Como puede ver, la llamada dentro de la sección crítica desapareció. Parece bastante arbitrario que se mantuviera la llamada anterior, y no estaría dispuesto a apostar que el compilador no tomará, en una futura versión, una decisión diferente sobre qué llamada mantener o si podría mover la función a alguna parte. completamente por completo

Conclusión 2: Pisa con cuidado, porque si no sabes qué promesas le estás haciendo al compilador, una versión futura del compilador puede sorprenderte.

+0

En su ejemplo con la sección crítica, ¿no sería una barrera de memoria (que típicamente es parte de la implementación de mutex) al usar la semántica de liberación de tienda en la llamada de 'desbloqueo' impide la reubicación de la llamada a 'big_math_func' desde dentro de la sección crítica fuera de la sección crítica? – Jason

+0

@Jason: una barrera de memoria solo evita que el procesador (no el compilador) reordena ciertas operaciones. A modo de comparación, la palabra clave 'volátil 'evita que el compilador (no el procesador) reordena ciertas operaciones. Cuando escribe código para sistemas multiprocesador (especialmente en un nivel bajo), ayuda pensar tanto en el procesador como en el compilador como su enemigo. –

+0

Un mutex también es una barrera compiladora completa para cualquier dato al que se pueda acceder desde otros hilos, es decir, básicamente cualquier cosa excepto variables locales cuya dirección nunca ha tomado o al menos nunca ha pasado al mundo exterior. –

Cuestiones relacionadas