Hace unos meses, mencioné en una publicación que 
esto es un mito, como si const ayudara a habilitar las optimizaciones del compilador en C y C ++ . Decidí que esta declaración debería explicarse, especialmente porque yo mismo creía en este mito antes. Comenzaré con teoría y ejemplos artificiales, y luego pasaré a experimentos y puntos de referencia basados en una base de código real: SQLite.
Prueba simple
Comencemos con, como me pareció, el ejemplo más simple y obvio de acelerar el código C con 
const . Digamos que tenemos dos declaraciones de funciones:
 void func(int *x); void constFunc(const int *x); 
Y supongamos que hay dos versiones del código:
 void byArg(int *x) { printf("%d\n", *x); func(x); printf("%d\n", *x); } void constByArg(const int *x) { printf("%d\n", *x); constFunc(x); printf("%d\n", *x); } 
Para ejecutar 
printf() , el procesador debe recuperar 
*x de la memoria a través de un puntero. Obviamente, la ejecución de 
constByArg() puede ser un poco más rápida, porque el compilador sabe que 
*x es una constante, por lo que no es necesario cargar su valor nuevamente después de que 
constFunc() haya hecho. Derecho? Veamos el código de ensamblador generado por GCC con optimizaciones habilitadas:
 $ gcc -S -Wall -O3 test.c $ view test.s 
Y aquí está el resultado completo del ensamblador para 
byArg() :
 byArg: .LFB23: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl (%rdi), %edx movq %rdi, %rbx leaq .LC0(%rip), %rsi movl $1, %edi xorl %eax, %eax call __printf_chk@PLT movq %rbx, %rdi call func@PLT # The only instruction that's different in constFoo movl (%rbx), %edx leaq .LC0(%rip), %rsi xorl %eax, %eax movl $1, %edi popq %rbx .cfi_def_cfa_offset 8 jmp __printf_chk@PLT .cfi_endproc 
La única diferencia entre el código ensamblador generado por 
byArg() y 
constByArg() es que 
constByArg() tiene una 
call constFunc@PLT , como en el código fuente. 
const sí mismo no hace ninguna diferencia.
Bien, eso fue GCC. Quizás necesitamos un compilador más inteligente. Di Clang
 $ clang -S -Wall -O3 -emit-llvm test.c $ view test.ll 
Aquí está el código intermedio. Es más compacto que el ensamblador, y eliminaré ambas funciones, para que entienda lo que quiero decir con "sin diferencia, excepto por la llamada":
 ; Function Attrs: nounwind uwtable define dso_local void @byArg(i32*) local_unnamed_addr #0 { %2 = load i32, i32* %0, align 4, !tbaa !2 %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %2) tail call void @func(i32* %0) #4 %4 = load i32, i32* %0, align 4, !tbaa !2 %5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4) ret void } ; Function Attrs: nounwind uwtable define dso_local void @constByArg(i32*) local_unnamed_addr #0 { %2 = load i32, i32* %0, align 4, !tbaa !2 %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %2) tail call void @constFunc(i32* %0) #4 %4 = load i32, i32* %0, align 4, !tbaa !2 %5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4) ret void } 
