Diferencias de rendimiento entre las versiones de depuración y lanzamiento
Debo admitir que normalmente no me he molestado en cambiar entre las configuraciones de depuración y versión en mi programa, y normalmente he optado por la configuración de depuración , incluso cuando los programas están realmente implementados en el lugar del cliente.
Hasta donde yo sé, la única diferencia entre estas configuraciones si no las cambia manualmente es que Debug tiene la DEBUG
constante definida y Release tiene marcado el código de Optimización .
Entonces mi pregunta es en realidad doble:
¿Hay muchas diferencias de rendimiento entre estas dos configuraciones? ¿Existe algún tipo específico de código que cause grandes diferencias en el rendimiento aquí, o en realidad no es tan importante?
¿Hay algún tipo de código que se ejecute bien en la configuración de depuración que pueda fallar en la configuración de versión , o puede estar seguro de que el código que se prueba y funciona bien en la configuración de depuración también funcionará bien en la configuración de versión?
El compilador de C# en sí no altera mucho el IL emitido en la compilación de lanzamiento. Lo notable es que ya no emite los códigos de operación NOP que le permiten establecer un punto de interrupción en una llave. El más importante es el optimizador integrado en el compilador JIT. Sé que hace las siguientes optimizaciones:
Método en línea. Una llamada a un método se reemplaza por la inyección del código del método. Este es importante, hace que los accesores a la propiedad sean esencialmente gratuitos.
Asignación de registros de CPU. Las variables locales y los argumentos de los métodos pueden permanecer almacenados en un registro de la CPU sin volver a almacenarse (o con menor frecuencia) en el marco de la pila. Este es un problema importante, que se destaca por dificultar la depuración de código optimizado. Y darle un significado a la palabra clave volátil .
Eliminación de verificació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 pueda verificar que un bucle nunca indexa una matriz fuera de límites, eliminará la verificación del índice. Uno grande.
Desenrollado del bucle. Los bucles con cuerpos pequeños se mejoran repitiendo el código hasta 4 veces en el cuerpo y haciendo menos bucles. Reduce el costo de la sucursal y mejora las opciones de ejecución superescalar 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 alineación. En otros casos, el compilador JIT puede determinar que el código no tiene posibles efectos secundarios. Esta optimización es lo que hace que el código de creación de perfiles sea tan complicado.
Elevación de códigos. El código dentro de un bucle que no se ve afectado por el bucle se puede sacar del bucle. El optimizador de un compilador de C dedicará mucho más tiempo a encontrar oportunidades para aprovechar. Sin embargo, es una optimización costosa debido al análisis de flujo de datos requerido y el jitter no permite tomar el tiempo, por lo que sólo plantea casos obvios. Obligar a los programadores de .NET a escribir un mejor código fuente y elevarse a sí mismos.
Eliminación de subexpresiones comunes. 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; El compilador detecta este ejemplo simple temprano, pero ocurre en el momento JIT cuando otras optimizaciones lo hacen posible.
Copiar propagación. x = un; 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 con los que 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, perfilas la compilación de depuración de tu aplicación y la comparas con la compilación de lanzamiento. Sin embargo, eso solo importa cuando el código está en su ruta crítica, el 5 al 10% del código que escribe realmente afecta el rendimiento de su programa. El optimizador JIT no es lo suficientemente inteligente como para saber de antemano qué es crítico, solo puede aplicar el dial "girarlo a 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. Leer un archivo, ejecutar una consulta de base de datos, etc. Hacer que el trabajo que realiza el optimizador JIT sea completamente invisible. Aunque no importa :)
El optimizador JIT es un código bastante confiable, principalmente porque ha sido puesto a prueba millones de veces. Es extremadamente raro tener problemas en la versión de lanzamiento de su programa. Sin embargo, sucede. Tanto el jitter x64 como el x86 han tenido problemas con las estructuras. El jitter x86 tiene problemas con la consistencia del punto flotante, produciendo resultados sutilmente diferentes cuando los intermedios de un cálculo de punto flotante se mantienen en un registro FPU con una precisión de 80 bits en lugar de truncarse cuando se vacían en la memoria.
Sí, existen muchas diferencias de rendimiento y estas realmente se aplican en todo el código. La depuración optimiza muy poco el rendimiento y el modo de lanzamiento mucho;
Solo el código que depende de la
DEBUG
constante puede funcionar de manera diferente con una versión de lanzamiento. Además de eso, no deberías ver ningún problema.
Un ejemplo de código marco que depende de la DEBUG
constante es el Debug.Assert()
método, que tiene el atributo [Conditional("DEBUG)"]
definido. Esto significa que también depende de la DEBUG
constante y esto no está incluido en la versión de lanzamiento.
Esto depende en gran medida de la naturaleza de su aplicación. Si su aplicación tiene mucha interfaz de usuario, probablemente no notará ninguna diferencia ya que el componente más lento conectado a una computadora moderna es el usuario. Si utiliza algunas animaciones de la interfaz de usuario, es posible que desee probar si puede percibir algún retraso notable al ejecutar la compilación DEBUG.
Sin embargo, si tiene muchos cálculos con muchos cálculos, entonces notará diferencias (podrían llegar hasta el 40% como mencionó @Pieter, aunque dependería de la naturaleza de los cálculos).
Es básicamente una compensación de diseño. Si está lanzando bajo la versión DEBUG, entonces, si los usuarios experimentan problemas, podrá obtener un rastreo más significativo y podrá realizar un diagnóstico mucho más flexible. Al publicar en la compilación DEBUG, también evita que el optimizador produzca Heisenbugs oscuros .
Mi experiencia ha sido que las aplicaciones de tamaño mediano o grande responden notablemente mejor en una versión de lanzamiento. Pruébelo con su aplicación y vea cómo se siente.
Una cosa que puede molestarle con las compilaciones de lanzamiento es que el código de compilación de depuración a veces puede suprimir las condiciones de carrera y otros errores relacionados con los subprocesos. El código optimizado puede dar como resultado el reordenamiento de las instrucciones y una ejecución más rápida puede exacerbar ciertas condiciones de carrera.
Nunca debe lanzar una compilación de depuración .NET a producción. Puede contener código desagradable para admitir Editar y continuar o quién sabe qué más. Hasta donde yo sé, esto sucede solo en VB, no en C# (nota: la publicación original está etiquetada como C#) , pero aún así debería dar motivos para hacer una pausa sobre lo que Microsoft cree que pueden hacer con una compilación de depuración. De hecho, antes de .NET 4.0, el código VB pierde memoria en proporción a la cantidad de instancias de objetos con eventos que se construyen para admitir Editar y continuar. (Aunque se informa que esto se solucionó según https://connect.microsoft.com/VisualStudio/feedback/details/481671/vb-classes-with-events-are-not-garbage-collected-when-debugging , el código generado se ve desagradable, crear WeakReference
objetos y agregarlos a una lista estática mientras se mantiene un candado ) ¡Ciertamente no quiero ningún tipo de soporte de depuración de este tipo en un entorno de producción!