El compilador de C # en sí no altera mucho la IL emitida en la versión de lanzamiento. Es notable que ya no emite los códigos de operación NOP que le permiten establecer un punto de interrupción en una llave rizada. El más grande es el optimizador integrado en el compilador JIT. Sé que hace las siguientes optimizaciones:
Método en línea. Una llamada al método se reemplaza por la inyección del código del método. Este es uno grande, hace que los accesos de propiedad sean esencialmente gratuitos.
Asignación de registro de CPU. Las variables locales y los argumentos del método pueden permanecer almacenados en un registro de CPU sin volver a almacenarse (o con menos frecuencia) en el marco de la pila. Este es uno grande, notable por hacer que la depuración de código optimizado sea tan difícil. Y dando un significado a la palabra clave volátil .
Eliminación de comprobación de índice de matriz. Una optimización importante cuando se trabaja con matrices (todas las clases de colección .NET usan una matriz internamente). Cuando el compilador JIT puede verificar que un bucle nunca indexa una matriz fuera de los límites, eliminará la verificación del índice. Uno grande.
Lazo se desenrolla. Los bucles con cuerpos pequeños se mejoran repitiendo el código hasta 4 veces en el cuerpo y haciendo bucles menos. Reduce el costo de sucursal y mejora las opciones de ejecución súper escalar del procesador.
Eliminación de código muerto. Una declaración como if (false) {/ ... /} se elimina por completo. Esto puede ocurrir debido al constante plegado y alineado. En otros casos, el compilador JIT puede determinar que el código no tiene ningún efecto secundario posible. Esta optimización es lo que hace que el código de perfil sea tan complicado.
Código de elevación. El código dentro de un bucle que no se ve afectado por el bucle se puede mover fuera del bucle. El optimizador de un compilador de C pasará mucho más tiempo buscando oportunidades para izar. Sin embargo, es una optimización costosa debido al análisis de flujo de datos requerido y el jitter no puede permitirse el tiempo, por lo que solo levanta casos obvios. Obligar a los programadores de .NET a escribir un mejor código fuente y izar ellos mismos.
Eliminación de subexpresión común. x = y + 4; z = y + 4; se convierte en z = x; Bastante común en declaraciones como dest [ix + 1] = src [ix + 1]; escrito para facilitar la lectura sin introducir una variable auxiliar. No es necesario comprometer la legibilidad.
Plegado constante. x = 1 + 2; se convierte en x = 3; Este simple ejemplo es captado temprano por el compilador, pero sucede en el momento JIT cuando otras optimizaciones lo hacen posible.
Copia de propagación. x = a; y = x; se convierte en y = a; Esto ayuda al asignador de registros a tomar mejores decisiones. Es un gran problema en el jitter x86 porque tiene pocos registros para trabajar. Hacer que seleccione los correctos es fundamental para el rendimiento.
Estas son optimizaciones muy importantes que pueden marcar una gran diferencia cuando, por ejemplo, perfila la compilación de depuración de su aplicación y la compara con la compilación de lanzamiento. Sin embargo, eso realmente solo importa cuando el código está en su ruta crítica, el 5 al 10% del código que escribe que realmente afecta el rendimiento de su programa. El optimizador JIT no es lo suficientemente inteligente como para saber por adelantado lo que es crítico, solo puede aplicar el dial "convertirlo en once" para todo el código.
El resultado efectivo de estas optimizaciones en el tiempo de ejecución de su programa a menudo se ve afectado por el código que se ejecuta en otro lugar. Lectura de un archivo, ejecución de una consulta dbase, etc. Haciendo que el trabajo del optimizador JIT sea completamente invisible. Aunque no le importa :)
El optimizador JIT es un código bastante confiable, principalmente porque se ha puesto a prueba millones de veces. Es extremadamente raro tener problemas en la versión de compilación de lanzamiento de su programa. Sin embargo, sucede. Tanto las fluctuaciones x64 como las x86 han tenido problemas con las estructuras. La fluctuación de fase x86 tiene problemas con la consistencia de coma flotante, produciendo resultados sutilmente diferentes cuando los intermedios de un cálculo de coma flotante se mantienen en un registro FPU con una precisión de 80 bits en lugar de truncarse cuando se vacía en la memoria.