domingo, 7 de diciembre de 2008

Lenguaje intermedio en .net

La función de este tipo de compiladores es traducir el código que el programador escribe en cualquiera de los lenguajes soportados por el CLR a un Lenguaje Intermedio estándar (LI). Este lenguaje es un lenguaje independiente de la plataforma.

Todas las herramientas de desarrollo generan el mismo IL, independientemente del lenguaje en el que esté escrito el código fuente, con lo cual las diferencias de implementación desaparecen en el momento en que entra en funcionamiento el CLR. Para que no haya ningún problema en la interacción de lenguajes, los distintos tipos de lenguajes tienen que seguir unas pautas establecidas en él.

Cuando un compilador genera código intermedio, también genera a su vez metadatos. Los metadatos es información que el CLR va a utilizar a en tiempo de ejcución, como puede ser, por ejemplo, la definición de tipos que hay en el código. Estos metadatos, junto con el código intermedio se almacenan en un fichero PE (Portable Executable). Una de las ventajas de este tipo de ejecutable es que al almacenar los metadatos y el código intermedio juntos, el código se describe así mismo, con lo cual ya no es necesario, por ejemplo tener IDLs.

Una herramienta muy interesante para ver como están constituidos estos ficheros PE, es ildasm.exe que se instala junto con el Framework. Esta herramienta nos permite, entre otras cosas, ver como está formado este tipo de ficheros, ver qué código intermedio ha generado cada función escrita y muchas otras cosas interesantes. Podemos encontrar un tutorial dentro de la documentación del Framework.

La especificación de este IL es abierta, con lo cual cualquiera que lo desee puede construir su propio compilador a código intermedio compatible con el CLR.

Compilación JIT

Una vez que se ha realizado un programa y se ha compilado con los compiladores que ofrece la plataforma, el código resultante no se puede ejecutar directamente sobre la máquina, ya que lo que contienen es código intermedio. Para poder ser ejecutados, necesitan previamente pasar por un compilador JIT.

En primer lugar, aclarar lo que significa JIT. Estas siglas pertenecen a Just-In-Time, nombre que recibe este tipo de compilación debido a que se realiza en tiempo de ejecución.

Antes de que el código intermedio anteriormente generado sea ejecutado, debe ser convertido por el JIT en código nativo, esto es, código que pueda entender el computador. Este código es específico para cada arquitectura y lo que le proporciona a .NET independencia de la plataforma, con lo cual, nosotros escribimos el mismo código independientemente de la arquitectura en la que se vaya a ejecutar.

No obstante esto no es siempre necesario, ya que para cierto tipo de aplicaciones, puede resultar ineficiente y hay opciones que permiten que la compilación JIT sólo se realice una única vez cuando la aplicación es instalada en la máquina (compilación en tiempo de instalación) y el código nativo quede almacenado en el sistema, al igual que ocurre con las aplicaciones no pertenecientes a .NET. Esto se hace mediante la herramienta Ngen.exe, en línea de comandos y es el generador de imágenes nativas.

Una de las ventajas de la compilación just-in-time es que no compila todo nada más llamar al ejecutable, esto es, en vez de convertir todo el fichero PE (ver Compiladores de Lenguaje Intermedio) a código nativo, va convirtiendo el código intermedio que vaya necesitando, ahorrando así tiempo y recursos. Además, el código que ya se ha traducido, es almacenado para llamadas posteriores.

En la etapa de generación de código nativo a partir de código intermedio, también tiene lugar un proceso de verificación (siempre va a suceder a menos que el administrador del sistema indique lo contrario). La verificación comprueba el código intermedio y los metadatos para comprobar si el código puede ser considerado como “seguro” (accede únicamente a aquellas direcciones de memoria que puede acceder). No obstante, no todos los lenguajes soportan la generación de código seguro (por ejemplo C++ con los punteros). Si las políticas de sistema requieren la ejecución de código seguro, se lanzará una excepción a la hora de ejecutar dicho código.

Estructura de datos

Las Estructuras de Datos se pueden definir como la organización de la información que permite un determinado lenguaje de programación. Cada estructura posee sus propias características de almacenamiento y recuperación de los datos.
Los Algoritmos constituyen la resolución de problemas computacionales mediante un lenguaje de programación.

Una de las novedades realmente únicas en .NET es la unificación del sistema de tipos que resuelve de forma brillante muchos de los problemas que siempre se han encontrado en el desarrollo de software. El runtime llamado CLR (Common Lenguaje Runtime) que el .NET nos proporciona, presenta de entre sus muchas características el conjunto de tipos común estandarizado para todos los lenguajes soportados, es el llamado CTS (Common Type System).

Common Type System (CTS): Es el conjunto de reglas que han de seguir las definiciones de tipos de datos para que el CLR las acepte. Es decir, aunque cada lenguaje gestionado disponga de su propia sintaxis para definir tipos de datos, en el MSIL resultante de la compilación de sus códigos fuente se han de cumplir las reglas del CTS. Divide además los tipos de datos en dos categorías:

  • Tipos por valor: Son aquellos tipos que contienen los datos, el valor en si. Se almacenan directamente en el disco duro de la computadora y cuando trabajamos con ellos, trabajamos directamente con el valor. No requiere de memoria adicional aparte de la estrictamente necesaria para almacenar el valor. Estos tipos son implementados por el runtime y aumentan considerablemente la velocidad.
  • Tipos por referencia: Son aquellos que hacen referencia a una posición de memoria, están guardados en la pila. Son más lentos que los tipos por valor y ocupan algo más de memoria, a cambio obtienen varias ventajas como por ejemplo que pueden contener métodos, polimorfismo…

Los Algorítmos permiten resolver problemas computacionales mediante lenguajes de programación. Como Ejemplo podemos poner dos de los más usuales:

  • Divide y Vencerás: Consiste en descomponer un problema en subproblemas, resolver cada subproblema y combinar las soluciones. El resultado, es la solución del problema original. Si los subproblemas son todavía demasiado grandes, se utiliza la misma táctica con ellos, esto es, dividirlos a ellos también, utilizando un algoritmo recursivo que vaya dividiendo más el sub-problema hasta que su solución sea trivial
  • Backtracking: El Backtracking o esquema de vuelta atrás, es un esquema que de forma sistemática y organizada, genera y recorre un espacio que contiene todas las posibles secuencias de decisiones. Este espacio se denomina el espacio de búsqueda del problema, y se representa como un árbol sobre el que el algoritmo hace un recorrido en profundidad partiendo de la raíz. Se conoce de antemano el orden en que se van a generar y recorrer sus nodos, y se continúa recorriendo el árbol mientras se cumplan las restricciones. Éste método tiene tres posibles esquemas: encontrar una solución factible, encontrar todas las soluciones factibles, encontrar la mejor solución factible.

No hay comentarios: