Por qué C# puede ser tan rápido como C o C++?


Muchas veces encuentro algo de desinformación entre las personas dando por sentado que un programa en C o C++ es enormemente mas rápido que un programa compilado para la plataforma .NET, ¿por qué algo como esto no es del todo cierto?

Aquí nombrare algunos de los factores mas importantes:

El compilador y el ciclo desde la codificación hasta la ejecución

C# es un lenguaje de alto nivel estandarizado bajo la norma ECMA-334 el cual es compilado por varias herramientas, los compiladores mas populares son:

  • El compilador incluido en Mono Runtime de Novell
  • El compilador incluido con .NET Framework Runtime de Microsoft

Ambos de estos compiladores parsean el código fuente entrante en C# y generan archivos de código ejecutable intermedio siguiendo los lineamientos de CIL como se describen en el documento ECMA-335, CIL comprende un lenguaje ensamblador orientado a objetos para una maquina virtual abstraída del modelo de computo del hardware con el fin de servir de puente para la representación de los programas en una forma generalizada, como consecuencia el juego de instrucciones de CIL no coincide con el juego de instrucciones de nuestras x86 ni AMD64.

Cuando un programa es ejecutado sobre la plataforma .NET el sistema de tiempo de ejecución realiza un paso de compilación adicional que traduce el código en CIL al código especifico de la maquina anfitriona usando técnicas JIT, es entonces cuando el programa es ejecutado.

Esta ultima compilación es el paso mas importante para comprender si puede o no existir degradación en el desempeño cuando se usa la plataforma .NET por tres razones:

  • La compilación JIT (Just-In-Time) genera código final usando el juego de instrucciones nativo de la maquina anfitriona (puede generar para x86 y para AMD64) durante la marcha (con el fin de compensar latencia de arranque y consumo de memoria) incluyendo las optimizaciones a las que halla lugar, sin embargo este código compilado es cacheado y el proceso solo es necesario realizarlo una vez.
  • El desarrollador tiene la opción incluir la realización de este paso durante la instalación en la maquina anfitriona. (con NGEN.EXE si usa la implementación de Microsoft; con la opción AOT si usa la implementación de Novell)
  • El desarrollador tiene la opción de realizar este paso durante la construcción (Ahead of Time). Es esta característica la que ha hecho posible la ejecución de programas escritos para .NET en las plataformas iOS (iPad, iPod)

Ya que finalmente código fuente tanto en C, C++ y C# terminará siendo traducido a código en el conjunto de instrucciones nativo de la arquitectura del anfitrión es muy probable que su tiempo de ejecución sea similar.

La gestión de la memoria

Dado que la maquina para la cual se construyen los programas en CIL es una maquina abstraída del hardware real (Virtual Execution System abreviada VES), es importante hacer la salvedad de que su modelo de memoria conserva los elementos mas populares de nuestros modelos de computo real que son: considerar un espacio de almacenamiento de propósito general como un arreglo de bytes direccionable (homologo a la RAM), y el concepto de pila (homologo a la RAM usada como Pila). Estos elementos permiten que para el programador el concepto de la memoria se conserve pese a que el concepto del VES no es totalmente equivalente al de la maquina anfitriona.

En C# la memoria es gestionada automáticamente, liberando al programador de la tarea de destrucción de las instancias de objetos. Pese a la opinión casi generalizada de que la gestión automática de memoria es un hueco para el desempeño, la invariante tras los algoritmos de recolección de basura conservan la esencia de que un objeto debe ser destruido una vez es inalcanzable por el código fuente (lo que en otras palabras es cuando no lo usarás mas). En la implementación de .NET el algoritmo de recolección de basura (Garbage Collection) es un hibrido basado en el algoritmo generacional el cual realiza un full-copy eventual con el fin de mejorar la estabilidad del sistema al intentar eliminar la fragmentación en las concesiones de memoria, este hecho no es posible en los lenguajes como C o C++ ya que las asignaciones de memoria se mantienen fijas en los valores de los punteros devueltos por las funciones de alojamiento (malloc(), operador new()); pese a que un full-copy va en detrimento de la velocidad de ejecución, en la práctica la frecuencia con la que ocurre es tan baja que su impacto se mantiene imperceptible.

Reflexión, Dominio y Algoritmia – Cuando .NET es verdaderamente lento?

La utilización intensiva o incorrecta de algunas características incluidas en la plataforma .NET le impiden desempeñarse adecuadamente, por ejemplo la funcionalidad de reflexión (introspección en otros lenguajes) es conocida por atraer lentitud a los programas que abusan de ella, esto es debido a que esta característica usualmente debe a travesar bloques de código que deben ser bien chequeados para no permitir acceso a secciones de memoria no autorizadas (Cuando la aplicación viola las reglas del dominio de la aplicación – AppDomain, de demanda de permisos o de Zona de confianza). Sin embargo el problema del bajo rendimiento en la reflexión es ampliamente solventado usando bibliotecas como Cecil que le permiten la misma funcionalidad sin incurrir en el coste del chequeo de seguridad.

El amplio computo de propósito general esta lejos de los escenarios aquí nombrados por lo que finalmente se mantiene la premisa de que la complejidad temporal de un programa es en comparativamente mas dependiente de su algoritmia que de la tecnología con la que se implementa.

Conclusión

Un programa no es de facto mas rápido solo por estar escrito en un lenguaje como C o C++, antes de hacer una afirmación como esa puede ser necesario tener en cuenta detalles sobre el proceso y las tecnologías de construcción y el tipo de funcionalidad.

Mucho de lo aquí dicho vale también para otros lenguajes basados en maquina virtual como el caso de Java. Incluso algunos entornos de ejecución Java pueden acelerar la ejecución de código en el hardware enviando bytecode directamente al procesador usando una tecnología llamada Jazelle disponible en muchos procesadores ARM (especialmente en teléfonos celulares).

Deja un comentario