lunes, 1 de febrero de 2010

Victoria de Ruby resolviendo Fibonacci (vs Python)

Conversando rápidamente con un 'amix' en twitter (@Yaraher - Alvaro Pereyra) me dí con la sorpresa de que el rendimiento de Ruby se ha incrementado dramáticamente, así que lo sometí a una sencilla prueba comparándolo con Python: la archi conocida secuencia fibonacci.

El vídeo muestra una ventana de Terminator, dividida en 2 áreas horizontales y 4 verticales.  En la primera sección vertical se ejecuta la secuencia fibonacci empleando Python2.6, en la segunda con Python 3.1, en la tercera con Ruby 1.8 y en la cuarta con Ruby 1.9.1.





En primer lugar las tareas se ejecutan simultáneamente, luego se ejecuta cada una individualmente y así poder apreciar su rendimiento como proceso mandatorio.


Código Python

def fib(n):
 if n == 0 or n == 1:
         return n
 else:
  return fib(n-1) + fib(n-2)

for i in range(36):
 #print "n=%d => %d" % (i, fib(i))
 print ("n=",i," =>",fib(i))


Código Ruby

def fib(n)
 if n == 0 || n == 1
              n
 else
  fib(n-1) + fib(n-2)
 end
end

36.times do |i|
 puts "n=#{i} => #{fib(i)}"
end




El resultado de la prueba fue una victoria absoluta para Ruby 1.9.1 (con unos impresionantes 8.xx segundos, pero solamente cuando es el proceso principal que emplea el CPU, cuando no es así el rendimiento es similar al de Python), ambas versiones de Python van muy parejas sin mucho cambio en el rendimiento (19 y 21 segundos) y si están empleando 1.8, dejen de hacerlo: es malísimo (62 segundos)


Haskell: paralelo rendimiento funcional

En esta ocasión compartiré algo sobre Haskell, un maravilloso lenguaje de programación que sigue el paradigma funcional y que venimos empleando en algunos proyectos de la empresa (ICTEC SAC), tratando de elevar el rendimiento de nuestras aplicaciones pero sin perder esa simplicidad que Python nos ofrece; es así que daré una pequeñísima introducción al lenguaje tomando como "hilo" sus características de paralelismo, es decir el cálculo simultáneo de operaciones en diferentes núcleos de uno o varios procesadores (ideal para clusters o computación de alto rendimiento).

Programar en Haskell implica adentrarse en la programación declarativa y basada en funciones matemáticas, a diferencia de la programación imperativa (donde se encuentran la mayoría de lenguajes de programación) que es más bien procedimental y algorítmica.  De programación funcional hablaré en un artículo más adelante porque hoy, me quiero concentrar en el potencial de Haskell para los desarrolladores y particularmente en emplear los dos núcleos de mi Core2Duo a demanda.  Recuerden que empleo Ubuntu Jaunty, por lo que los procedimientos de instalación pueden ser diferentes en otras distros, pero el código es aplicable en cualquier SO.



Por qué programar en Haskell? Hay muchas razones, pero una de las mejores es el rendimiento, para ello revisen este enlace http://shootout.alioth.debian.org/u64/which-programming-languages-are-fastest.php ,sin obviar el código corto, limpio, con mayor mantenibilidad, etc.

Lo usual es que Haskell no venga integrado en nuestro SO, pero gracias a los repositorios que ofrece Ubuntu instalarlos es muy simple
    sudo apt-get install ghc6 libghc6-parallel-prof
   
Hecho esto, debemos saber que Haskell nos ofrece un intérprete interactivo y claro, un compilador.  Para acceder al intérprete interactivo lo hacemos con:
    ghci

Listo! Ya lo tenemos instalados, ahora a emplearlo y en esta ocasión he escogido la clásica secuencia fibonacci para demostrar el rendimiento de este lenguaje


    import Control.Monad
    import Text.Printf

    fib :: Int -> Int
    fib 0 = 0
    fib 1 = 1
    fib n = fib (n-1) + fib (n-2)

    main = forM_ [0..35] $ \i -> 
            printf "n=%d => %d\n" i (fib i)
       



    * Llamamos a los módulos Control.Monad y Text.Printf, el primero nos da acceso a acciones abstractas datatipadas,  mientras que el segundo al conocido printf que nos permite imprimir texto con formato
    * Definimos la función fib indicando que recibiremos un enetero (Int) y recibiremos otro (Int)
    * Dentro de fib manipulamos los datos y establecemos que n es el valor a recibir y el resultado de su operación el (valor) de retorno
    * Emplean main establecemos un bloque que puede invocar al módulo, que gracias a la función forM_ (parte de Control.Monad) nos permite iterar sobre los 35 primeros primos, cada valor va pasando al printf que como argumentos tiene la llamada al módulo fib.
    * Grabamos el archivo con extensión .hs y salimos al intérprete (fibo.hs)

