Alguns meses atrás, mencionei em um post que 
isso é um mito, como se o const ajude a habilitar otimizações do compilador em C e C ++ . Decidi que essa afirmação deveria ser explicada, principalmente porque eu mesmo acreditava nesse mito antes. Vou começar com a teoria e exemplos artificiais, e depois passar para experimentos e benchmarks baseados em uma base de código real - SQLite.
Teste simples
Vamos começar com, como me pareceu, o exemplo mais simples e óbvio de acelerar o código C com 
const . Digamos que temos duas declarações de função:
 void func(int *x); void constFunc(const int *x); 
E suponha que haja duas versões do 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 executar 
printf() , o processador deve recuperar 
*x da memória através de um ponteiro. Obviamente, a execução de 
constByArg() pode ser um pouco mais rápida, porque o compilador sabe que 
*x é uma constante; portanto, não há necessidade de carregar seu valor novamente após 
constFunc() . Certo? Vamos ver o código do assembler gerado pelo GCC com as otimizações ativadas:
 $ gcc -S -Wall -O3 test.c $ view test.s 
E aqui está o resultado completo do assembler 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 
A única diferença entre o código do assembler gerado por 
byArg() e 
constByArg() é que 
constByArg() possui uma 
call constFunc@PLT , como no código-fonte. 
const próprio 
const não faz diferença.
Ok, isso foi o GCC. Talvez precisemos de um compilador mais inteligente. Diga Clang.
 $ clang -S -Wall -O3 -emit-llvm test.c $ view test.ll 
Aqui está o código intermediário. É mais compacto que o assembler, e vou abandonar as duas funções, para que você entenda o que quero dizer com "nenhuma diferença, exceto a chamada":
 ; 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 } 
Opção que (tipo) funciona
E aqui está o código no qual a presença 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;  
O código do assembler para 
localVar() , que contém duas instruções otimizadas fora 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 
O middleware LLVM é um pouco mais limpo. 
load antes da segunda chamada para 
printf() foi otimizado fora 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 } 
Portanto, 
constLocalVar() ignorou com êxito a reinicialização 
*x , mas você pode notar algo estranho: nos corpos 
localVar() e 
constLocalVar() a mesma chamada para 
constFunc() . Se o compilador pode descobrir que 
constFunc() não modificou 
*x em 
constLocalVar() , por que ele não entende que a mesma chamada de função não modificou 
*x em 
localVar() ?
A explicação é por que 
const em C é impraticável para usar como otimização. Em C, 
const tem essencialmente dois significados possíveis:
- isso pode significar que uma variável é um pseudônimo somente leitura para alguns dados, que podem ou não ser constantes.
 
- ou pode significar que a variável é realmente uma constante. Se você desamarrar constde um ponteiro para um valor constante e depois escrever nele, obterá um comportamento indefinido. Por outro lado, não haverá problema seconstfor um ponteiro para um valor que não é uma constante.
 
Aqui está um exemplo explicativo de implementação de 
constFunc() :
 
