24 de agosto de 2010

Código en C dentro de un programa Perl

Hola, siguiendo el hilo de los comentarios a la entrada anterior de Álvaro, hoy pongo un ejemplo de como incluir una o más subrutinas escritas en C dentro de un texto en Perl, recurriendo al módulo Inline:
 #!/usr/bin/perl -w 
 # programa 7 
 use Inline C; 
 use strict; 
 my $BASE = 0.987654321; 
 my $EXP = 1000; 
 print "# $BASE^$EXP = \n"; 
 print "# ".potencia_perl($BASE,$EXP)." (perl)\n"; 
 print "# ".potencia_C($BASE,$EXP)." (C)\n"; 
 sub potencia_perl 
 { 
    my ($base,$exp) = @_; 
    my $result = $base; 
    for(my $p=1; $p<$exp; $p++) { $result *= $base; } 
    return $result; 
 } 
 __END__  
 ### alternativa en C 
 __C__ 
 #include <stdio.h> 
 float potencia_C( double base, double exp )  
 { 
    double result = base; 
    int p; 
    for(p=1; p<exp; p++) 
    {  
       result *= base; 
    }    
    return result; 
 } 

La salida obtenida pone en evidencia que diferentes lenguajes suelen tener diferentes precisiones a la hora de representar números y operar con ellos:

$ perl prog7.pl 
# 0.987654321^1000 = 
#  4.02687472213071e-06 (perl)
#  4.02687464884366e-06 (C)

5 comentarios:

  1. Solo dos matices sobre el artículo. El primero es sobre la precisión: no depende del lenguaje sino de las librerías matemáticas que ellos usan.

    Y segundo matiz, hay que indicar que la solución de integrar código C dentro de Perl, es, normalmente, para llamar a librerías externas o acelerar una parte del proceso que Perl lo hace de forma muy lenta, como es este caso.

    En concreto, y usando el módulo Benchmark, sale

                   Rate Perl (sub)          C
    Perl (sub)   3576/s         --       -99%
    C          384615/s     10656%         --

    Sí: C es más de un 10.000% más rápido que Perl.

    Ahora bien... esto no siempre es cierto. Se pierde una cierta cantidad de tiempo entre el paso de parámetros, la llamada a la función y la recogida de los resultados. Y puede ser apreciable. Por ejemplo, si ejecutamos un benckmark con la función de exponenciación de Perl (**) nos quedaremos asombrados:

                    Rate Perl (sub)          C  Perl (**)
    Perl (sub)    3584/s         --       -99%      -100%
    C           389610/s     10772%         --       -81%
    Perl (**)  2093023/s     58305%       437%         --

    Así que usando la propia función de exponenciación de Perl, el resultado es que es un 58.305% más rápida que la función potencia_Perl() y un 437% más rápida que la función potencia_C().

    Naturalmente, esto es solo un ejemplo. En la vida real sí que encontraremos muchos casos en los que sí merece la pena hacer un trabajo extra para rehacer una parte de nuestro programa en C. Usaremos los perfiladores (como Devel::NYTProf) para averiguar qué partes de nuestro programa son los más lentos.

    ResponderEliminar
  2. Gracias por tu comentario,
    quisiera añadir que también la forma de representar internamente los números afecta a la precisión numérica, como se demuestra en el ejemplo de
    http://www.eead.csic.es/compbio/material/bioinfoPerl/node79.html

    Bruno

    Enlaces interesantes sobre esto:
    http://docstore.mik.ua/orelly/perl/prog3/ch02_06.htm
    http://perldoc.perl.org/perlnumber.html
    http://stackoverflow.com/questions/2056681/does-scientific-notation-affect-perls-precision

    ResponderEliminar
  3. Suele ocurrir que la mejor forma de hacer que algo vaya más rapido no sea pasandolo a C sino empleando un mejor algoritmo:

    #!/usr/bin/perl

    use strict;
    use warnings;

    use Inline 'C';
    use strict;
    my $BASE = 0.987654321;
    my $EXP = 1000;
    print "# $BASE^$EXP = \n";
    print "# ".potencia_perl($BASE,$EXP)." (perl)\n";
    print "# ".potencia_perl2($BASE,$EXP)." (perl2)\n";
    print "# ".potencia_C($BASE,$EXP)." (C)\n";
    print "# ".potencia_C2($BASE,$EXP)." (C2)\n";

    sub potencia_perl {
    my ($base,$exp) = @_;
    my $result = $base;
    for(my $p=1; $p<$exp; $p++) {
    $result *= $base;
    }
    return $result;
    }

    sub potencia_perl2 {
    my ($base, $exp) = @_;
    my $r = 1;
    while ($exp) {
    $r *= $base if $exp & 1;
    $exp >>= 1;
    $base *= $base;
    }
    $r;
    }

    use Benchmark 'cmpthese';

    my @base = map rand(1.5), 1..1000;
    my @exp = map rand(5000), @base;

    cmpthese(-1, { perl => sub { potencia_perl ($base[$_], $exp[$_]) for 0..$#base },
    perl2 => sub { potencia_perl2($base[$_], $exp[$_]) for 0..$#base },
    C => sub { potencia_C ($base[$_], $exp[$_]) for 0..$#base },
    C2 => sub { potencia_C2 ($base[$_], $exp[$_]) for 0..$#base } } );

    __END__
    ### alternativa en C
    __C__
    #include
    double potencia_C( double base, int exp ) {
    double result = base;
    int p;
    for(p=1; p>= 1;
    base *= base;
    }
    return r;
    }

    -------

    que resulta en...

    Rate perl C perl2 C2
    perl 1.96/s -- -85% -99% -100%
    C 13.3/s 580% -- -91% -99%
    perl2 142/s 7117% 961% -- -86%
    C2 982/s 49961% 7262% 594% --

    ResponderEliminar
  4. ¿Dónde está potencia_C2()? Parece que el código está cortado.

    Solo un detalle: si

    sub potencia_perl2 {
        my ($base, $exp) = @_;
        my $r = 1;
        while ($exp) {
            $r *= $base if         $exp & 1;
            $exp >>= 1;
            $base *= $base;
        }
        $r;
    }

    la escribimos de esta manera:

    sub potencia_perl2 {
        my ($r, $base, $exp) = (1, @_);
        while ($exp) {
            $r    *= $base if $exp & 1;
            $base *= $base if $exp >>=1;
        }
        $r;
    }

    mejoramos los tiempos un 2% (en mi equipo).

    De todas maneras, sigue estando muy lejos de los tiempos de '**' (en mi ordenador, '**' es un 1405% más rápido que potencia_perl2).

    Ahora bien, si lo pasamos a C:
    float potencia_C2( double base, int exp ) {
        double result = 1;
        while(exp) {
            if (exp &   1) { result *= base; }
            if (exp >>= 1) { base *= base; }
        }
        return result;
    }

    Entonces ya empezamos a obtener valores interesantes:
    Perl (sub) 3523/s
    Perl (sub2) 132332/s
    Perl (sub3) 136533/s
    C 378508/s
    C2 1654153/s
    Perl (**) 1942492/s (solo un 17% más rápido que C2)

    Mi equipo es un AMD Turion(tm) 64 X2 Mobile (x86_64) a 1.6Ghz

    ResponderEliminar
  5. Aqui esta el codigo completo con algunas variaciones más:

    http://pastebin.com/kRbW24kr

    En mi ordenador (un no muy reciente Pentium 4 a 3GHz), las diferencias entre perl2, perl3 y perl4 son mínimas y a veces gana uno y otras veces otro aunque en promedio parece que perl4 es el más rápido y perl2 el más lento.

    ResponderEliminar