DIVULGAAJARE: PROGRAMMA LEXIKON
La salida al mercado de TIS 100 me obliga a continuar con la olvidada serie de artículos DivulgaAjare, ésa que inicié para explicar porque Java es tan lento y que hoy verá cumplido su objetivo.
En un capítulo anterior expliqué el funcionamiento interno de un microprocesador, y como al final éstos realizan su tarea en base a datos e instrucciones, codificadas ambas cosas en 1’s y 0’s
Para escribir los primeros programas a nuestros “ancestros” no les quedaba mas remedio que introducir esos 1’s y 0’s manualmente, físicamente mediante palancas o de una forma más cómoda con tarjetas perforadas ( agujero=1, no agujero=0). Esto era un coñazo y muy susceptible a errores. Pero por mucho que hayamos avanzado desde entonces los errores han avanzado a igual o mayor velocidad que nosotros.
Para facilitar las cosas nació el lenguaje ensamblador, que calificaba con nombres reconocibles las instrucciones que anteriormente solo tenían asignado un código binario, por ejemplo: 0001 → Sumar → SUM. Así mismo se nombraron los registros internos de procesador, los únicos lugares donde realmente pueden manipularse los datos, por ejemplo RXA, RXB… así SUM RXA RXB suma el contenido de RXA con RXB y deja el resultado en RXA. (Si, el orden de los operandos suele ser <destino> <origen>)
El juego de instrucciones de los microprocesadores empezó siendo muy pequeño, limitándose a operaciones de Suma, Resta, Mover, Salto (ejecutar una instrucción distinta de la siguiente) y Salto condicional dependiendo si cierto registro contiene un número mayor, menor o igual a 0. Por lo que construir un programa era complejo, difícil de modificar y lento, por sencilla que fuera la tarea a resolver.
La traducción de un programa en ensamblador a los 1’s y 0’s que componen el código máquina entendible por el ordenador es directa y trivial, incluso aunque una misma instrucción tenga asignado diferente código binario entre los diversos microprocesadores.
El juego de instrucciones de Amigobot, por si quereis programar vuestra propia Roomba optimizandola para vuestro hogar.
Sin embargo cada microprocesador tiene un juego de instrucciones propio y ligeramente diferente del resto. También varían en cantidad y tamaño de los registros internos . Incluso la representación interna de los números cambiaba y en algunos procesadores se “leen de izquierda a derecha” y en otros de “derecha a izquierda”. Por lo que era muy probable que un programa en ensamblador que funcionase en uno no funcione en otro de distinta marca, o incluso entre diferentes modelos de una misma marca.
Eso sí, dado que los programas se escribían a mano específicamente para cierto procesador estos estaban MUY optimizados. Los genios que se dedicaban a ello utilizaban toda clase de trucos para sacar el máximo rendimiento al procesador. Esta dedicación exclusiva es lo que sigue permitiendo hoy en día que hardware inferior como en el de las consolas no esté tan atrasado con respecto al de los Pcs, que necesitan mucha más potencia para igualar el rendimiento de máquinas inferiores pero con software dedicado y optimizado.
Pero por mucho rendimiento que seas capaz de sacar a un procesador llega un momento en el que esa diferencia de potencia deja de ser rentable en términos de productividad. Los programas se volvieron tan complejos, tan difíciles de adaptar al cambiante hardware y tan lentos de programar que fue necesario desarrollar un nuevo paradigma: los lenguajes de alto nivel.
Estos lenguajes se basan en describir una serie de operaciones y flujos de control legibles para humanos aunque no para los ordenadores, así no es necesario ser programador para entender que el siguiente código calcula la suma de los 10 primeros números naturales (empezando desde 1):
int acum=0;
for(int i=1;i<=10;i++){
acum=acum+i;
}
Hay multitud de lenguajes de este tipo: C, Pascal, ADA, Cobol, Fortran… cada uno con sus diferencias que los hacen idóneos para una u otra tarea, pero todos tienen algo en común: necesitan ser compilados. Es decir, la máquina no entiende el texto que compone nuestro programa, dicho texto debe ser traducido a las instrucciones disponibles para el procesador, y más concretamente a las códigos numéricos que las representan. De esta traducción se encargan un tipo de programas llamados compiladores, y su tarea es bastante compleja.
Por un lado deben tratar sintácticamente y semánticamente nuestro programa para “entender” lo que hace, y después debe construir el código máquina equivalente. Dicha construcción muchas veces no es tan eficiente como la que podría escribir un ser humano capaz de hacerlo, pero lo cierto es que no hay muchos humanos de esos, y su tiempo se paga en tinta de impresora. Posteriormente al compilador hay otro paso, el Linkeador, que se encarga de unir nuestro programa con otros que ya estuvieran en nuestro sistema, por ejemplo las llamadas al sistema operativo para leer/escribir en la memoria RAM, Disco Duro, Tarjeta de Video/Sonido, etc…
Los programas que utilizamos por lo general ya vienen compilados, no es algo que hagamos nosotros, eso tiene el inconveniente de que están compilados para un subconjunto estándar de instrucciones/registros que todos los fabricante de compatibles cumplen. Es por ello que no estaremos aprovechando el 100% de nuestra máquina, ya que para hacerlo deberíamos usar un compilador específico para ella. Para tratar de paliar este problema los compiladores modernos son capaces de producir un código único que intente aprovechar las ventajas de las diversas configuraciones hardware, pero sigue siendo más ineficiente que una compilación específica y es el origen de muchos de esos bugs que solo se descubren tras la publicación del programa.
La mayoría de programas que usamos están programados mediante lenguajes compilados, sin embargo estos lenguajes tienen un defecto. Cada vez que modificamos algo del programa hay que volver a compilarlos/linkarlos, y eso puede llegar a tardar mucho tiempo. Eso es un tostón en tiempo de desarrollo cuando los cambios son frecuentes, por ello nacieron los lenguajes interpretados.
Estrucutra básica de un proceso de compilación (C en este caso)
Los lenguajes interpretados son aquellos que van traduciendo sus instrucciones legibles para humanos a código máquina línea a línea, según se va llegando a ellas. De esa manera podemos parar la ejecución en cualquier momento, modificar nuestro programa y continuar su ejecución como si tal cosa. Esto en cierta medida también puede hacerse con los programas compilados, pero con muchas restricciones y solo si habilitamos ciertas opciones durante su compilación que los hace un poco más ineficientes.
Otra de las grandes ventajas de los lenguajes interpretados es su flexibilidad. Dado que son traducidos a código máquina a la vez que se ejecutan no necesitan saber de antemano cual es el tipo de las variables (el espacio donde guardamos los datos que vamos a necesitar posteriormente) que vamos a usar. Así en un mismo espacio podremos guardar un texto, un número, una fecha, o incluso una llamada a otra parte del programa. Igualmente las clases que definimos como vimos en la parte de programación orientada a objetos pueden mutar en tiempo de ejecución.
Como resultado de todo esto nuestros programas pueden ser más flexibles escribiendo menos código, pero como contrapartida muchos de los errores que cometamos no los descubriremos hasta poner en marcha el programa, y el código puede llegar a ser mucho más enrevesado y difícil de entender / modificar.
Otra desventaja de este modelo es que por lo general los programas que se ejecutan de esta manera son considerablemente más lentos que los compilados. Aunque su rendimiento está avanzando a pasos agigantados gracias a avances como la compilación JIT (Just In Time) que compila la línea a ejecutar solo cuando se necesita y la deja ya compilada por si se volviera a ella a no ser que la modifiquemos.
El ejemplo más evidente de lenguaje interpretado es el clásico BASIC (mas bien es una extensa familia de lenguajes), pero el más en boga es Javascript (nada que ver con Java) Es un lenguaje que se utiliza por multitud de páginas web, ya que los navegadores incluyen un intérprete para poder ejecutarlo. No solo eso, desde que Google sacó su motor de Javascript V8 su rendimiento es tan impresionante que unido a su flexibilidad Javascript está imponiéndose también en la parte servidora, consiguiendo rendimientos hasta 10 veces superiores a los de servidores web tan asentados como Apache.
Muchos empezamos nuestros pinitos en la programación con una pantalla parecida.
Volviendo a los lenguajes compilados, otro de sus problemas es que a pesar de sus intentos de independizar el lenguaje del código máquina que generan para que un mismo programa funcione en cualquier procesador y cualquier sistema operativo lo cierto es que esta independencia nunca se ha logrado, y sólo los programas más sencillos han logrado eso tan soñado por los programadores de escribelo una vez, ejecútalo donde sea. Para solventar esto es para lo que nació Java.
La ida que subyace bajo Java es la de crear una máquina virtual, inexistente pero con características comunes y definidas, escribir programas para dicha máquina y que cada procesador y sistema operativo disponga de un programa intermedio que traslade las operaciones realizadas sobre ese hardware virtual al hardware real. Ese programa intermedio es lo que se llama Máquina Virtual Java (JVM), y es lo que necesitas instalar para jugar a Minecraft
El concepto es parecido al programar para pongamos… una Super Nintendo, y luego ejecutar dicho programa en un emulador en nuestro PC, nuestra Wii U, nuestra PSP o incluso… nuestra Super Nintendo. Nuestro programa funcionará bien siempre que la Super Nintendo esté bien emulada.
Así pues Java no está pensado con el rendimiento en mente, sino con la compatibilidad, ya que cuando compilamos Java estamos creando un código máquina para un hardware inexistente, que cuando se ejecute debe convertirse al código máquina real.
Y aunque Java es famoso por su lentitud lo cierto es que no lo es tanto, al menos estrictamente hablando. Es cierto que la JVM tarda bastante en arrancarse, pero una vez arrancada el rendimiento de sus programas no tiene tanto que envidiar a lo conseguido por C. Lo que ocurre es que a su vez hay otros factores que influyen en lo pesado de su ejecución, los cuales acaban dando la razón a sus detractores, y al final el resultado, que es lo que importa, es que ciertamente Java es lento.
– Java está pensado para ejecutarse en cualquier parte, desde un PC a una cafetera, literalmente, y la JVM está limitada a las funciones comunes de este hardware tan dispar, es por ello que normalmente desde Java no puedan accederse ni a las instrucciones más avanzadas de nuestro procesador, ni a hardware especializado como las tarjetas gráficas. Sí que hay formas de acceder a estas funciones, pero de forma limitada y a costa de perder la compatibilidad.
– En Java la gestión de memoria está automatizada. Eso ha hecho que la gestión de la misma sea más sencilla, construir los programas sea más rápido y seguro y por tanto barato. Pero el hecho de no tener que gestionarla ha vuelto “vagos” a los programadores, y me incluyo, lo que hace que la malgasten y no es extraño que durante la ejecución de los programas Java su uso aumente y aumente hasta reventar. En nuestro descargo diré que no es fácil saber gestionar bien algo que resulta transparente.
– La JVM es muy pesada, ocupa mucho, tarda bastante en cargar y hasta que no lo hace no empieza a ejecutarse nuestro programa. Desde hace tiempo se dice que se está trabajando para hacerla mas modular y que solo cargue aquellas partes que nuestro programa necesite. Otro problema es que cada programa Java en ejecución necesita de su propia JVM, aunque ejecutes dos veces el mismo programa. Tambien se está trabajando en que una JVM pueda dar servicio a varios programas Java a la vez. Pero ya sabeis: promesas, promesas…
Así pues ¿ Por qué Notch eligió java para programar Minecraft? Pues básicamente porque era el lenguaje con el que estaba acostumbrado a programar. Seguramente si hubiera sabido en lo que se convertiría hubiera escogido otro lenguaje. Por otra parte Java tiene una ventaja indirecta: es muy fácilmente descompilable. Esto es: obtener el programa legible para los humanos a partir de su código ejecutable. Eso posibilitó la creación de Mods desde su mismo lanzamiento pre-alpha, y dado que se tiene acceso a su código fuente, las posibilidades de modificación no están restringidas como en otros juegos sino que son infinitas como sus mundos.
En fin, espero haber aclarado las dudas que nunca tuvisteis sobre los diversos tipos de lenguaje de programación y su evolución. Lo interesante de esta evolución es que aunque los lenguajes cambian, maduran, se abandonan… sus paradigmas permanecen como permanecen los problemas para cuya solución se idearon. No se sabe que fue antes: si lenguaje o pensamiento, pero lo que os puedo asegurar es que aprender un lenguaje de programación con un enfoque diferente al que estáis acostumbrado os cambiará la forma de razonar.