localVar() forneceu 
constFunc() um ponteiro 
const para uma variável não 
const . Como a variável não era 
const inicialmente, 
constFunc() pode se tornar um mentiroso e modificar a variável com força sem iniciar o UB. Portanto, o compilador não pode assumir que, após retornar 
constFunc() variável terá o mesmo valor. A variável 
constLocalVar() realmente é 
const ; portanto, o compilador não pode assumir que não será alterado, pois desta vez 
será UB para 
constFunc() , para que o compilador desvincule 
const e 
constFunc() a variável.
As 
byArg() e 
constByArg() do primeiro exemplo são inúteis, porque o compilador não pode 
constByArg() se 
*x é 
const .
Mas de onde veio a inconsistência? Se o compilador puder assumir que 
constFunc() não altera seu argumento quando chamado de 
constLocalVar() , ele pode aplicar as mesmas otimizações às chamadas 
constFunc() , certo? Não. O compilador não pode assumir que 
constLocalVar() jamais será chamado. E se isso não acontecer (por exemplo, porque é apenas um resultado adicional do gerador de código ou da operação de macro), o 
constFunc() poderá alterar silenciosamente os dados sem iniciar o UB.
Você pode precisar ler os exemplos e explicações acima várias vezes. Não se preocupe, isso soa absurdo - é. Infelizmente, gravar em variáveis 
const é o pior tipo de UB: na maioria das vezes, o compilador nem sabe se será UB. Portanto, quando o compilador vê 
const , deve proceder do fato de que alguém pode alterá-lo em algum lugar, o que significa que o compilador não pode usar 
const para otimização. Na prática, isso é verdade, porque muitos códigos C reais contêm uma rejeição de 
const no estilo de "eu sei o que estou fazendo".
Em resumo, existem muitas situações em que o compilador não pode usar 
const para otimização, incluindo a recuperação de dados de outro escopo usando um ponteiro ou a colocação de dados em um heap. Ou pior ainda, geralmente em situações em que o compilador não pode usar 
const , isso não é necessário. Por exemplo, qualquer compilador que se preze pode entender sem 
const que neste código 
x é uma constante:
 int x = 42, y = 0; printf("%d %d\n", x, y); y += x; printf("%d %d\n", x, y); 
Portanto, 
const quase inútil para otimização, porque:
- Com algumas exceções, o compilador é forçado a ignorá-lo, pois algum código pode desatar legalmente a const.
 
- Na maioria das exceções acima, o compilador ainda pode entender que a variável é uma constante.
 
C ++
Se você escreve em C ++, 
const pode afetar a geração de código através da sobrecarga de funções. Você pode ter sobrecargas 
const e non- 
const da mesma função e o non- 
const pode ser otimizado (por um programador, não por um compilador), por exemplo, para copiar menos.
 void foo(int *p) {  
Por um lado, não acho que, na prática, isso seja frequentemente aplicado no código C ++. Por outro lado, para que realmente faça a diferença, um programador deve fazer suposições que não estão disponíveis para o compilador, pois elas não são garantidas pelo idioma.
Experimente o SQLite3
Teoria suficiente e exemplos absurdos. Que efeito o 
const na base de código real? Decidi experimentar o SQLite DB (versão 3.30.0), porque:
- Ele usa const.
- Esta é uma base de código não trivial (mais de 200 KLOC).
 
- Como banco de dados, ele inclui vários mecanismos, começando com o processamento de valores de sequência e terminando com a conversão de números para a data.
 
- Pode ser testado com uma carga limitada do processador.
 
Além disso, o autor e os programadores envolvidos no desenvolvimento já passaram anos melhorando a produtividade, para que possamos assumir que eles não perderam nada óbvio.
Preparação
Fiz duas cópias do 
código fonte . Um compilado no modo normal e o segundo pré-processado usando um hack para transformar 
const em um comando ocioso:
 #define const 
(GNU) 
sed pode adicionar isso no topo de cada arquivo com o comando 
sed -i '1i#define const' *.c *.h .
O SQLite complica um pouco as coisas, usando scripts para gerar código durante a compilação. Felizmente, os compiladores apresentam muito ruído ao misturar código com 
const e sem 
const , para que você possa perceber e configurar imediatamente os scripts para adicionar meu código anti- 
const .
A comparação direta dos códigos compilados não faz sentido, pois uma pequena alteração pode afetar todo o esquema de memória, o que levará a uma alteração nos ponteiros e nas chamadas de função no código inteiro. Portanto, tomei uma 
objdump -d libSQLite3.so.0.8.6 desmontada ( 
objdump -d libSQLite3.so.0.8.6 ) como o tamanho do nome binário e mnemônico de cada instrução. Por exemplo, esta função:
 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 transforma em:
 SQLite3_blob_read 7lea 5jmpq 4nopl 
Ao compilar, não alterei as configurações de montagem do SQLite.
Análise de código compilada
Para libSQLite3.so, a versão com 
const ocupava 4.740.704 bytes, aproximadamente 0,1% a mais que a versão sem 
const com 4.736.712 bytes. Nos dois casos, 1374 funções foram exportadas (sem contar as funções auxiliares de baixo nível no PLT) e 13 apresentaram diferenças nas projeções.
Algumas mudanças foram relacionadas ao hack de pré-processamento. Por exemplo, aqui está uma das funções alteradas (removi algumas definições específicas do 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; } } 
Se removermos 
const , essas constantes se transformarão em variáveis 
static . Não entendo por que alguém que não se importa com 
const tornar essas variáveis 
static . Se removermos 
static e 
const , o GCC considerará novamente como constantes e obteremos o mesmo resultado. Devido a essas variáveis 
static const , alterações em três funções em treze resultaram falsas, mas eu não as corrigi.
O SQLite usa muitas variáveis globais, e a maioria das verdadeiras otimizações de 
const está conectada a isso: como substituir uma comparação por uma variável por uma constante ou reverter parcialmente o loop em uma etapa (para entender que tipo de otimização foi feita, usei o 
Radare ). Algumas mudanças não merecem menção. 
SQLite3ParseUri() contém 487 instruções, mas 
const fez apenas uma alteração: fez essas duas comparações:
 test %al, %al je <SQLite3ParseUri+0x717> cmp $0x23, %al je <SQLite3ParseUri+0x717> 
