jueves, enero 27, 2011

Consejos para crear aplicaciones de alto rendimiento

jueves, enero 27, 2011 por Martín

Siguiendo con el post anterior, sobre la presentación sobre LMAX en InfoQ, de mi resumen me restaba comentar los seis consejos que ellos daban para la creación de aplicaciones de alto rendimiento que fuesen capaces de gestionar un gran número de transacciones con una latencia muy baja.

Su máxima era: hoy en día con un único hilo puedes jugar con 3 billones de instrucciones por segundo. Con no hacer nada estúpido tendrás 10 mil transacciones por segundo sin problemas. ¿Pero cómo hacemos para no meternos en problemas? Aquí resumo sus consejos:


  1. Mechanical Sympathy. Ser amigos de los sistemas. Saber lo que en realidad está pasando cuando ejecutamos un programa. Hemos llegado a un punto donde hay tantas librerías, estructuras, frameworks que nos llegamos a olvidar de que al final todo va a la memoria, donde será consumido por la CPU y sus núcleos, donde hay diferentes operaciones, unas mejores y otras peores, donde hay diferentes cachés. Todo eso que nos enseñaban en la facultad en Estructura de Computadores y que se nos ha olvidado. Todo eso... sigue siendo importante.

    Es como conducir. Podemos conducir normalmente, no hay ningún problema. Pero si queremos ser un piloto de carreras entonces tienes que saber como funciona un motor. No hace falta que lo sepas fabricar, pero tienes que saber como funciona y conocer el impacto de tus acciones cuando conduces sobre el motor y las diferentes partes del coche.

    Además, si trabajamos en sistema de alto rendimiento debemos estar al tanto de todas los avances y evoluciones. En la presentación se ponen algunos ejemplos:


    • La latencia de la memoria no ha mejorado en los últimos años ya que aunque es más rápida también hay que manejar mayores cantidades y sincronizar el acceso de más CPUs y núcleos.

    • Las redes todos las vemos como algo lento. Pero ojo, una red Ethernet de 10 Gigabit se puede configurar para evitar el Kernel por completo y obtener latencias de microsegundos. Si eso se añaden el uso de UDP en vez de TCP y Multicast, entonces se obtienen niveles throughput realmente buenos.

    • Al contrario de lo que se piensa, las SSDs no son mucho más rápidas que los discos duros para el acceso secuencial a los datos. Los discos normales son rapidísimos para acceso secuencial de datos, por ejemplo para streaming de vídeo. Las SSDs son mucho mejores para cuando hay múltiples hilos accediendo a datos no secuenciales.


    Conocer estas cosas nos hace crear mejores aplicaciones. Esto es algo relacionado con lo que Carlos Bueno, ingeniero de Facebook, define como un programador Full-Stack y cuyo artículo recomiendo la lectura y donde define así a programadores generalistas capaces de realizar aplicaciones no-triviales sabiendo realmente lo que pasa a todos los niveles, datos, software, hardware, redes. No se trata de ser un experto en todas esas áreas sino simplemente de saber las consecuencias de tus acciones a esos niveles.

  2. Mantener el espacio de trabajo en memoria. Hasta hace nada los sistemas operativos de 32 nos limitaban bastante, pero hoy por hoy con los sistemas operativos de 64 bits. la memoria a la que pueden acceder las aplicaciones es brutal. Podemos tener heaps de 32Gb. y quedarnos tan anchos. Siempre que mantengamos los datos y el procesado de esos datos lo suficientemente cerca obtendremos un gran rendimiento.

  3. Escribir código cache-friendly. Es importante conocer la arquitectura de nuestras CPUs. Intentar acomodarse al tamaño de las cachés y mantener tantos datos en caché como se pueda. El tamaño de los objetos es importante. También es importante que datos a los que haya que acceder concurrentemente no estén en la misma línea de caché ya que se crea contención a nivel de la CPU (imaginémonos varios servicios intentando acceder concurrentemente en software al mismo objeto. Lo mismo pasa con múltiples cores y el acceso a la caché).

    Comentan en la presentación que hacer este tipo de aplicaciones de alto rendimiento es como cuando hacías juegos en los 80/90. Hay que conocer muy bien la arquitectura de tus servidores. Un ejemplo práctico, si creas una LinkedList lo más probable es que acabes con una estructura en espaguetti con punteros para aquí y para allá y en la que el compilador no puede hacer absolutamente nada. Si por el contrario utilizas un array, lo más probable es que esos datos acaben en la misma línea de caché con un acceso secuencial y además las CPUs modernas suelen hacer prefetching por lo que el acceso será mucho más rápido.

    Nuevamente, no se trata de ser un experto en todo, sino simplemente no tenerle miedo al hardware y saber las consecuencias de nuestras elecciones de algoritmos, estructuras, etc.

  4. Escribir código limpio y compacto. A las máquinas virtuales les gustan los métodos pequeños. Cuanto más limpio y compacto sea el código más fácil es la tarea de la máquina virtual. (Sobre esto se hace mucho hincapié en el libro de JRockit que recomendaba hace unos días).

    Por ejemplo, comentan como anécdota que un desarrollador de repente quiso eliminar todos los try/catch de una parte de su sistema porque había leído en Internet que eran malos y que degradaban el rendimiento. Lo que en realidad estaba pasando era que el método era enorme, y que el JIT no era capaz de optimizarlo así que se ejecutaba en modo interpretado. (Otra nota, en el 2007 yo mismo escribía un artículo sobre este tema).

    Moraleja: En vez de perseguir fantasmas, mejor pararse a pensar e intentar hacer las cosas simples.

  5. Invertir en el modelo de dominio. Alto rendimiento es equivalente a disponer de un modelo simple que hace exacta y únicamente lo que debe hacer. No se trata de hacer cosas super-inteligentes ni super-innovadoras desplazando bits de aquí para allá. Los principios más básicos de la orientación a objetos, lejos de hacer los programas más lentos, pueden ayudarnos a conseguir alto rendimiento al mantener nuestros programas mucho más simples y compactos.

    Un ejemplo, garantizando que el principio de responsabilidad única se cumple para todas tus clases es una gran victoria para conseguir aplicaciones eficientes y de gran rendimiento. En cuanto las clases empiezan a adoptar responsabilidades que no les corresponden entonces la complejidad aumenta. Los datos y responsabilidades se separan y entremezclan y como consecuencia el rendimiento se degrada.

  6. Escoger el modelo de concurrencia adecuado. ¿Locks u operaciones atómicas? ¿Synchronized u operaciones atómicas? Las operaciones atómicas son mejores y se llevan mejor con la CPU pero son más complejas de implementar y gestionar. Synchronized se traduce en múltiples operaciones de CPU pero una operación atómica se traduce en una única operación. Sin embargo para los programadores es más intuitivo utilizar synchronized. ¿Qué utilizamos entonces? Seguramente depende.

    Es necesario conocer el impacto de nuestro modelo de concurrencia en el hardware. Si hacemos una suma entre múltiples threads utilizando bloqueos por ejemplo, es posible que llegado el momento se produzca una pugna por obtener el control de la CPU entre los hilos que realizan la suma. El sistema operativo nos robará el cotexto para realizar el arbitraje de la pugna, y seguramente aprovechará los ciclos restantes para hacer cualquier otra tarea que no tiene que ver con nuestra aplicación, robándonos todavía más tiempo.



Y con esto termino el resumen de la charla LMAX. Como veis, me ha dado para dos posts y es que me pareció apasionante. Espero que si no se os da bien el inglés os hayan gustado estos posts, y si se os da bien el inglés os recomiendo ver el video, que está realmente bien.

comments

0 Respuestas a "Consejos para crear aplicaciones de alto rendimiento"