Probaremos nuestro ejercicio directamente desde el intérprete Haskell, con lo cual desde el mismo directorio donde grabamos nuestro archivo lo invocamos con ghci.  Dentro de este, empleamos una comprensión de listas para invocarlo:
    ghci fibo.hs
    [fib x | x <-[0..35]]
    *Al cabo de unos segundos aparecerá una lista con la secuencia fibonacci
    *Salimos con [CTRL]+Z
   
El problema de lo que acabamos de hacer es que nos ha tomado algunos valiosos segundos y eso NO ES ACEPTABLE, tratemos -ahora- de compilar la aplicación
    ghc -o fibo fibo.hs
    *Esperamos unos momentos y ya tenemos un ejecutable listo para correr
    ./fibo
   
    *Estoy empleando la aplicación time para medir los tiempos de ejecución y consumo de CPU, lo pueden instalar con
    sudo apt-get install time
   
    *Lo empleamos así
    /usr/bin/time ./fibo
   
    *Nos presenta el resultado de los 35 números en 3.04 seg y con un consumo de CPU de 99%

Eso es rápido? Contra qué lo comparamos? Bueno, ahora lo entenderán porque voy a hacer lo mismo con un programa en Python


 def fib(n):
     if n == 0 or n == 1:
         return n
     else:
         return fib(n-1) + fib(n-2)

 for i in range(36):
     print "n=%d => %d" % (i, fib(i))


    *Lo grabamos como fibo.py y ejecutamos
    /usr/bin/time python fibo.py
   
    *El resultado se obtuvo en 18.79 seg y con un consumo de CPU de 100%
   
Ahora ya tenemos una referencia, Haskell ejecutó la aplicación compilada 6 veces más rápido que en Python, pero digamos qué hay de C?

 #include

 int main()
 {
      unsigned int i=0,j=0,sum=1,num=0;
      num=9227466;
 
      while(sum < num)
      {
         printf("%d ",sum);
         i=j;
         j=sum;
         sum=i+j;
       }
 }

   
    *Los grabamos como fibo.c,compilamos y ejecutamos
    gcc -o fibo-c fibo.c
    /usr/bin/time ./fibo-c

   
    *El resultado se obtuvo en 0.0 seg y con un consumo de CPU de 0%
   
"Pero mejor programemos en C", dirán algunos de ustedes, pero quienes desarrollamos sabemos que hacerlo así implicaría manejar engorrosos procesos que harán muy largo y lento el trabajo y es por ello que existen otros lenguajes de programación: hacer nuestra vida más simple (cuando es posible).  Ahora, tratemos de hacer algo más interesante aún: elevar el rendimiento de nuestra aplicación Haskell hasta menos de un segundo; Gracias a que tengo 2 núcleos puedo dividir las operaciones que realiza el programa para que cada uno de ellos cumpla una tarea y así acelerar el proceso.


 import Control.Monad
 import Text.Printf
 import Control.Parallel

 fib :: Int -> Int
 fib 0 = 0
 fib 1 = 1
 fib n = l `pseq` r `pseq` l+r
         where
         l = fib (n-1)
         r = fib (n-2)

 main = forM_ [0..35] $ \i -> 
         printf "n=%d => %d\n" i (fib i) 



           
    *El programa es el mismo que el original, pero se ha agregado el módulo Control.Parallel y dentro de fib n se ha invocado a la función pseq para que cada argumento pase por un proceso en cada núcleo
    *Lo grabamos como fibo-m.hs, compilamos y ejecutamos
    ghc -o fibo-m -O2 --make fibo-m.hs -threaded
    /usr/bin/time ./fibo-m
   
    *El resultado ahora es de 0.41 seg y con un consumo de CPU de 102%
   
La cosa mejoró verdad? Con Haskell podemos manejar con precisión nuestros núcleos y dividir las tareas, optimizando nuestras aplicaciones con suma facilidad, claro que al precio de mayor consumo de CPU, pero en los tiempos en que este se eleva cada día eso ya no es un problema. Los próximos días trataré de introducirlos a este maravilloso lenguaje ya que ahora se deben haber quedado expectantes con nuestras pruebas.