style guide coding c performance constants c-preprocessor

coding - c style guide



"Constante estática" vs "# definir" para la eficiencia en C (5)

Considere los siguientes 2 archivos de prueba

Test1.c : Utiliza la función estática constante.

// Test1.c uses static const.. #include <stdio.h> static const foo = 6; int main() { printf("%d", foo); return 0; }

Test2.c: utiliza macro.

// Test2.c uses macro.. #include <stdio.h> #define foo 6 int main() { printf("%d", foo); return 0; }

y las equivalencias de ensamblaje correspondientes al usar gcc -O0 (predeterminado) son las siguientes,

Asamblea para Test1.c:

0000000000000000 <main>: 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 48 83 ec 20 sub rsp,0x20 8: e8 00 00 00 00 call d <main+0xd> d: b8 06 00 00 00 mov eax,0x6 12: 89 c2 mov edx,eax 14: 48 8d 0d 04 00 00 00 lea rcx,[rip+0x4] # 1f <main+0x1f> 1b: e8 00 00 00 00 call 20 <main+0x20> 20: b8 00 00 00 00 mov eax,0x0 25: 48 83 c4 20 add rsp,0x20 29: 5d pop rbp 2a: c3 ret 2b: 90 nop

Montaje para Test2.c:

0000000000000000 <main>: 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 48 83 ec 20 sub rsp,0x20 8: e8 00 00 00 00 call d <main+0xd> d: ba 06 00 00 00 mov edx,0x6 12: 48 8d 0d 00 00 00 00 lea rcx,[rip+0x0] # 19 <main+0x19> 19: e8 00 00 00 00 call 1e <main+0x1e> 1e: b8 00 00 00 00 mov eax,0x0 23: 48 83 c4 20 add rsp,0x20 27: 5d pop rbp 28: c3 ret 29: 90 nop

En ambos casos, no está utilizando memoria externa. Pero la diferencia es que #define reemplaza foo por el valor, static const es una instrucción, por lo que incrementa el puntero de la instrucción a la siguiente instrucción y usa 1 registro adicional para almacenar el valor.

Por esto, podemos decir que la macro es mejor que las constantes estáticas pero la diferencia es mínima.

EDITAR: Cuando se usa la opción de compilación -O3 (es decir, en la optimización en), tanto test1.c como test2.c evalúan lo mismo.

0000000000000000 <main>: 0: 48 83 ec 28 sub rsp,0x28 4: e8 00 00 00 00 call 9 <main+0x9> 9: 48 8d 0d 00 00 00 00 lea rcx,[rip+0x0] # 10 <main+0x10> 10: ba 06 00 00 00 mov edx,0x6 15: e8 00 00 00 00 call 1a <main+0x1a> 1a: 31 c0 xor eax,eax 1c: 48 83 c4 28 add rsp,0x28 20: c3 ret 21: 90 nop

Entonces, gcc trata tanto a static const como a #define como lo mismo cuando optimiza.

Hace poco me preguntaba cuál es la diferencia entre #define y static const en C y por qué existen dos métodos para hacer las mismas cosas. Encontré algunas personas que tenían preguntas similares aquí:

Mucha gente habla sobre las mejores prácticas y las convenciones, así como por razones prácticas para usar una sobre la otra, como la necesidad de pasar un puntero a una constante, lo que puedo hacer con una static const pero no con una #define . Sin embargo, todavía tengo que encontrar a alguien que hable sobre una comparación de la eficiencia de los dos.

Por lo que entiendo sobre el preprocesador de C , si tengo una declaración como esta:

#define CONSTANT 6

Creo un valor constante que se puede usar así.

char[CONSTANT] que se reemplazará con esta declaración char[6] antes de compilarse.

Esto me parece que sería más eficiente que usar una static const constant = 6; porque esto crearía una variable llamada constante que viviría en la pila y supongo que vendría con algo más de equipaje que un #define . Suponiendo que necesito una constante en una situación en la que podría optar por usar un preprocesador #define o una instrucción static const sin razones obvias para elegir una sobre la otra, ¿cuál es más eficiente? ¿Y cómo exactamente voy a probar esto yo mismo?


Has cambiado totalmente tu pregunta. Aquí está mi respuesta a tu nueva pregunta:

Debido a que estamos hablando de C, y suponiendo que está declarando la matriz en la pila, la respuesta es realmente muy interesante. En este caso, no es posible que haya alguna diferencia entre los dos. ¡El "6" no se usa realmente en tiempo de ejecución! Debido a que solo lo está usando para dimensionar una matriz en la pila, el compilador simplemente usa esto para calcular cuánto espacio de pila se requiere para la variable.

