Logo de Torre de Babel
Portada Libros Diseño web Artículos Glosario RSS
Buscar

Programación y paralelismo (threads, GPGPU, MPI)

Los ordenadores no tendrían utilidad alguna si no existiesen programadores que creasen software que les hiciese funcionar y ofrecer a los usuarios finales aquello que necesitan. Dicho software puede tomar distintas formas, desde el firmware que se incluye en ROM (o alguna variante PROM/EPROM/etc.) y se encarga de la puesta en marcha y ofrece los servicios más básicos hasta las aplicaciones de control de procesos o diseño asistido, pasando por los sistemas operativos y los propios compiladores e intérpretes de multitud de lenguajes.

En los primeros ordenadores el software se ejecutaba en forma de procesos por lotes: cada programador ponía su tarea en cola y esperaba a que llegase su turno para obtener la salida correspondiente. Cada tarea era intrínsecamente secuencial, en el sentido de que no ejecutaba más de una instrucción de manera simultánea. Posteriormente llegaron los sistemas de tiempo compartido capaces de atender interactivamente a varios usuarios y, en apariencia, ejecutar múltiples tareas en paralelo, si bien la realidad era que el sistema operativo se encargaba de que el procesador fuese saltando de una tarea a otra cada pocos ciclos, consiguiendo esa ilusión de paralelismo o multitarea.

Para los programadores de un sistema operativo la implementación del tiempo compartido implicaba codificar algoritmos relativamente complejos, de los cuales el más conocido es Round Robin, capaces de seleccionar en cada momento el proceso que ha de pasar a ejecutarse e impedir situaciones indeseadas como, por ejemplo, que una tarea no obtenga nunca tiempo de procesador. Con el tiempo los microprocesadores implementaron en hardware una gran cantidad de lógica que facilitaba el intercambio rápido de tareas, haciendo más fácil el trabajo de los programadores de sistemas.

Los programadores de aplicaciones, por el contrario, siguieron durante décadas diseñando software asumiendo que sus programas obtenían el control total del ordenador, implementando los algoritmos para que se ejecutasen secuencialmente de principio a fin y dejando que el sistema operativo los interrumpiese cada cierto tiempo para volverlos a poner en marcha un instante después, todo ello a tal velocidad que los usuarios tenían la sensación esperada: la aplicación les atendía de manera continua sin problemas. Esto es especialmente cierto en aquellos programas en los que existe comunicación con el usuario, ya que la mayor parte del tiempo se encuentran a la espera de una acción por parte de éste: una entrada por teclado, la pulsación de un botón, etc.

Dado que los microprocesadores solamente eran capaces de ejecutar una tarea en un instante dado, antes de que contasen con pipelines primero, varias unidades funcionales después (procesadores superescalares) y múltiples núcleos finalmente; los programadores de aplicaciones que querían ejecutar más de un trabajo en paralelo recurrían a diversos trucos, como el uso de interrupciones. De esta forma era posible, por ejemplo, imprimir un documento o realizar otra tarea lenta mientras se seguía atendiendo al usuario. De ahí se pasó a la programación con múltiples hilos o threads, de forma que un programa podía ejecutar múltiples secuencias de instrucciones en paralelo. Esos hilos eran gestionados por el sistema operativo mediante el citado algoritmo de tiempo compartido: el procesador seguía ejecutando únicamente una tarea en cada instante.

Aunque el multiproceso simétrico existe desde la década de los 60 en máquinas tipo mainframe, no fue hasta finales de los 90 cuando los servidores y estaciones de trabajo con zócalos para dos procesadores se hicieron suficientemente asequibles como para adquirir cierta popularidad. Estas máquinas contaban con dos procesadores y, en consecuencia, tenían capacidad para ejecutar dos tareas con paralelismo real. Obviamente los algoritmos de tiempo compartido seguían estando presentes, ya que el número de procesos en ejecución suele ser mucho mayor, pero el rendimiento era muy superior al ofrecido por los ordenadores personales.

