2010-05-09 6 views
14

Estoy pensando en utilizar las funciones pure/const con mayor fuerza en mi código C++. (pure/const attribute in GCC)Funciones Pure/const en C++

Sin embargo, tengo curiosidad por lo estricto que debería ser y lo que posiblemente podría romperse.

El caso más obvio son salidas de depuración (en cualquier forma, podría estar en cout, en algún archivo o en alguna clase de depuración personalizada). Probablemente tenga muchas funciones, que no tienen ningún efecto secundario a pesar de este tipo de salida de depuración. No importa si la salida de depuración se realiza o no, esto no tendrá ningún efecto en el resto de mi aplicación.

O en otro caso estoy pensando en el uso de alguna clase SmartPointer que puede hacer algunas cosas adicionales en la memoria global cuando se está en modo de depuración. Si uso dicho objeto en una función pure/const, tiene algunos efectos secundarios leves (en el sentido de que algunos recuerdos probablemente serán diferentes) que no deberían tener ningún efecto secundario real (en el sentido de que el comportamiento está en de cualquier manera diferente).

Similar también para mutexes y otras cosas. Puedo pensar en muchos casos complejos donde tiene algunos efectos secundarios (en el sentido de que algunos recuerdos serán diferentes, tal vez incluso se crean algunos hilos, se hace alguna manipulación del sistema de archivos, etc.) pero no tiene diferencia computacional (todos esos efectos secundarios) bien podría ser dejado de lado y yo preferiría eso).

Por lo tanto, para resumir, quiero marcar funciones como pure/const que no son puros/const en un sentido estricto. Un ejemplo sencillo:

int foo(int) __attribute__((const)); 

int bar(int x) { 
    int sum = 0; 
    for(int i = 0; i < 100; ++i) 
     sum += foo(x); 
    return sum; 
} 

int foo_callcounter = 0; 

int main() { 
    cout << "bar 42 = " << bar(42) << endl; 
    cout << "foo callcounter = " << foo_callcounter << endl; 
} 

int foo(int x) { 
    cout << "DEBUG: foo(" << x << ")" << endl; 
    foo_callcounter++; 
    return x; // or whatever 
} 

Tenga en cuenta que la función foo no está const en un sentido estricto. Sin embargo, no importa qué foo_callcounter está al final. Tampoco importa si la declaración de depuración no está hecha (en caso de que no se llame a la función).

que sería de esperar la salida:

DEBUG: foo(42) 
bar 42 = 4200 
foo callcounter = 1 

Y sin optimización:

DEBUG: foo(42) (100 times) 
bar 42 = 4200 
foo callcounter = 100 

Ambos casos son totalmente bien porque lo único que importa para mi caso de uso es el valor de retorno de la barra (42).

¿Cómo funciona en la práctica? Si marcó funciones como pure/const, ¿podría romper algo (teniendo en cuenta que el código es correcto)?


