O próprio compilador C # não altera muito a IL emitida na compilação Release. Notável é que ele não emite mais os códigos de operação NOP que permitem definir um ponto de interrupção em uma chave. O grande problema é o otimizador incorporado ao compilador JIT. Eu sei que ele faz as seguintes otimizações:
Método inlining. Uma chamada de método é substituída pela injeção do código do método. Esse é um grande problema, torna os acessadores de propriedades essencialmente gratuitos.
Alocação de registro da CPU. Variáveis locais e argumentos de método podem permanecer armazenados em um registro da CPU sem nunca (ou com menos frequência) serem armazenados de volta no quadro da pilha. Esse é um grande exemplo, por tornar tão difícil o código otimizado para depuração. E dando um significado à palavra-chave volátil .
Eliminação de verificação de índice de matriz. Uma otimização importante ao trabalhar com matrizes (todas as classes de coleção do .NET usam uma matriz internamente). Quando o compilador JIT pode verificar se um loop nunca indexa uma matriz fora dos limites, ele elimina a verificação do índice. Um grande.
Loop desenrolando. Os loops com corpos pequenos são aprimorados repetindo o código até 4 vezes no corpo e repetindo menos. Reduz o custo da filial e melhora as opções de execução superescalares do processador.
Eliminação de código morto. Uma declaração como se (false) {/ ... /} é completamente eliminada. Isso pode ocorrer devido a dobramentos e inlining constantes. Outros casos é onde o compilador JIT pode determinar que o código não tem efeito colateral possível. Essa otimização é o que torna o código de criação de perfis tão complicado.
Código de elevação. O código dentro de um loop que não é afetado pelo loop pode ser movido para fora do loop. O otimizador de um compilador C gastará muito mais tempo encontrando oportunidades de içar. No entanto, é uma otimização cara devido à análise de fluxo de dados necessária e a instabilidade não pode permitir o tempo; portanto, apenas elimina casos óbvios. Forçando programadores .NET a escrever melhor código-fonte e a se elevar.
Eliminação de subexpressão comum. x = y + 4; z = y + 4; torna-se z = x; Bastante comum em declarações como dest [ix + 1] = src [ix + 1]; escrito para facilitar a leitura sem introduzir uma variável auxiliar. Não há necessidade de comprometer a legibilidade.
Dobragem constante. x = 1 + 2; torna-se x = 3; Este exemplo simples é capturado cedo pelo compilador, mas acontece no momento da JIT, quando outras otimizações tornam isso possível.
Copiar propagação. x = a; y = x; torna-se y = a; Isso ajuda o alocador de registros a tomar melhores decisões. É um grande problema no jitter x86, pois possui poucos registros para trabalhar. Tê-lo selecionado os corretos é fundamental para o desempenho.
Essas são otimizações muito importantes que podem fazer muita diferença quando, por exemplo, você cria um perfil da versão Debug do seu aplicativo e a compara à versão Release. Isso realmente importa apenas quando o código está no seu caminho crítico, os 5 a 10% do código que você escreve que realmente afetam o desempenho do seu programa. O otimizador de JIT não é inteligente o suficiente para saber de antemão o que é crítico; ele pode aplicar apenas o dial "turn on onze" para todo o código.
O resultado efetivo dessas otimizações no tempo de execução do seu programa geralmente é afetado pelo código executado em outro local. Lendo um arquivo, executando uma consulta dbase, etc. Tornando o trabalho o otimizador de JIT completamente invisível. Mas não se importa :)
O otimizador JIT é um código bastante confiável, principalmente porque foi testado milhões de vezes. É extremamente raro ter problemas na versão de compilação do seu programa. Isso acontece no entanto. Os tremores x64 e x86 tiveram problemas com estruturas. O jitter x86 tem problemas com a consistência do ponto flutuante, produzindo resultados sutilmente diferentes quando os intermediários de um cálculo de ponto flutuante são mantidos em um registro FPU com precisão de 80 bits, em vez de serem truncados quando descarregados na memória.