El escenario de la computación personal ha cambiado drásticamente desde el inicio del nuevo milenio. Si en los años previos los fabricantes de microprocesadores competían casi exclusivamente en velocidad, lo que permitía al software aprovechar la mayor potencia sin trabajo adicional por parte del programador, en la última década han aparecido los microprocesadores multinúcleo y los procesadores gráficos con capacidades GPGPU (General-Purpose Computation on Graphics Hardware), lo que ha traído el final del Free Lunch para los programadores. Ahora aprovechar la potencia de un ordenador implica necesariamente el uso de todo ese paralelismo de una forma u otra.

A los microprocesadores multinúcleo y las GPU habría que sumar una opción cada vez más alcance de cualquiera: los cluster de ordenadores. Si bien antes eran una opción reservada a centros de supercomputación, en la actualidad hay multitud de usuarios que disponen de varias máquinas conectadas en red local, lo cual abre las puertas (con el software adecuado) a la configuración en cluster para aprovechar el paralelismo y ejecutar software distribuyendo el trabajo entre múltiples máquinas.

En conjunto la popularización de estos mecanismos de paralelización implican la aparición de un nuevo modelo de diseño de software y el necesario reciclaje de los programadores. Ya no basta con escribir sentencias que se ejecutarán una tras otra, sin más, siendo preciso planificar una arquitectura de mayor complejidad si se quiere obtener el mayor beneficio del hardware disponible. Algunas ideas al respecto:

  • Distribuir el trabajo a realizar entre varios ordenadores conectados en red local es una tarea relativamente simple gracias a MPI (Message Passing Interface), una interfaz de la que existen múltiples implementaciones para diferentes sistemas operativos. Personalmente he usado Open MPI, una implementación Open Source de MPI, y su funcionamiento en un cluster con red Ethernet es muy simple.
  • En cada una de los ordenadores del cluster (o el único ordenador si no se dispone de uno) el software ha de estructurarse de forma que se aprovechen al máximo los núcleos con que cuente el microprocesador. Esto implica trabajar con múltiples hilos, algo realmente simple en la plataforma Java o la plataforma .NET y que en el caso de lenguajes como C/C++ significa recurrir a bibliotecas como POSIX threads.
  • Si los ordenadores en que se ejecute el software cuentan con hardware de vídeo de última generación, todos aquellos procesos con alto nivel de paralelismo tipo SIMD (Single Instruction Multiple Data) pueden acelerarse hasta un punto realmente sorprendente, ya que las GPU pueden realizar en un ciclo operaciones en paralelo sobre 256, 320 e incluso más operandos, mientras que un microprocesador tendría que recurrir a un bucle y consumir un número mucho mayor de ciclos. Hasta no hace mucho programar tareas a ejecutar en una GPU tenía el inconveniente de que cada fabricante ofrecía su solución propietaria: CUDA en el caso de NVidia (es la opción que he usado en algunas ocasiones y sobre la que publiqué un artículo hace aproximadamente un año en una conocida revista española) o Stream en el de ATI. Desde hace algo más de un año existe otra opción: OpenCL, un estándar que no solamente funciona con GPU de diferentes fabricantes sino que, además, está diseñado para repartir tareas entre CPU y GPU. Una opción adicional, siempre que se trabaje sobre Windows, es DirectCompute, una API similar a OpenCL que hace posible la programación GPGPU sin que importe el fabricante de hardware.

El Grupo Khronos ha hecho pública recientemente la versión 1.1 de OpenCL y tanto ATI como NVidia ofrecen controladores para esta API, por lo que posiblemente sea la mejor alternativa si uno no quiere atarse a la oferta de una determinada empresa.

Aprender a usar estas herramientas será (sino lo es ya) un requisito indispensable para cualquier programador, no exclusivamente para los desarrolladores de software de sistemas. El proceso, sin embargo, será lento y mientras tanto el hardware de que disponemos en nuestro escritorio estará muy infrautilizado, ya que muy pocas aplicaciones aprovechan su potencia. El sistema operativo utiliza los múltiples núcleos de los actuales microprocesadores para repartir la carga de trabajo, pero apenas hace uso de la GPU. De hecho las GPU, salvo en el caso de los juegos y algunas aplicaciones específicas de gráficos/vídeo, son el recurso mas desaprovechado. La próxima generación de navegadores, no obstante, promete hacer uso de esa potencia a través de la aceleración por hardware de la composición de páginas.


Publicado el 14/9/2010

Curso de shaders

Torre de Babel - Francisco Charte Ojeda - Desde 1997 en la Web