Cuando estamos trabajando con la plataforma de .NET tenemos una gran variedad de opciones en cuanto a lenguajes que podemos utilizar para crear nuestras aplicaciones, entre estos están C#, F# y Visual Basic que son los lenguajes más conocidos de la plataforma, aunque no los únicos, ya que además puedes utilizar, por ejemplo, C++, todo esto de manera interoperable, es decir, que puedes realizar tus programas en cualquiera de estos lenguajes y comunicar entre si partes del mismo escritas en los distintos lenguajes soportados por la plataforma.
Pero, te preguntaras ¿Cómo es todo esto posible? Es magia… O mas bien es la ingeniería por detrás de la plataforma de .NET que permite realizar programas utilizando diversidad de lenguajes en conjunto gracias a su entorno de ejecución mejor conocido como Common Language Runtime (CLR) y un lenguaje especial al cual se compilan nuestros programas hechos con .NET llamado Intermediate Language (IL).
Código Administrado y No Administrado
Antes de comenzar a conocer un poco mas sobre el CLR e IL, debemos conocer los conceptos de código administrado y no administrado, esto es importante de destacar ya que una de ambas es la forma en que se ejecutan nuestras aplicaciones en el sistema al cual han sido destinadas.
Código administrado
El código administrado o gestionado es como su nombre lo dice, cuando el código de nuestro programa, o mas bien los recursos que este utiliza son administrados por un componente adicional en su ejecución que, esto es, nuestro código ejecutándose bajo la supervisión de un entorno de ejecución o maquina virtual, que en el caso de .NET es el CLR la maquina virtual encargada de supervisar o administrar la ejecución de nuestro código y además gestionar el uso de los recursos que el sistema operativo brinda a nuestros programas, de lo cual se vera un poco mas a detalle cuando hablemos del CLR.
Esto significa que el código de la aplicación no se ejecuta directamente sobre el sistema operativo del dispositivo, sino que en vez de ello, se apoya de un ambiente de ejecución (runtime engine) para ser ejecutado, encargado de asignar recursos y servicios de soporte como seguridad, administración de la memoria, entre otros.
Este entorno de ejecución es conocido mas formalmente como maquina virtual de proceso o aplicación abreviado PVM (Process-level Virtual Machine) que se ejecuta como una aplicación cualquiera dentro del sistema y soporta un proceso individual e independiente dentro del sistema operativo permitiendo así que los lenguajes en los que fueron escritos nuestras aplicaciones se comuniquen con el sistema, siendo un intermediario entre el lenguaje y el código maquina.
Normalmente la PVM se crea cuando el proceso es iniciado y se detiene cuando finaliza el proceso, esto es importante, puesto que algunas optimizaciones que son realizadas solo persisten durante el ciclo de vida de dicho proceso.
Es importante mencionar el termino de PVM, ya que muchas veces asociamos al termino de maquina virtual solo a aquellas que permiten crear una capa adicional en nuestro sistema operativo para ejecutar sobre este otro sistema operativo virtualizado, esto ultimo se conoce como maquina virtual del sistema (System Virtual Machine) la cual su propósito es ejecutar un sistema operativo completo dentro del sistema operativo de nuestra maquina física o anfitriona.
Código no administrado
Es código que se ejecuta directamente en el sistema operativo, es decir, no existe ningún entorno de ejecución o maquina virtual intermedia para ser ejecutado, por lo tanto, es responsabilidad del desarrollador gestionar los recursos a los que acceden sus programas para que estos sean eficientes, aunque tienden a tener mayor rendimiento que aquellos con código administrado ya que esta capa intermedia no existe y por lo tanto el proceso de interpretar, compilar o transformar el código a código maquina durante el tiempo de ejecución no existe; los programas escritos en lenguajes que compilan directamente a código máquina como C o C++ serian ejemplos de código no gestionado aunque no significa que no podamos escribir código en C#, por ejemplo, no gestionado.
¿Cuál es la Mejor Opción?
Como vemos la diferencia principal entre ambos esta en sobre quien recae la responsabilidad de administrar los recursos del sistema como la memoria o los servicios que se requieran de este, en el caso del código administrado recae sobre el entorno de ejecución o maquina virtual correspondiente mientras que el código no administrado es responsabilidad del desarrollador cuidar estos aspectos.
La decisión sobre uno u otro dependerá del proyecto en cuestión, y, almenos cuando se desarrolla con .NET la mayoría de los casos de uso están cubiertos, por lo que lo mas común será trabajar con código administrado a través del CLR, permitiendo que la experiencia de desarrollo sea mucho mejor, mejorando la productividad y portabilidad de las aplicaciones.
Son pocos los casos en que es necesario salir fuera de lo convencional, aunque pueden existir como es el caso de los controladores de dispositivos (device drivers).
Si por la naturaleza del proyecto es imposible usar código administrado al cien por ciento, puede ser recomendable combinar ambos esquemas: se desarrollan como código no administrado los componentes que así lo requieran, y el resto se implementa como código administrado.
En cuanto a desempeño de la aplicación se refiere, los mejores resultados se pueden obtener usando código no administrado, sin embargo, con el paso del tiempo los lenguajes, herramientas disponibles y el hardware evolucionan lo que tiene como resultado que en muchas ocasiones las mejoras en rendimiento no serán notables, por lo que utilizar código administrado es prácticamente la opción predeterminada por el beneficio que esto aporta en cuanto a la experiencia de desarrollo que proporciona y la capacidad de ejecutar nuestras aplicaciones o programas en múltiples sistemas y arquitecturas, siempre y cuando el entorno de ejecución lo permita.
Common Language Runtime (CLR)
Su objetivo es el de proporcionar un entorno de ejecución independiente de la plataforma de hardware y del sistema operativo, que oculte los detalles de la plataforma subyacente y permita que un programa se ejecute siempre de la misma forma sobre cualquier plataforma. Esto es debido a que el código de máquina es específico al dispositivo siendo utilizado y programar a ese nivel requiere un conocimiento sobre el sistema operativo y la máquina. El CLR actúa como una máquina virtual, sirviendo de puente entre los lenguajes de alto nivel disponibles en .NET y el código de máquina que la computadora puede entender.
Si programamos en C#, el CLR, es a Java la JVM (Java Virtual Machine) salvando las diferencias entre ambas plataformas, siendo el ejemplo más conocido y cercano en lo que respecta, ya que su propósito es interpretar, compilar o traducir un código intermedio entre el lenguaje y código máquina.
De parte de Microsoft se refieren al CLR como un entorno de ejecución de código administrado (managed runtime environment) donde el cual los programas escritos en cualquiera de los lenguajes de la plataforma como Visual Basic, C# o F# después de ser compilados a Intermediate Language puedan ejecutarse en el CLR. Cuando tú escribes tu código en cualquiera de estos lenguajes estableciendo una versión del entorno a utilizar y lo compilas, lo que realmente estás haciendo es crear información descriptiva sobre el programa que es almacenada en el programa compilado como metadatos, estos metadatos están contenidos en archivos .exe y/o .dll más conocidos como ensamblados, los cuales se encontraran en lenguaje intermedio optimizado para su ejecución, toda esta información le dice al CLR el lenguaje en el cual fue escrito el programa, la versión, librerías de clases, referencias y todo aquello que el programa necesitará para su ejecución, lo que se conoce como aplicación autocontenida (self-contained application) teniendo todo lo necesario para su ejecución en cualquier máquina con el entorno de ejecución instalado.
De esta forma el CLR permite a tus programas que, por ejemplo, un objeto instanciado a partir de una clase escrita en un lenguaje pueda llamar al método de otra clase escrita en otro lenguaje.
Funciones del CLR
- Administración de memoria a través del Garbage Collector.
- Manejo de excepciones.
- Seguridad de tipos.
- Seguridad entre procesos.
- Administración del código en ejecución.
Componentes del CLR
- Compilador JIT (Just-In-Time Compilation).
- Class loader.
- Thread pool.
Intermediate Language (IL)
También conocido como Microsoft Intermediate Language (MSIL durante la beta de los lenguajes de .NET) o Common Intermediate Language (CIL) es el código generado a partir del proceso de compilación de nuestro programa escrito en el lenguaje de nuestra elección en la plataforma sin importar cual sea este.
El lenguaje intermedio es código que tiene un menor nivel de abstracción que los lenguajes como C# o F# (lenguajes de alto nivel) lo que lo hace un poco mas difícil de escribir, leer y manejar para los desarrolladores, pero es de mas alto nivel que el código máquina propiamente.
El lenguaje intermedio trabaja por medio de instrucciones que indican como posicionar o localizar cada uno de los elementos de nuestro programa manipulando así el stack o la pila de ejecución a través de estas instrucciones que indican la operación a realizar. Las instrucciones que componen un programa escrito en lenguaje intermedio son llamadas opcodes (abreviado de operation codes), estas instrucciones se encuentran en los metadatos generados al momento de compilar nuestro programa.
Lo que Microsoft busca a través del lenguaje intermedio es que tengamos la libertad de seleccionar cualquier lenguaje soportado por la plataforma de .NET y programar en este lenguaje sin preocuparnos de como este se ejecutara en cuestión, además de tener la capacidad de utilizar estos lenguajes en conjunto para crear nuestros programas. Esto lo consiguió creando el lenguaje intermedio siguiendo la especificación abierta y estándar técnico conocido como Common Language Infrastructure (CLI) permitiendo así que los programas escritos en cualquier lenguaje sean interoperables entre si y puedan ser ejecutados en distintos tipos de hardware; CLI a su ves incluye el Common Type System (CTS) y Common Language Specification (CLS).
Algunas implementaciones del CLI son .NET Framework, .NET (o .NET Core) y Mono.
Curiosidades sobre el CLR e IL
¿La compilación JIT realizada por el CLR se ejecuta por cada método, cada vez que se llama?
El código IL normalmente siempre es pasado por el compilador JIT cada vez que se ejecuta el programa, es decir, se compila en cada ejecución, pero cada pieza de código IL se compila una sola vez cuando es llamado y no vuelve a recompilarse nuevamente una vez ya se ha realizado esta única ocasión, por lo que no hay optimizaciones basadas en el uso o anteriores compilaciones.
Esto es, conforme los bits de los ensamblados son ejecutados el compilador JIT compila y cachea estos bits en memoria. De esta forma cada bit es solamente compilado una vez antes de su ejecución, luego se utiliza su versión nativa en memoria para su ejecución. Por esta razón la compilación JIT se realizara en cada ejecución del programa ya que como se almacena en memoria el resultado al finalizar el programa es que esta memoria es liberada de nuestro sistema operativo.
De manera simplificada, esto significa que la compilación ocurre una sola vez por método durante la ejecución del programa en la primera invocación de ese método y cuando el programa se detiene toda la compilación se pierde.
Es por esto que en muchos tutoriales, blogs o cursos habrás escuchado que mencionan que “al ejecutar la aplicación siempre tardara un poco mas en ejecutarse cuando llamas al método, porque aun no esta en memoria.”
¿Qué ocurre con los genéricos?
Los genéricos en .NET presentan un comportamiento particular respecto a la compilación JIT, ya que el CLR maneja su compilación de manera optimizada dependiendo del tipo con el que se instancien, es decir, si son tipos por valor (value types) o tipos de referencia (reference types) lo cual se explica a continuación:
-
Cuando un tipo genérico es instanciado con un tipo de valor (por ejemplo,
int
,float
,struct
, etc.), el CLR compila una versión específica del método genérico para cada tipo de valor con el que se instancie. Esto se debe a que los tipos de valor en .NET tienen un tamaño y representación diferentes en memoria, por lo que el código generado tiene que ser único para cada tipo de valor.- Por ejemplo, la clase genérica
List<T>
la usas conList<int>
yList<double>
, el CLR generará dos compilaciones diferentes, una paraint
y otra paradouble
.
Esto ocurre porque los tipos de valor son almacenados directamente en memoria, y la compilación específica optimiza cómo se manejan esos valores.
- Por ejemplo, la clase genérica
-
Cuando un tipo genérico es instanciado con un tipo de referencia (por ejemplo,
string
,object
, o cualquier clase definida por ti), el CLR comparte una única versión compilada para todos los tipos de referencia. Es decir, la compilación JIT se realiza una vez para todos los tipos de referencia, ya que estos tipos son siempre manejados por referencias (punteros o direcciones de memoria), y la representación en memoria es uniforme para todos los tipos de referencia.- Por ejemplo, si usas
List<string>
yList<object>
, el CLR compilará una sola versión para los tipos de referencia y la reutilizará para ambos casos.
- Por ejemplo, si usas
¿Cuándo se usa Reflection también se almacena en memoria el código compilado resultante mediante JIT?
En el caso de utilizar Reflection en .NET para invocar métodos sean genéricos o no, el código sí se compila mediante el JIT y se almacena en memoria:
- Cuando invocas un método mediante Reflection, el CLR aún tiene que compilar el método desde IL a código máquina usando el JIT. Esto ocurre de la misma manera que si el método hubiera sido invocado directamente en el código.
- Una vez que el método es compilado mediante el JIT, el código resultante se almacena en memoria para reutilizarlo en futuras invocaciones, incluso si el método fue invocado a través de Reflection.
El JIT compila el método la primera vez que es invocado y almacena el código nativo en memoria por lo que si vuelves a invocar ese mismo método con Reflection, el CLR reutiliza el código compilado, evitando recompilarlo.
Prácticamente lo que cambia es como invocas el método, que es estática o directa si llamas al método desde tu código de la forma convencional y dinámica o indirecta que es cuando utilizas Reflection escaneando los metadatos de los tipos para encontrar el método y ejecutarlo; para el CLR da igual la forma en que lo ejecutes, lo único que importa es que estas ejecutándolo y lo compilara a código nativo o maquina, sin embargo, ten en cuenta que usar Reflection afecta el rendimiento pero esto no tiene que ver con el CLR como tal sino mas bien con el uso de la inspección de tipos y el descubrimiento de los mismos en tiempo de ejecución mediante Reflection.
De esta forma una vez que el método es compilado a código nativo, tanto para invocaciones directas como para invocaciones con Reflection, el código compilado se guarda y se reutiliza. Esto significa que el proceso de compilación en sí no se repite, pero la parte dinámica de la invocación a través de Reflection (búsqueda e invocación) sigue siendo más lenta porque siempre se realizará.
Recursos adicionales
https://www.geeksforgeeks.org/common-language-runtime-clr-in-c-sharp/
https://en.wikipedia.org/wiki/Common_Intermediate_Language
https://www.artima.com/articles/clr-design-choices
https://snifftontechnologies.wordpress.com/2014/03/05/what-is-common-language-runtime-in-c/
Top comments (0)