Supongamos que tiene un espacio de direcciones de 32 bits y su función local contiene esta matriz de 6 bytes (myArray) y un entero de 32 bits sin signo (myInt). El compilador crea las siguientes instrucciones para ingresar a esta función: - Escriba la dirección de retorno de 4 bytes en la pila - Mueva el puntero de la pila hacia adelante 10 bytes
Al ejecutar la función, el tiempo de ejecución no conoce los nombres ni los tamaños de ninguna variable. Si tu codigo dice

myInt = 5; myArray[myInt] = 25;

entonces el compilador habrá generado estas instrucciones:

- write 00000000 00000000 00000000 00000101 starting at address (StackPointer - 4) - write 00001101 starting at (StackPointer - 10 + (value at Stackpointer - 4))

Como puede ver, el valor "6" no se usa en el tiempo de ejecución. De hecho, puedes escribir al índice 6, 7, 8, lo que quieras. El tiempo de ejecución no sabrá que está desbordando el final de la matriz. (pero dependiendo de cómo escriba el código, el compilador puede detectar el error en el momento de la compilación)

Pasé por alto algunos detalles allí (sin duda algunos de los cuales ni siquiera estoy al tanto) pero eso es lo esencial. (Doy la bienvenida a sus comentarios)

Definir el 6 como una "const" puede causar que el valor se almacene en 4 bytes de espacio inútil, pero eso no afectará la ejecución. Obviamente, se optimizará porque nunca se utiliza.

Pero, habiendo dicho todo eso, nunca se preocupe por ahorrar un byte de espacio. La capacidad de mantenimiento del código es mucho más importante. El riesgo de introducir un pequeño error, o hacer que su código sea un poco menos legible, estos riesgos son un billón de billones de veces más caros que el costo de unos pocos bytes adicionales o un ciclo de procesador adicional. Use constantes y enumeraciones para aprovechar todos los beneficios enumerados aquí


La forma rápida de probar preguntas simples de optimización es usar godbolt .

Para su problema específico, un compilador de optimización moderno debería ser capaz de producir el mismo código para ambos casos y, de hecho, solo los optimizará a una constante. Podemos ver esto con el siguiente programa ( verlo en vivo ):

#include <stdio.h> #define CONSTANT 6 static const int constant = 6; void func() { printf( "%d/n", constant ) ; printf( "%d/n", CONSTANT ) ; }

En ambos casos ambos accesos se reducen a lo siguiente:

movl $6, %esi #,


Si la definición de la constante es visible para la traducción, el compilador es ciertamente capaz de utilizar eso como una optimización.

esto crearía una variable llamada constante que viviría en la pila y supongo que vendría con algo más de equipaje que un #define.

Podría "vivir" en múltiples lugares. Un compilador puede sustituir la constante donde se hace referencia, sin requerir almacenamiento estático o de pila.

Suponiendo que necesito una constante en una situación en la que podría optar por usar un preprocesador #define o una instrucción const estática sin razones obvias para elegir una sobre la otra, ¿cuál es más eficiente?

Depende del compilador y de la arquitectura. Me da la impresión de que algunas personas creen que #define tiene una gran ventaja. No lo hace El caso obvio es una evaluación compleja o llamada de función (digamos sin(4.8) . Considere una constante utilizada dentro de un bucle. Una constante apropiadamente podría evaluarse una vez. Una definición podría evaluarse en cada iteración.

¿Y cómo exactamente voy a probar esto yo mismo?

Lea el ensamblaje producido por cada compilador que use y mida.

Si desea una regla general, diría "Use una constante, a menos que #define brinde una mejora mensurable en el escenario".

Hubo una buena reseña en los documentos de GCC sobre esto. Tal vez alguien recuerda dónde estaba exactamente.


static const variables static const no se crean (al menos no deberían) en la pila; el espacio para ellos se reserva cuando se carga el programa, por lo que no debe haber una penalización de tiempo de ejecución asociada con su creación.

Puede haber una penalización de tiempo de ejecución asociada con su inicialización. aunque la versión de gcc que estoy usando inicializa la constante en el momento de la compilación; No sé qué tan común es ese comportamiento. Si existe una penalización de tiempo de ejecución, solo se produce una vez al inicio del programa.