E trocou:
 cmp $0x23, %al je <SQLite3ParseUri+0x717> test %al, %al je <SQLite3ParseUri+0x717> 
Benchmarks
O SQLite vem com um teste de regressão para medir o desempenho, e eu o executei centenas de vezes para cada versão do código usando as configurações de compilação padrão do SQLite. Tempo de execução em segundos:
Pessoalmente, não vejo muita diferença. Eu removi o 
const de todo o programa; portanto, se havia uma diferença notável, era fácil perceber. No entanto, se o desempenho é extremamente importante para você, mesmo pequenas acelerações podem agradá-lo. Vamos fazer uma análise estatística.
Eu gosto de usar o teste Mann-Whitney U para essas tarefas.É semelhante ao teste t mais conhecido, projetado para determinar diferenças em grupos, mas é mais resistente a variações aleatórias complexas que ocorrem ao medir o tempo em computadores (devido a mudanças imprevisíveis de contexto, erros em páginas de memória, etc.). Aqui está o resultado:
O teste U encontrou uma diferença estatisticamente significativa no desempenho. Mas - uma surpresa! - A versão sem 
const acabou sendo mais rápida, em cerca de 60 ms, ou seja, em 0,5%. Parece que o pequeno número de “otimizações” feitas não valeu o aumento na quantidade de código. É improvável que o 
const ativado grandes otimizações, como a auto-vetorização. Obviamente, sua milhagem pode depender de vários sinalizadores no compilador, ou em sua versão, ou na base de código ou em outra coisa. Mas me parece honesto dizer que, mesmo que o 
const melhorado o desempenho de C, eu não percebi isso.
Então, para que const é necessário?
Por todas as suas falhas, a 
const em C / C ++ é útil para fornecer segurança de tipo. Em particular, se você usar 
const em combinação com mover semântica e 
std::unique_pointer , poderá implementar a propriedade explícita do ponteiro. A incerteza sobre a propriedade do ponteiro foi um grande problema nas bases de código C ++ mais antigas, com mais de 100 KLOC, por isso sou grato a 
const por resolvê-lo.
No entanto, antes de ir além do uso de 
const para fornecer segurança de tipo. Ouvi dizer que era considerado correto usar o 
const mais ativamente possível para melhorar o desempenho. Ouvi dizer que se o desempenho é realmente importante, é necessário refatorar o código para adicionar mais 
const , mesmo que o código se torne menos legível. Parecia razoável na época, mas desde então percebi que isso não era verdade.