Tenga en cuenta que sé que algunos compiladores pueden no admitir este atributo en absoluto. (Por cierto, los estoy recogiendo here.) También sé cómo hacer uso de los atributos de una manera que el código se mantiene portátil (a través de #defines). Además, todos los compiladores que son interesantes para mí lo soportan de alguna manera; así que no me importa si mi código se ejecuta más lento con compiladores que no lo hacen.

También sé que el código optimizado probablemente se vea diferente según el compilador e incluso la versión del compilador.


muy relevante es también this LWN article "Implications of pure and constant functions", especialmente el capítulo "Trampas". (Gracias ArtemGr por la pista.)

+1

¿Por qué está tan interesado en usar este atributo? ¿Ves importantes beneficios de rendimiento en código real (no artificial)? Jugué con él hace unos años y nunca vi que hiciera algo que la línea ya no estaba haciendo. – timday

+0

Por supuesto, la línea tendría más o menos las mismas ventajas e incluso más sobre esto. Sin embargo, no solo quiere poner ** todo ** su código en los archivos de encabezado. Puede que exista una función bastante grande/grande que no desee en un encabezado, por lo que no puede estar en línea pero que todavía es pura/const. Además, puede haber casos en los que su función no sea pura/const en un sentido estricto, por lo que incluso al subrayarla no se realizarían tales optimizaciones (porque el compilador lo ve como no puro/const) pero usted tiene el conocimiento adicional de que puede ser usado como si fuera puro/const. – Albert

+1

Consulte también el capítulo "Trucos" en http://lwn.net/Articles/285332/ – ArtemGr

Respuesta

1

Yo esperaría que la salida:

Yo esperaría que la entrada:

int bar(int x) { 
    return foo(x) * 100; 
} 

Su código se ve realmente extraño para mí. Como mantenedor, creo que foo en realidad tiene efectos secundarios o, más probablemente, lo reescriba inmediatamente en la función anterior.

¿Cómo funciona en la práctica? Si marcó funciones como pure/const, ¿podría romper algo (teniendo en cuenta que el código es correcto)?

Si el código es correcto, entonces no. Pero las posibilidades de que su código sea correcto son pequeñas.Si el código es incorrecta, esta característica puede enmascarar errores:

int foo(int x) { 
    globalmutex.lock(); 
    // complicated calculation code 
     return -1; 
    // more complicated calculation 
    globalmutex.unlock(); 
    return x; 
} 

ahora, dada la barra de arriba:

int main() { 
    cout << bar(-1); 
} 

Esto termina con __attribute__((const)) pero los puntos muertos de otra manera.

También depende en gran medida de la implementación. Por ejemplo:

void f() { 
    for(;;) 
    { 
     globalmutex.unlock(); 
     cout << foo(42) << '\n'; 
     globalmutex.lock(); 
    } 
} 

Cuando el compilador debe mover la llamada foo(42)? ¿Está permitido optimizar este código? ¡No en general! Entonces, a menos que el ciclo sea realmente trivial, no tiene beneficios de su función. Pero si su bucle es trivial, puede optimizarlo usted mismo.

EDIT: como Albert pidió una situación menos obvio, aquí se trata: F o ejemplo, si implementa operator << para una ostream, se utiliza el ostream :: centinela que bloquea el búfer de la secuencia. Supongamos que llamas a puro/const fdespués de que lanzaste o antes de lo bloqueaste. Alguien usa este operador cout << YourType() y f también usa cout << "debug info". Según usted, el compilador puede poner la invocación de f en la sección crítica. Se produce un punto muerto.

+0

Acerca de su ejemplo: el compilador puede mover el código donde quiera porque le dijo que no depende de ninguna otra manera en todo lo demás en su código. Lo cual está mal, por supuesto, porque depende del globalmutex. - Por supuesto, usar la palabra clave de forma incorrecta/inválida puede romper las cosas, pero eso no es lo que estoy pidiendo. – Albert

+0

@ Albert: Tus preguntas suenan así: "¿Puedo romper un código marcando una función pura/const excepto aquellas situaciones en las que es incorrecto usar pure/const?" ¿Qué respuesta estás esperando? – ybungalobill

+0

No, eso no es lo que estaba preguntando. Me preguntaba: ¿Puedo romper algo marcando las funciones pure/const donde debería ser válido/correcto para hacerlo? Es decir. ¿Hay algún caso no obvio o siempre funcionará? – Albert

3

Definitivamente podría romper la portabilidad de su código. ¿Y por qué querrías implementar tu propio puntero inteligente, aparte de la experiencia de aprendizaje? ¿No hay suficientes disponibles para usted en bibliotecas estándar (cercanas)?

+0

¿Cómo podría romper la compatibilidad, dado que solo lo usaría para casos en los que tendría alguna diferencia en el comportamiento/el resultado computacional? ? El SmartPointer fue codificado principalmente para poder agregar código de depuración personalizado cuando sea necesario. Además, necesitaba una posibilidad para algunas C-estructuras para ejecutar una función de limpieza personalizada en lugar de simplemente un 'eliminar'. (Ofc podría haberlo hecho también a través de una clase contenedora). Y no había smartptr en ninguna de las librerías que usamos en ese momento. Ya estamos migrando para impulsar :: shared_ptr en este momento. No importa tanto para mi pregunta qué smartptr se usa. – Albert

+2

@Albert No dije "compatibilidad". Dije "portabilidad": los atributos no son una característica estándar de C++. Si compila el código con otro compilador, existe la posibilidad de que no reconozca la etiqueta __attribute__, o si lo hace no tratará el atributo específico de la misma manera que lo hace GCC. –

+1

@Neil Si se dirige a otros compiladores, puede #definir __atribuir __ (x) y hacer que el código se compile para que no sea tan malo. Además, a veces la portabilidad es menos importante que el rendimiento. –

17

Estoy pensando en usar las funciones pure/const con mayor fuerza en mi código C++.

Esa es una pendiente resbaladiza. Estos atributos son no estándar y su beneficio se limita principalmente a micro-optimizaciones.

No es una buena solución de compromiso. Escriba código limpio en su lugar, no aplique tales micro-optimizaciones a menos que haya perfilado cuidadosamente y no haya forma de evitarlo. O no en absoluto.

Observe que en principio estos atributos son bastante agradables porque indican suposiciones implícitas de las funciones explícitamente tanto para el compilador como para el programador. Eso es bueno. Sin embargo, existen otros métodos para hacer suposiciones similares explícitas (incluida la documentación). Pero dado que estos atributos no son estándar, tienen sin lugar en el código normal. Deben restringirse a muy uso juicioso en bibliotecas de rendimiento crítico donde el autor intenta emitir el mejor código para cada compilador. Es decir, el escritor es consciente del hecho de que solo GCC puede usar estos atributos y ha tomado diferentes decisiones para otros compiladores.

+4

Para eso es '# define', por lo que puedes documentar estas facetas del código para futuros programadores, y también permitir que el compilador conozca el secreto cuando sea compatible. –

+3

Veo que los necios partidarios están fuera de nuevo, votando. –

+0

Sé que estos atributos son solo GCC. Por lo que probablemente algunos otros compiladores tengan banderas similares. (Consulte aquí: http://stackoverflow.com/questions/2798188/pure-const-function-attributes-in-different-compilers) Puedo pensar en muchos casos en los que tendría una notable diferencia de rendimiento. Además, en principio, el código es más claro si deja la optimización al compilador y no lo hace a mano. Y estoy bastante seguro de que no encontraría todo el código a mano donde tal optimización sería buena. – Albert

0

Creo que nadie sabe esto (con la excepción de los programadores gcc), simplemente porque confías en el comportamiento indefinido e indocumentado, que puede cambiar de una versión a otra. Pero ¿qué tal algo como esto:


#ifdef NDEBUG \ 
    #define safe_pure __attribute__((pure)) \ 
#else \ 
    #define safe_pure \ 
#endif 

Yo sé que no es exactamente lo que quiere, pero ahora se puede utilizar el atributo pura sin romper las reglas.
Si quiere saber la respuesta, puede preguntar en los foros de gcc (lista de correo, lo que sea), ellos deberían poder darle la respuesta exacta.
Significado del código: cuando se define NDEBUG (símbolo utilizado en assert macros), no depuramos, no tenemos efectos secundarios, podemos usar el atributo pure. Cuando se define, tenemos efectos secundarios, por lo que no usará atributos puros.

+0

pure/const está muy claramente definido. El comportamiento de la optimización puede diferir, pero eso no importa en absoluto (debido a la definición de pure/const). Tampoco estoy realmente preguntando aquí sobre cómo '# ifdef' mi código para diferentes compiladores. Estoy preguntando si puede haber algún problema con pure/const. – Albert

+0

Sí, pure/const están claramente definidos, pero si también se definió su comportamiento de error (por error me refiero al uso de pure/const en la función non-pure/non-const), no tendría que preguntar aquí. Además, no te proporciono #ifdef para compilador diferente, te proporciono #ifdef para diferentes configuraciones (depuración y publicación). –

+0

Por supuesto, también está definido: el compilador lo maneja de la misma manera (porque no sabrá en ese momento si su atributo const/pure es realmente correcto; lo asumirá porque usted puso el atributo). – Albert

0

Examinaré el asm generado para ver qué diferencia hacen. (Mi suposición sería que el cambio de flujos de C++ a otra cosa produciría un mayor beneficio real, consulte: http://typethinker.blogspot.com/2010/05/are-c-iostreams-really-slow.html)

+0

se perdió el objetivo de la pregunta por completo, la transmisión solo está aquí en la construcción de depuración para ayudar a analizar lo que está sucediendo ... –

+0

@Matthieu M. ¿Cómo se está sugiriendo observar el asm generado que no tiene el punto? Además, la pregunta indica el uso de atributos en las funciones de depuración ellos mismos; No veo ninguna mención de que el uso de la secuencia sea solo en compilaciones de depuración. Simplemente estaba tratando de resaltar que el uso de la transmisión probablemente eclipsaría cualquier beneficio que pudiera proporcionar el uso del atributo. – KSchmidt

+0

Admito que (desafortunadamente) la biblioteca IO Stream es lenta, pero el texto trata del uso de los atributos pure/const en el modo de depuración y el 3er párrafo comienza con "El caso más obvio es la salida de depuración". También estoy de acuerdo en que echar un vistazo al asm generado podría ayudar, aunque si se usa un compilador inteligente no debería ser necesario bajar mucho. La eliminación de subexpresión común se puede ver en un nivel más alto en ambos framework gcc/llvm. –