Más allá de eso, cualquier diferencia de rendimiento en tiempo de ejecución entre un objeto estático y const cualificado y un literal 1 (que es a lo que una macro eventualmente se expandirá) debería ser despreciable o inexistente, dependiendo del tipo de literal y la operación involucrada.

Ejemplo estúpido ( gcc version 4.1.2 20070115 (SUSE Linux) ):

#include <stdio.h> #define FOO_MACRO 5 static const int foo_const = 5; int main( void ) { printf( "sizeof FOO_MACRO = %zu/n", sizeof FOO_MACRO ); printf( "sizeof foo_const = %zu/n", sizeof foo_const ); printf( " &foo_const = %p/n", ( void * ) &foo_const ); printf( "FOO_MACRO = %d/n", FOO_MACRO ); printf( "foo_const = %d/n", foo_const ); return 0; }

Salida:

sizeof FOO_MACRO = 4 sizeof foo_const = 4 &foo_const = 0x400660 FOO_MACRO = 5 foo_const = 5

La dirección de foo_const encuentra en la sección .rodata del binario:

[fbgo448@n9dvap997]~/prototypes/static: objdump -s -j .rodata static static: file format elf64-x86-64 Contents of section .rodata: 40065c 01000200 05000000 73697a65 6f662046 ........sizeof F ^^^^^^^^ 40066c 4f4f5f4d 4143524f 203d2025 7a750a00 OO_MACRO = %zu.. 40067c 73697a65 6f662066 6f6f5f63 6f6e7374 sizeof foo_const 40068c 203d2025 7a750a00 20202020 20202666 = %zu.. &f 40069c 6f6f5f63 6f6e7374 203d2025 700a0046 oo_const = %p..F 4006ac 4f4f5f4d 4143524f 203d2025 640a0066 OO_MACRO = %d..f 4006bc 6f6f5f63 6f6e7374 203d2025 640a00 oo_const = %d..

Tenga en cuenta que el objeto ya está inicializado en 5, por lo que no hay una penalización de inicialización en tiempo de ejecución.

En las declaraciones de printf , la instrucción de cargar el valor de foo_const en %esi requiere un byte más que el de cargar el valor literal 0x5 , y la instrucción tiene que eliminar la referencia al registro %rip :

400538: be 05 00 00 00 mov $0x5,%esi ^^^^^^^^^^^^^^ 40053d: bf ab 06 40 00 mov $0x4006ab,%edi 400542: b8 00 00 00 00 mov $0x0,%eax 400547: e8 e4 fe ff ff callq 400430 <printf@plt> 40054c: 8b 35 0e 01 00 00 mov 270(%rip),%esi # 400660 <foo_const> ^^^^^^^^^^^^^^^^^ 400552: bf bb 06 40 00 mov $0x4006bb,%edi 400557: b8 00 00 00 00 mov $0x0,%eax 40055c: e8 cf fe ff ff callq 400430 <printf@plt>

¿Esto se traducirá en una diferencia de rendimiento de tiempo de ejecución medible? Tal vez, bajo las circunstancias correctas. Si está haciendo algo vinculado a la CPU varios cientos de miles de veces en un circuito cerrado, entonces sí, usar una macro (que se resuelve en un literal) sobre una variable static const puede ser considerablemente más rápido. Si esto es algo que sucede una vez durante la vida útil del programa, entonces la diferencia es demasiado pequeña para medirla y no hay una razón convincente para usar la macro sobre la variable static const .

Como siempre, la corrección y la capacidad de mantenimiento son más importantes que el rendimiento 2 . Es menos probable que cometa un error al utilizar una static const lugar de una macro. Considere el siguiente escenario:

#define FOO 1+2 ... x = FOO * 3;

¿Qué respuesta esperarías y qué respuesta obtendrías ? Compara eso con

static const int foo = 1+2; ... x = foo * 3;

Sí, podría arreglar el caso de macros utilizando paréntesis - (1 + 2) . El punto es que este escenario no es un problema si usa el objeto static const . Es una forma menos de dispararte en el pie.

1. Por ahora, solo estoy hablando de simples literales escalares (enteros o flotadores), no de literales compuestos; No he investigado su comportamiento.

2. No importa qué tan rápido sea su código si le da una respuesta incorrecta o si hace algo incorrecto. No importa qué tan rápido sea su código si nadie puede repararlo o actualizarlo porque no pueden entender cómo funciona. No importa qué tan rápido sea su código si muere en el primer indicio de mala entrada. No importa qué tan rápido sea su código si abre la puerta al malware.