DIVULGAJARE: MICROPROCESADURIZATE
Si en el anterior artículo hablábamos de las diferencias entre la programación Procedural y la Orientada a Objetos lo siguiente es hablar de los diferentes tipos de lenguajes en base a cómo consiguen que el ordenador entienda y ejecute sus instrucciones. Pero para poder comprenderlo bien, y para que pueda presumir sobre estos conocimientos que ya estaban desfasados cuando los aprendí en la facultad de informática, antes haremos una paradita para conocer como funciona un microprocesador. Es importante resaltar lo de que todo lo que voy a contar es de cuando el mar muerto solo estaba enfermo, por lo que las cifras que voy a comentar seguro que están más que superadas, pero seguro que la esencia del funcionamiento sigue siendo la misma.
Lo primero que hay que saber es que, hasta que la computación cuántica lo revolucione todo, los microprocesadores no son más que un complejísimo circuito eléctrico con capacidad de transmitir, transformar y almacenar dos voltajes diferentes que son lo que comúnmente llamamos 0s y 1s.
Así un microprocesador suele contar con unos pocos registros internos, tan pocos que tienen nombre propio y se listan en la documentación del mismo, con capacidad para almacenar 8, 16,32… bits y una memoria MUY limitada (unos pocos KB) donde se guardaran las instrucciones y datos que se van ejecutar.
El registro más importante de todos se llama Program Counter (PC) y a cada tick del reloj interno del procesador, cuya velocidad se mide en los conocidos Megaherzios, incrementa en uno su contenido, va a la posición de memoria indicada por dicho número, extrae la instrucción allí almacenada y la coloca en la circuitería, que a base de transistores que dejan pasar o no la corriente consigue que dicha instrucción se ejecute.
Dichas instrucciones son de una simplicidad insultante, del estilo:
– Mueve el contenido del registro A al registro B
– Suma el registro A al registro B y el resultado déjalo en C
– Si el registro A contiene un 0 ejecuta la siguiente instrucción, si no salta a la siguiente.
– Pon en contenido del registro A en el Program Counter
– Almacena el contenido del registro A en la posición de memoria indicada por B.
– NoOP: No hacer nada, como veremos en algunos casos esta instrucción es necesaria
Con este juego de instrucciones tan sencillo tenemos una máquina de Turing universal, es decir, que cualquier programa informático que se nos pueda ocurrir puede transformarse a una secuencia de estas instrucciones.
Esquma básico de un microprocesador. En este esquema aparece tambien la unidad Aritmético-lógica. Un microprocesador auxiliar especializado en operaciones matemáticas tan complicadas como las multiplicaciones de números decimales, algo nada sencillo de representar mediante números binarios que tienen una cantidad fija de bits
Quizás os estéis preguntando cómo caben juegos de gigas y gigas si la memoria interna del procesador es tan escasa. La memoria se encuentra dividida de manera lógica( es decir que no es una división real, sino una convención) en páginas, de tamaño cada vez más grande según ha ido avanzando la informática. Dichas páginas se mueven en bloque entre las diferentes memorias del ordenador, que son (ordenadas de más a menos rápidas, pero también de mayor a menor coste y de menor tamaño a mayor):
– Memoria interna
– Caché L1
– Cache L2
– Cache L3
– RAM
Cuando el procesador necesita acceder a una posición de memoria que no está en las páginas cargadas en ese momento en la memoria interna, pide a la Cache L1 que se las pase, si la Cache L1 tampoco las tiene se las pide a la L2 y así sucesivamente. Por cierto quizás os hayáis dado cuenta de que debido a este mecanismo y al funcionamiento del Program Counter, el tamaño de éste determina la cantidad de memoria máxima direccionable, es por ello que las máquinas de 32 bits pueden acceder como mucho a 3 GB de RAM, desperdiciándose el resto.
El reloj que marca el incremento del PC no puede ser todo lo rápido que los diseñadores del procesador querrían. Por un lado está la ya conocida cuestión de que toda esa electricidad moviéndose provoca el calentamiento del circuito hasta eventualmente fundirlo.
Pero primigeniamente había otra limitación: el reloj no puede ser más rápido que la más lenta de las instrucciones que el procesador es capaz de ejecutar. Porque sí, las instrucciones tienen diferentes velocidades. Mover el contenido de un registro a otro es más rápido que hacer una suma y dejar su resultado en un tercer registro, y esto es más rápido que hacer una comparación y en función de su resultado cambiar o no el contenido del Program Counter. Así pues siendo burros, si nuestra operación más lenta tarda 0.5 segundos, nuestro reloj solo podrá ir a 2 herzios, si sólo tardase 0.25 segundos podría ir a 4Hz y así sucesivamente.
Para evitar esta limitación se crearon los procesadores segmentados que consisten en dividir el circuito eléctrico del procesador en varias etapas estancas en los que las operaciones eléctricas que acaban arrojando el resultado de una instrucción solo se mueven hacia delante:
Esquema de un microprocesador segmentado en el que se pueden distinguir las diferentes etapas.
Las instrucciones más sencillas puede que se salten alguna etapa, y quizás alguna etapa está dedicada en exclusiva a operaciones aritméticas y el resto de instrucciones la obvian.
¿Qué se consigue con esto? Que en cada momento haya en ejecución tantas instrucciones como segmentos esté dividido el procesador (como máximo teórico). Es decir que una instrucción puede entrar al procesador antes de que la anterior haya acabado del todo. Ahora ya podemos acelerar nuestro reloj tan rápido según como sea la velocidad de nuestro segmento más lento.
Antes ejecutábamos una instrucción por tick, ahora en cada tick podemos tener varias instrucciones en marcha. Sin embargo no todo son ventajas. Si no tenemos cuidado una instrucción “rápida” podría adelantar a una lenta que sin embargo debe ejecutarse primero, si una instrucción es de salto o salto condicional las posteriores que ya están dentro del procesador puede que no haya que ejecutarlas. Por ello a veces se introducen instrucciones NoOP para dejar un tiempo de separación entre las que de verdad son relevantes.
Todo ello complica el diseño del circuito una barbaridad, y se llegan a hacer cosas tan bizarras como que cuando entra una instrucción de salto condicional entren al procesador las dos secuencias de instrucciones posibles, para que cuando la condición se resuelva la buena ya vaya adelantada y la otra se descarte. Supongo que ahora ya estaréis comprendiendo porqué dentro de los procesadores también hay bugs.
No sé si todo esto os parecerá avanzado pero esta técnica ya la usaban procesadores como el 8086, con lo que no quiero ni imaginar las técnicas que usaran los modernos multi-core, cell y similares, que creo que tiran más por la idea de tener varios Program Counter trabajando en paralelo, o al menos eso es lo que su nombre sugiere.
En cualquier caso, como vemos las instrucciones que un procesador entiende son únicamente conjuntos de 0 y 1, que internamente son representados mediante dos voltajes diferentes, y que son almacenados para su reutilización en memoria persistente (que no se borre al apagar el ordenador) mediante diferente técnicas: magnéticas como los discos duros normales, ópticas en los CDs… pero en los albores de la informática la memoria se manipulaba manualmente mediante palancas.
¿Quien dijo que las chicas no sabían programar? Mirar a ésta, a saber que GOTY estará pergueñando.
Palanca arriba significaba 1, palanca abajo significaba 0, se programaba moviéndolas y al terminar de “escribir” se encendía el ordenata y que fuera lo que Deus Ex quisiera. Como podréis comprender esto no era una técnica muy eficiente, y solo estaba al alcance de los mejores expertos que estaban manipulando aparatos que ocupaban edificios enteros y costaban millones de dólares. Es por ello que el paso a la utilización de tarjetas y cintas perforadas para representar esos 1s y 0s fue una bendición, ya no había que manipular palancas una a una. Pero el programa seguía siendo ilegible para los humanos.
Es por ello que cada instrucción recibió un nombre memotécnico que se asociaba al conjunto de 0s y 1s que la programaban: MOV para mover, SUM para sumar, GOTO para saltar a una posición de memoria etc…
Así nació el Ensamblador, el lenguaje más rápido de ejecutar de todos porque representa directamente los 0s, y 1s que se convierten en los voltajes que el procesador entiende. A los programas que se encargan de interpretar un programa escrito en un lenguaje para “humanos” y convertirlo en estos 0s y 1s se le llama compilador. El primero fue el compilador de Ensamblador.
Pero tenéis que tener en cuenta que cada microprocesador es diferente, tienen juegos de instrucciones diferentes, y éstas se representan con diferentes juegos de 0’s y 1’s. Diferente cantidad de registros internos y de tamaño variable…. Por lo que un aspecto clave para que un programa sea eficiente en tu ordenador, o que simplemente funcione, es que se haya compilado con un compilador adaptado específicamente a tu procesador.
Y hasta aquí éste tochoartículo divulgativo. Hemos llegado al Ensamblador, el lenguaje más rápido de todos, pero también en el que es más complicado desarrollar pues hasta el programa más sencillo se compondrá de miles/millones/billones de instrucciones en los que un solo error tendrá consecuencias imprevisibles. Para simplificar la tarea de programación se idearon lenguajes mas comprensibles, que son de los que hablaremos en la próxima entrega, si es que aun estais despiertos.