Opción que (tipo) funciona
Y aquí está el código en el que la presencia de 
const realmente importa:
 void localVar() { int x = 42; printf("%d\n", x); constFunc(&x); printf("%d\n", x); } void constLocalVar() { const int x = 42;  
El código del ensamblador para 
localVar() , que contiene dos instrucciones optimizadas fuera de 
constLocalVar() :
 localVar: .LFB25: .cfi_startproc subq $24, %rsp .cfi_def_cfa_offset 32 movl $42, %edx movl $1, %edi movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax leaq .LC0(%rip), %rsi movl $42, 4(%rsp) call __printf_chk@PLT leaq 4(%rsp), %rdi call constFunc@PLT movl 4(%rsp), %edx # not in constLocalVar() xorl %eax, %eax movl $1, %edi leaq .LC0(%rip), %rsi # not in constLocalVar() call __printf_chk@PLT movq 8(%rsp), %rax xorq %fs:40, %rax jne .L9 addq $24, %rsp .cfi_remember_state .cfi_def_cfa_offset 8 ret .L9: .cfi_restore_state call __stack_chk_fail@PLT .cfi_endproc 
El middleware LLVM es un poco más limpio. 
load antes de que la segunda llamada a 
printf() se optimizara fuera de 
constLocalVar() :
 ; Function Attrs: nounwind uwtable define dso_local void @localVar() local_unnamed_addr #0 { %1 = alloca i32, align 4 %2 = bitcast i32* %1 to i8* call void @llvm.lifetime.start.p0i8(i64 4, i8* nonnull %2) #4 store i32 42, i32* %1, align 4, !tbaa !2 %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 42) call void @constFunc(i32* nonnull %1) #4 %4 = load i32, i32* %1, align 4, !tbaa !2 %5 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0), i32 %4) call void @llvm.lifetime.end.p0i8(i64 4, i8* nonnull %2) #4 ret void } 
Entonces, 
constLocalVar() ignoró con éxito el reinicio 
*x , pero podría notar algo extraño: en los cuerpos 
localVar() y 
constLocalVar() la misma llamada a 
constFunc() . Si el compilador puede descubrir que 
constFunc() no modificó 
*x en 
constLocalVar() , entonces ¿por qué no puede entender que la misma llamada de función no modificó 
*x en 
localVar() ?
La explicación es por qué no es práctico usar 
const en C como optimización. En C, 
const tiene esencialmente dos significados posibles:
- puede significar que una variable es un seudónimo de solo lectura para algunos datos, que puede ser constante o no.
 
- o puede significar que la variable es realmente una constante. Si desatas la constde un puntero a un valor constante y luego le escribes, obtendrás un comportamiento indefinido. Por otro lado, no habrá problema siconstes un puntero a un valor que no es una constante.
 
Aquí hay un ejemplo explicativo de implementación de 
constFunc() :
 
localVar() le dio a 
constFunc() un puntero 
const a una variable no 
const . Dado que la variable no era 
const inicialmente, 
constFunc() puede resultar ser un mentiroso y modificar a la fuerza la variable sin iniciar UB. Por lo tanto, el compilador no puede suponer que después de devolver 
constFunc() variable tendrá el mismo valor. La variable en 
constLocalVar() realmente es 
const , por lo que el compilador no puede suponer que no se cambiará, porque esta vez 
será UB para 
constFunc() , de modo que el compilador desenlazará 
const y escribirá en la variable.
Las 
byArg() y 
constByArg() del primer ejemplo son inútiles, porque el compilador no puede 
constByArg() si 
*x es 
const .
¿Pero de dónde vino la inconsistencia? Si el compilador puede suponer que 
constFunc() no cambia su argumento cuando se lo llama desde 
constLocalVar() , entonces puede aplicar las mismas optimizaciones a las llamadas 
constFunc() , ¿verdad? No El compilador no puede asumir que 
constLocalVar() vez se llamará a 
constLocalVar() . Y si no lo hace (por ejemplo, porque es solo un resultado adicional del generador de código o la operación de macro), entonces 
constFunc() puede cambiar silenciosamente los datos sin iniciar UB.
Es posible que deba leer los ejemplos y la explicación anteriores varias veces. No te preocupes que suene absurdo, lo es. Desafortunadamente, escribir en variables 
const es el peor tipo de UB: la mayoría de las veces, el compilador ni siquiera sabe si será UB. Por lo tanto, cuando el compilador ve 
const , debe proceder del hecho de que alguien puede cambiarlo en algún lugar, lo que significa que el compilador no puede usar 
const para la optimización. En la práctica, esto es cierto, porque una gran cantidad de código C real contiene un rechazo de 
const en el estilo de "Sé lo que estoy haciendo".
En resumen, hay muchas situaciones en las que el compilador no puede usar 
const para la optimización, incluida la recuperación de datos de otro ámbito mediante un puntero o la colocación de datos en un montón. O peor aún, generalmente en situaciones donde el compilador no puede usar 
const , esto no es necesario. Por ejemplo, cualquier compilador que se respete puede comprender sin 
const que en este código 
x es una constante:
 int x = 42, y = 0; printf("%d %d\n", x, y); y += x; printf("%d %d\n", x, y); 
Entonces 
const casi inútil para la optimización, porque:
- Con algunas excepciones, el compilador se ve obligado a ignorarlo, ya que algunos códigos pueden desatar legalmente la const.
 
- En la mayoría de las excepciones anteriores, el compilador aún puede entender que la variable es una constante.
 
C ++
Si escribe en C ++, entonces 
const puede afectar la generación de código a través de la sobrecarga de funciones. Puede tener sobrecargas 
const y no 
const de la misma función, y no 
const puede ser optimizado (por un programador, no un compilador), por ejemplo, para copiar menos.
 void foo(int *p) {  
Por un lado, no creo que en la práctica esto se aplique a menudo en código C ++. Por otro lado, para que realmente marque la diferencia, un programador debe hacer suposiciones que no están disponibles para el compilador, ya que no están garantizadas por el lenguaje.
Experimente con SQLite3
Suficiente teoría y ejemplos descabellados. ¿Qué efecto tiene 
const en la base de código real? Decidí experimentar con SQLite DB (versión 3.30.0) porque:
- Utiliza const.
- Esta es una base de código no trivial (más de 200 KLOC).
 
- Como base de datos, incluye varios mecanismos, comenzando con el procesamiento de valores de cadena y terminando con la conversión de números hasta la fecha.
 
- Se puede probar con una carga limitada del procesador.
 
Además, el autor y los programadores involucrados en el desarrollo ya han pasado años mejorando la productividad, por lo que podemos suponer que no se perdieron nada obvio.
Preparación
Hice dos copias del 
código fuente . Uno compilado en modo normal, y el segundo preprocesado utilizando un hack para convertir a 
const en un comando inactivo:
 #define const 
(GNU) 
sed puede agregar esto encima de cada archivo con el comando 
sed -i '1i#define const' *.c *.h .
SQLite complica las cosas un poco, usando scripts para generar código durante la compilación. Afortunadamente, los compiladores introducen mucho ruido al mezclar código con 
const y sin 
const , por lo que inmediatamente puede notar y configurar los scripts para agregar mi código anti- 
const .
La comparación directa de los códigos compilados no tiene sentido, ya que un pequeño cambio puede afectar todo el esquema de memoria, lo que conducirá a un cambio en los punteros y las llamadas de función en todo el código. Por lo tanto, tomé un elenco desmontado ( 
objdump -d libSQLite3.so.0.8.6 ) como el tamaño del binario y el nombre mnemotécnico de cada instrucción. Por ejemplo, esta función:
 000000000005d570 <SQLite3_blob_read>: 5d570: 4c 8d 05 59 a2 ff ff lea -0x5da7(%rip),%r8 # 577d0 <SQLite3BtreePayloadChecked> 5d577: e9 04 fe ff ff jmpq 5d380 <blobReadWrite> 5d57c: 0f 1f 40 00 nopl 0x0(%rax) 
Se convierte en:
 SQLite3_blob_read 7lea 5jmpq 4nopl 
Al compilar, no cambié la configuración del ensamblaje SQLite.
Análisis de código compilado
Para libSQLite3.so, la versión con 
const ocupó 4.740.704 bytes, aproximadamente un 0.1% más que la versión sin 
const con 4.736.712 bytes. En ambos casos, se exportaron 1374 funciones (sin contar las funciones auxiliares de bajo nivel en el PLT), y 13 tuvieron diferencias en los lanzamientos.
Algunos cambios estaban relacionados con el hack de preprocesamiento. Por ejemplo, aquí está una de las funciones modificadas (eliminé algunas definiciones específicas de SQLite):
 #define LARGEST_INT64 (0xffffffff|(((int64_t)0x7fffffff)<<32)) #define SMALLEST_INT64 (((int64_t)-1) - LARGEST_INT64) static int64_t doubleToInt64(double r){ /* ** Many compilers we encounter do not define constants for the ** minimum and maximum 64-bit integers, or they define them ** inconsistently. And many do not understand the "LL" notation. ** So we define our own static constants here using nothing ** larger than a 32-bit integer constant. */ static const int64_t maxInt = LARGEST_INT64; static const int64_t minInt = SMALLEST_INT64; if( r<=(double)minInt ){ return minInt; }else if( r>=(double)maxInt ){ return maxInt; }else{ return (int64_t)r; } } 
Si eliminamos 
const , estas constantes se convierten en variables 
static . No entiendo por qué alguien a quien no le importa 
const hacer que estas variables sean 
static . Si eliminamos tanto la 
static como la 
const , GCC nuevamente las considerará constantes y obtendremos el mismo resultado. Debido a estas variables 
static const , los cambios en tres funciones de trece resultaron ser falsas, pero no las solucioné.
SQLite usa muchas variables globales, y la mayoría de las optimizaciones 
const verdaderas están conectadas con esto: como reemplazar una comparación con una variable con una comparación con una constante o retroceder parcialmente el ciclo un paso (para comprender qué tipo de optimizaciones se hicieron, usé 
Radare ). No vale la pena mencionar algunos cambios. 
SQLite3ParseUri() contiene 487 instrucciones, pero 
const realizó solo un cambio: tomó estas dos comparaciones:
 test %al, %al je <SQLite3ParseUri+0x717> cmp $0x23, %al je <SQLite3ParseUri+0x717> 
Y cambiado:
 cmp $0x23, %al je <SQLite3ParseUri+0x717> test %al, %al je <SQLite3ParseUri+0x717> 
Puntos de referencia
SQLite viene con una prueba de regresión para medir el rendimiento, y lo ejecuté cientos de veces para cada versión del código usando la configuración de compilación estándar de SQLite. Tiempo de ejecución en segundos:
Personalmente, no veo mucha diferencia. Eliminé 
const de todo el programa, así que si había una diferencia notable, entonces era fácil notarlo. Sin embargo, si el rendimiento es extremadamente importante para usted, incluso una pequeña aceleración puede complacerlo. Hagamos un análisis estadístico.
Me gusta usar la prueba U de Mann-Whitney para tales tareas. Es similar a la prueba t más conocida, diseñada para determinar las diferencias en los grupos, pero es más resistente a las variaciones aleatorias complejas que ocurren al medir el tiempo en las computadoras (debido a cambios de contexto impredecibles, errores en páginas de memoria, etc.). Aquí está el resultado:
La prueba U encontró una diferencia estadísticamente significativa en el rendimiento. Pero, ¡una sorpresa! - La versión sin 
const resultó ser más rápida, en unos 60 ms, es decir, en un 0,5%. Parece que el pequeño número de "optimizaciones" realizadas no valió el aumento en la cantidad de código. Es poco probable que 
const activado alguna optimización importante, como la vectorización automática. Por supuesto, su kilometraje puede depender de varios indicadores en el compilador, o en su versión, o en la base del código, o en otra cosa. Pero me parece honesto decir que incluso si 
const mejora el rendimiento de C, no me di cuenta de esto.
Entonces, ¿para qué se necesita const?
Para todos sus defectos, 
const en C / C ++ es útil para proporcionar seguridad de tipo. En particular, si usa 
const en combinación con semántica de movimiento y 
std::unique_pointer , puede implementar la propiedad explícita del puntero. La incertidumbre sobre la propiedad del puntero fue un gran problema en las bases de código C ++ más antiguas de más de 100 KLOC, por lo que estoy agradecido de poder 
const para resolverlo.
Sin embargo, antes de ir más allá de usar 
const para proporcionar seguridad de tipo. Escuché que se considera correcto usar 
const tan activamente como sea posible para mejorar el rendimiento. Escuché que si el rendimiento es realmente importante, entonces debes refactorizar el código para agregar más 
const , incluso si el código se vuelve menos legible. Parecía razonable en ese momento, pero desde entonces me di cuenta de que esto no era cierto.